Skip to content

Commit

Permalink
Merge pull request #889 from tfranzel/improved_warnings
Browse files Browse the repository at this point in the history
improve trace messages / warnings & add color #866
  • Loading branch information
tfranzel authored Dec 9, 2022
2 parents 0f01020 + aec3708 commit 51e2905
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Generate your schema with the CLI:

.. code:: bash
$ ./manage.py spectacular --file schema.yml
$ ./manage.py spectacular --color --file schema.yml
$ docker run -p 80:8080 -e SWAGGER_JSON=/schema.yml -v ${PWD}/schema.yml:/schema.yml swaggerapi/swagger-ui
If you also want to validate your schema add the ``--validate`` flag. Or serve your schema directly
Expand Down
2 changes: 1 addition & 1 deletion drf_spectacular/contrib/django_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
return []

result = []
with add_trace_message(filterset_class.__name__):
with add_trace_message(filterset_class):
for field_name, filter_field in filterset_class.base_filters.items():
result += self.resolve_filter_field(
auto_schema, model, filterset_class, field_name, filter_field
Expand Down
47 changes: 41 additions & 6 deletions drf_spectacular/drainage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import contextlib
import functools
import inspect
import sys
from collections import defaultdict
from typing import DefaultDict
Expand All @@ -8,9 +9,13 @@
class GeneratorStats:
_warn_cache: DefaultDict[str, int] = defaultdict(int)
_error_cache: DefaultDict[str, int] = defaultdict(int)
_blue = ''
_red = ''
_yellow = ''
_clear = ''

def __getattr__(self, name):
if not self.__dict__:
if 'silent' not in self.__dict__:
from drf_spectacular.settings import spectacular_settings
self.silent = spectacular_settings.DISABLE_ERRORS_AND_WARNINGS
try:
Expand All @@ -33,12 +38,25 @@ def reset(self):
self._warn_cache.clear()
self._error_cache.clear()

def enable_color(self):
self._blue = '\033[0;34m'
self._red = '\033[0;31m'
self._yellow = '\033[0;33m'
self._clear = '\033[0m'

def emit(self, msg, severity):
assert severity in ['warning', 'error']
msg = _get_current_trace() + str(msg)
cache = self._warn_cache if severity == 'warning' else self._error_cache

source_location, breadcrumbs = _get_current_trace()
prefix = f'{self._blue}{source_location}: ' if source_location else ''
prefix += self._yellow if severity == 'warning' else self._red
prefix += f'{severity.capitalize()}'
prefix += f' [{breadcrumbs}]: ' if breadcrumbs else ': '

msg = prefix + self._clear + str(msg)
if not self.silent and msg not in cache:
print(f'{severity.capitalize()} #{len(cache)}: {msg}', file=sys.stderr)
print(msg, file=sys.stderr)
cache[msg] += 1

def emit_summary(self):
Expand Down Expand Up @@ -80,17 +98,34 @@ def reset_generator_stats():


@contextlib.contextmanager
def add_trace_message(trace_message):
def add_trace_message(obj):
"""
Adds a message to be used as a prefix when emitting warnings and errors.
"""
_TRACES.append(trace_message)
sourcefile, lineno = _get_source_location(obj)
_TRACES.append((sourcefile, lineno, obj.__name__))
yield
_TRACES.pop()


@functools.lru_cache(maxsize=1000)
def _get_source_location(obj):
try:
sourcefile = inspect.getsourcefile(obj)
lineno = inspect.getsourcelines(obj)[1]
return sourcefile, lineno
except: # noqa: E722
return None, None


def _get_current_trace():
return ''.join(f"{trace}: " for trace in _TRACES if trace)
source_locations = [t for t in _TRACES if t[0]]
if source_locations:
source_location = f"{source_locations[-1][0]}:{source_locations[-1][1]}"
else:
source_location = ''
breadcrumbs = ' > '.join(t[2] for t in _TRACES)
return source_location, breadcrumbs


def has_override(obj, prop):
Expand Down
2 changes: 1 addition & 1 deletion drf_spectacular/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def parse(self, input_request, public):
f'DEFAULT_SCHEMA_CLASS pointing to "drf_spectacular.openapi.AutoSchema" '
f'or any other drf-spectacular compatible AutoSchema?'
)
with add_trace_message(getattr(view, '__class__', view).__name__):
with add_trace_message(getattr(view, '__class__', view)):
operation = view.schema.get_operation(
path, path_regex, path_prefix, method, self.registry
)
Expand Down
4 changes: 4 additions & 0 deletions drf_spectacular/management/commands/spectacular.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@ def add_arguments(self, parser):
parser.add_argument('--validate', dest="validate", default=False, action='store_true')
parser.add_argument('--api-version', dest="api_version", default=None, type=str)
parser.add_argument('--lang', dest="lang", default=None, type=str)
parser.add_argument('--color', dest="color", default=False, action='store_true')

