Skip to content

Commit aec3708

Browse files
committed
improve trace messages / warnings & add color #866
1 parent 2a2c175 commit aec3708

8 files changed

Lines changed: 73 additions & 17 deletions

File tree

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Generate your schema with the CLI:
156156

157157
.. code:: bash
158158
159-
$ ./manage.py spectacular --file schema.yml
159+
$ ./manage.py spectacular --color --file schema.yml
160160
$ docker run -p 80:8080 -e SWAGGER_JSON=/schema.yml -v ${PWD}/schema.yml:/schema.yml swaggerapi/swagger-ui
161161
162162
If you also want to validate your schema add the ``--validate`` flag. Or serve your schema directly

drf_spectacular/contrib/django_filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
5757
return []
5858

5959
result = []
60-
with add_trace_message(filterset_class.__name__):
60+
with add_trace_message(filterset_class):
6161
for field_name, filter_field in filterset_class.base_filters.items():
6262
result += self.resolve_filter_field(
6363
auto_schema, model, filterset_class, field_name, filter_field

drf_spectacular/drainage.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import functools
3+
import inspect
34
import sys
45
from collections import defaultdict
56
from typing import DefaultDict
@@ -8,9 +9,13 @@
89
class GeneratorStats:
910
_warn_cache: DefaultDict[str, int] = defaultdict(int)
1011
_error_cache: DefaultDict[str, int] = defaultdict(int)
12+
_blue = ''
13+
_red = ''
14+
_yellow = ''
15+
_clear = ''
1116

1217
def __getattr__(self, name):
13-
if not self.__dict__:
18+
if 'silent' not in self.__dict__:
1419
from drf_spectacular.settings import spectacular_settings
1520
self.silent = spectacular_settings.DISABLE_ERRORS_AND_WARNINGS
1621
try:
@@ -33,12 +38,25 @@ def reset(self):
3338
self._warn_cache.clear()
3439
self._error_cache.clear()
3540

41+
def enable_color(self):
42+
self._blue = '\033[0;34m'
43+
self._red = '\033[0;31m'
44+
self._yellow = '\033[0;33m'
45+
self._clear = '\033[0m'
46+
3647
def emit(self, msg, severity):
3748
assert severity in ['warning', 'error']
38-
msg = _get_current_trace() + str(msg)
3949
cache = self._warn_cache if severity == 'warning' else self._error_cache
50+
51+
source_location, breadcrumbs = _get_current_trace()
52+
prefix = f'{self._blue}{source_location}: ' if source_location else ''
53+
prefix += self._yellow if severity == 'warning' else self._red
54+
prefix += f'{severity.capitalize()}'
55+
prefix += f' [{breadcrumbs}]: ' if breadcrumbs else ': '
56+
57+
msg = prefix + self._clear + str(msg)
4058
if not self.silent and msg not in cache:
41-
print(f'{severity.capitalize()} #{len(cache)}: {msg}', file=sys.stderr)
59+
print(msg, file=sys.stderr)
4260
cache[msg] += 1
4361

4462
def emit_summary(self):
@@ -80,17 +98,34 @@ def reset_generator_stats():
8098

8199

82100
@contextlib.contextmanager
83-
def add_trace_message(trace_message):
101+
def add_trace_message(obj):
84102
"""
85103
Adds a message to be used as a prefix when emitting warnings and errors.
86104
"""
87-
_TRACES.append(trace_message)
105+
sourcefile, lineno = _get_source_location(obj)
106+
_TRACES.append((sourcefile, lineno, obj.__name__))
88107
yield
89108
_TRACES.pop()
90109

91110

111+
@functools.lru_cache(maxsize=1000)
112+
def _get_source_location(obj):
113+
try:
114+
sourcefile = inspect.getsourcefile(obj)
115+
lineno = inspect.getsourcelines(obj)[1]
116+
return sourcefile, lineno
117+
except: # noqa: E722
118+
return None, None
119+
120+
92121
def _get_current_trace():
93-
return ''.join(f"{trace}: " for trace in _TRACES if trace)
122+
source_locations = [t for t in _TRACES if t[0]]
123+
if source_locations:
124+
source_location = f"{source_locations[-1][0]}:{source_locations[-1][1]}"
125+
else:
126+
source_location = ''
127+
breadcrumbs = ' > '.join(t[2] for t in _TRACES)
128+
return source_location, breadcrumbs
94129

95130

96131
def has_override(obj, prop):

drf_spectacular/generators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def parse(self, input_request, public):
235235
f'DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" '
236236
f'or any other drf-spectacular compatible AutoSchema?'
237237
)
238-
with add_trace_message(getattr(view, '__class__', view).__name__):
238+
with add_trace_message(getattr(view, '__class__', view)):
239239
operation = view.schema.get_operation(
240240
path, path_regex, path_prefix, method, self.registry
241241
)