def handle(self, *args, **options):
if options['generator_class']:
generator_class = import_string(options['generator_class'])
else:
generator_class = spectacular_settings.DEFAULT_GENERATOR_CLASS

if options['color']:
GENERATOR_STATS.enable_color()

generator = generator_class(
urlconf=options['urlconf'],
api_version=options['api_version'],
Expand Down
2 changes: 1 addition & 1 deletion drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1497,7 +1497,7 @@ def resolve_serializer(self, serializer, direction, bypass_extensions=False) ->
assert_basic_serializer(serializer)
serializer = force_instance(serializer)

with add_trace_message(serializer.__class__.__name__):
with add_trace_message(serializer.__class__):
component = ResolvedComponent(
name=self._get_serializer_name(serializer, direction, bypass_extensions),
type=ResolvedComponent.SCHEMA,
Expand Down
19 changes: 17 additions & 2 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_command_plain(capsys):
assert 'paths' in schema


def test_command_parameterized(capsys):
def test_command_parameterized():
with tempfile.NamedTemporaryFile() as fh:
management.call_command(
'spectacular',
Expand All @@ -44,10 +44,25 @@ def test_command_fail(capsys):
'--urlconf=tests.test_command',
)
stderr = capsys.readouterr().err
assert 'Error #0: func: unable to guess serializer' in stderr
assert 'Error [func]: unable to guess serializer' in stderr
assert 'Schema generation summary:' in stderr


def test_command_color(capsys):
management.call_command(
'spectacular',
'--color',
'--urlconf=tests.test_command',
)
stderr = capsys.readouterr().err
assert '\033[0;31mError [func]:' in stderr

# undo global state change
from drf_spectacular.drainage import GENERATOR_STATS
GENERATOR_STATS._red = GENERATOR_STATS._blue = ''
GENERATOR_STATS._yellow = GENERATOR_STATS._clear = ''


def test_command_check(capsys):
management.call_command('check', '--deploy')
stderr = capsys.readouterr().err
Expand Down
12 changes: 7 additions & 5 deletions tests/test_warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ class XViewset(mixins.ListModelMixin, viewsets.GenericViewSet):
pass # pragma: no cover

generate_schema('x', XViewset)
assert 'XViewset: exception raised while getting serializer.' in capsys.readouterr().err
assert (
'Error [XViewset]: exception raised while getting serializer.'
) in capsys.readouterr().err


def test_extend_schema_unknown_class(capsys):
Expand Down Expand Up @@ -157,7 +159,7 @@ def get(self, request):
pass # pragma: no cover

generate_schema('x', view=XView)
assert 'XView: unable to guess serializer.' in capsys.readouterr().err
assert 'Error [XView]: unable to guess serializer.' in capsys.readouterr().err


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

generate_schema('x', view=XAPIView)
assert (
'XAPIView: XSerializer: could not follow field source through intermediate property'
'[XAPIView > XSerializer]: could not follow field source through intermediate property'
) in capsys.readouterr().err


Expand Down Expand Up @@ -208,8 +210,8 @@ def get(self, request):

generate_schema('x', view=XAPIView)
stderr = capsys.readouterr().err
assert 'XAPIView: XSerializer: unable to resolve type hint for function "x"' in stderr
assert 'XAPIView: XSerializer: unable to resolve type hint for function "get_y"' in stderr
assert '[XAPIView > XSerializer]: unable to resolve type hint for function "x"' in stderr
assert '[XAPIView > XSerializer]: unable to resolve type hint for function "get_y"' in stderr


def test_unable_to_traverse_union_type_hint(capsys):
Expand Down

0 comments on commit 51e2905

Please sign in to comment.