drf_spectacular/management/commands/spectacular.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ def add_arguments(self, parser):
4040
parser.add_argument('--validate', dest="validate", default=False, action='store_true')
4141
parser.add_argument('--api-version', dest="api_version", default=None, type=str)
4242
parser.add_argument('--lang', dest="lang", default=None, type=str)
43+
parser.add_argument('--color', dest="color", default=False, action='store_true')
4344

4445
def handle(self, *args, **options):
4546
if options['generator_class']:
4647
generator_class = import_string(options['generator_class'])
4748
else:
4849
generator_class = spectacular_settings.DEFAULT_GENERATOR_CLASS
4950

51+
if options['color']:
52+
GENERATOR_STATS.enable_color()
53+
5054
generator = generator_class(
5155
urlconf=options['urlconf'],
5256
api_version=options['api_version'],

drf_spectacular/openapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1495,7 +1495,7 @@ def resolve_serializer(self, serializer, direction, bypass_extensions=False) ->
14951495
assert_basic_serializer(serializer)
14961496
serializer = force_instance(serializer)
14971497

1498-
with add_trace_message(serializer.__class__.__name__):
1498+
with add_trace_message(serializer.__class__):
14991499
component = ResolvedComponent(
15001500
name=self._get_serializer_name(serializer, direction, bypass_extensions),
15011501
type=ResolvedComponent.SCHEMA,

tests/test_command.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_command_plain(capsys):
1919
assert 'paths' in schema
2020

2121

22-
def test_command_parameterized(capsys):
22+
def test_command_parameterized():
2323
with tempfile.NamedTemporaryFile() as fh:
2424
management.call_command(
2525
'spectacular',
@@ -44,10 +44,25 @@ def test_command_fail(capsys):
4444
'--urlconf=tests.test_command',
4545
)
4646
stderr = capsys.readouterr().err
47-
assert 'Error #0: func: unable to guess serializer' in stderr
47+
assert 'Error [func]: unable to guess serializer' in stderr
4848
assert 'Schema generation summary:' in stderr
4949

5050

51+
def test_command_color(capsys):
52+
management.call_command(
53+
'spectacular',
54+
'--color',
55+
'--urlconf=tests.test_command',
56+
)
57+
stderr = capsys.readouterr().err
58+
assert '\033[0;31mError [func]:' in stderr
59+
60+
# undo global state change
61+
from drf_spectacular.drainage import GENERATOR_STATS
62+
GENERATOR_STATS._red = GENERATOR_STATS._blue = ''
63+
GENERATOR_STATS._yellow = GENERATOR_STATS._clear = ''
64+
65+
5166
def test_command_check(capsys):
5267
management.call_command('check', '--deploy')
5368
stderr = capsys.readouterr().err

tests/test_warnings.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
122122
pass # pragma: no cover
123123

124124
generate_schema('x', XViewset)
125-
assert 'XViewset: exception raised while getting serializer.' in capsys.readouterr().err
125+
assert (
126+
'Error [XViewset]: exception raised while getting serializer.'
127+
) in capsys.readouterr().err
126128

127129

128130
def test_extend_schema_unknown_class(capsys):
@@ -157,7 +159,7 @@ def get(self, request):
157159
pass # pragma: no cover
158160

159161
generate_schema('x', view=XView)
160-
assert 'XView: unable to guess serializer.' in capsys.readouterr().err
162+
assert 'Error [XView]: unable to guess serializer.' in capsys.readouterr().err
161163

162164

163165
def test_unable_to_follow_field_source_through_intermediate_property_warning(capsys):
@@ -180,7 +182,7 @@ def get(self, request):
180182

181183
generate_schema('x', view=XAPIView)
182184
assert (
183-
'XAPIView: XSerializer: could not follow field source through intermediate property'
185+
'[XAPIView > XSerializer]: could not follow field source through intermediate property'
184186
) in capsys.readouterr().err
185187

186188

@@ -208,8 +210,8 @@ def get(self, request):
208210

209211
generate_schema('x', view=XAPIView)
210212
stderr = capsys.readouterr().err
211-
assert 'XAPIView: XSerializer: unable to resolve type hint for function "x"' in stderr
212-
assert 'XAPIView: XSerializer: unable to resolve type hint for function "get_y"' in stderr
213+
assert '[XAPIView > XSerializer]: unable to resolve type hint for function "x"' in stderr
214+
assert '[XAPIView > XSerializer]: unable to resolve type hint for function "get_y"' in stderr
213215

214216

215217
def test_unable_to_traverse_union_type_hint(capsys):

0 commit comments

Comments
 (0)