-
Notifications
You must be signed in to change notification settings - Fork 260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed issue #314 - include information about view/serializer in warnings #322
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
|
||
from drf_spectacular.authentication import OpenApiAuthenticationExtension | ||
from drf_spectacular.contrib import * # noqa: F403, F401 | ||
from drf_spectacular.drainage import get_override, has_override | ||
from drf_spectacular.drainage import add_trace_message, get_override, has_override | ||
from drf_spectacular.extensions import ( | ||
OpenApiFilterExtension, OpenApiSerializerExtension, OpenApiSerializerFieldExtension, | ||
) | ||
|
@@ -161,7 +161,7 @@ def _process_override_parameters(self): | |
required=property_name in mapped.get('required', []), | ||
) | ||
else: | ||
warn(f'could not resolve parameter annotation {parameter}. skipping.') | ||
warn(f'could not resolve parameter annotation {parameter}. Skipping.') | ||
return result | ||
|
||
def _get_format_parameters(self): | ||
|
@@ -324,10 +324,10 @@ def _resolve_path_parameters(self, variables): | |
schema = resolved_parameter['schema'] | ||
elif get_view_model(self.view) is None: | ||
warn( | ||
f'could not derive type of path parameter "{variable}" because because it ' | ||
f'is untyped and obtaining queryset from {self.view.__class__} failed. ' | ||
f'consider adding a type to the path (e.g. <int:{variable}>) or annotating ' | ||
f'the parameter type with @extend_schema. defaulting to "string".' | ||
f'{self.view.__class__.__name__}: could not derive type of path parameter ' | ||
f'"{variable}" because it is untyped and obtaining queryset from the viewset ' | ||
f'failed. Consider adding a type to the path (e.g. <int:{variable}>) or ' | ||
f'annotating the parameter type with @extend_schema. Defaulting to "string".' | ||
) | ||
else: | ||
try: | ||
|
@@ -338,9 +338,10 @@ def _resolve_path_parameters(self, variables): | |
description = get_pk_description(model, model_field) | ||
except django_exceptions.FieldDoesNotExist: | ||
warn( | ||
f'could not derive type of path parameter "{variable}" because ' | ||
f'model "{model}" did contain no such field. consider annotating ' | ||
f'parameter with @extend_schema. defaulting to "string".' | ||
f'{self.view.__class__.__name__}: could not derive type of path ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefix can be dropped again (see other comment) |
||
f'parameter "{variable}" for view because ' | ||
f'model "{model}" did contain no such field. Consider annotating ' | ||
f'parameter with @extend_schema. Defaulting to "string".' | ||
) | ||
|
||
parameters.append(build_parameter_type( | ||
|
@@ -417,15 +418,15 @@ def _map_model_field(self, model_field, direction): | |
if not field_cls: | ||
warn( | ||
f'model field "{model_field.get_internal_type()}" has no mapping in ' | ||
f'ModelSerializer. it may be a deprecated field. defaulting to "string"' | ||
f'ModelSerializer. It may be a deprecated field. Defaulting to "string"' | ||
) | ||
return build_basic_type(OpenApiTypes.STR) | ||
return self._map_serializer_field(field_cls(), direction) | ||
else: | ||
error( | ||
f'could not resolve model field "{model_field}". failed to resolve through ' | ||
f'could not resolve model field "{model_field}". Failed to resolve through ' | ||
f'serializer_field_mapping, get_internal_type(), or any override mechanism. ' | ||
f'defaulting to "string"' | ||
f'Defaulting to "string"' | ||
) | ||
return build_basic_type(OpenApiTypes.STR) | ||
|
||
|
@@ -642,7 +643,7 @@ def _map_serializer_field(self, field, direction): | |
schema = self._map_model_field(field.model_field, direction) | ||
return append_meta(schema, meta) | ||
|
||
warn(f'could not resolve serializer field "{field}". defaulting to "string"') | ||
warn(f'could not resolve serializer field "{field}". Defaulting to "string"') | ||
return append_meta(build_basic_type(OpenApiTypes.STR), meta) | ||
|
||
def _map_min_max(self, field, content): | ||
|
@@ -794,8 +795,8 @@ def _map_response_type_hint(self, method): | |
return resolve_type_hint(hint) | ||
except UnableToProceedError: | ||
warn( | ||
f'unable to resolve type hint for function "{method.__name__}". consider ' | ||
f'using a type hint or @extend_schema_field. defaulting to string.' | ||
f'unable to resolve type hint for function "{method.__name__}". Consider ' | ||
f'using a type hint or @extend_schema_field. Defaulting to string.' | ||
) | ||
return build_basic_type(OpenApiTypes.STR) | ||
|
||
|
@@ -835,15 +836,15 @@ def _get_serializer(self): | |
return view.serializer_class | ||
else: | ||
error( | ||
f'Unable to guess serializer for {view.__class__.__name__}. This is graceful ' | ||
f'{view.__class__.__name__}: unable to guess serializer. This is graceful ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefix can be dropped again (see other comment) |
||
f'fallback handling for APIViews. Consider using GenericAPIView as view base ' | ||
f'class, if view is under your control. ignoring view for now. ' | ||
f'class, if view is under your control. Ignoring view for now. ' | ||
) | ||
else: | ||
error('Encountered unknown view base class. please report this issue. ignoring for now') | ||
error('Encountered unknown view base class. Please report this issue. Ignoring for now') | ||
except Exception as exc: | ||
error( | ||
f'Exception raised while getting serializer from {view.__class__.__name__}. Hint: ' | ||
f'exception raised while getting serializer. Hint: ' | ||
f'Is get_serializer_class() returning None or is get_queryset() not working without ' | ||
f'a request? Ignoring the view for now. (Exception: {exc})' | ||
) | ||
|
@@ -939,8 +940,8 @@ def _get_request_for_media_type(self, serializer): | |
request_body_required = False | ||
else: | ||
warn( | ||
f'could not resolve request body for {self.method} {self.path}. defaulting to generic ' | ||
'free-form object. (maybe annotate a Serializer class?)' | ||
f'could not resolve request body for {self.method} {self.path}. Defaulting to generic ' | ||
'free-form object. (Maybe annotate a Serializer class?)' | ||
) | ||
schema = build_generic_type() | ||
schema['description'] = 'Unspecified request body' | ||
|
@@ -974,7 +975,7 @@ def _get_response_bodies(self): | |
warn( | ||
f'could not resolve "{response_serializers}" for {self.method} {self.path}. ' | ||
f'Expected either a serializer or some supported override mechanism. ' | ||
f'defaulting to generic free-form object.' | ||
f'Defaulting to generic free-form object.' | ||
) | ||
schema = build_basic_type(OpenApiTypes.OBJECT) | ||
schema['description'] = _('Unspecified response body') | ||
|
@@ -1005,7 +1006,7 @@ def _get_response_for_code(self, serializer, status_code, media_types=None): | |
else: | ||
warn( | ||
f'could not resolve "{serializer}" for {self.method} {self.path}. Expected either ' | ||
f'a serializer or some supported override mechanism. defaulting to ' | ||
f'a serializer or some supported override mechanism. Defaulting to ' | ||
f'generic free-form object.' | ||
) | ||
schema = build_basic_type(OpenApiTypes.OBJECT) | ||
|
@@ -1123,27 +1124,27 @@ def resolve_serializer(self, serializer, direction) -> ResolvedComponent: | |
f'https://github.com/tfranzel/drf-spectacular/issues ' | ||
) | ||
serializer = force_instance(serializer) | ||
|
||
component = ResolvedComponent( | ||
name=self._get_serializer_name(serializer, direction), | ||
type=ResolvedComponent.SCHEMA, | ||
object=serializer, | ||
) | ||
if component in self.registry: | ||
return self.registry[component] # return component with schema | ||
|
||
self.registry.register(component) | ||
component.schema = self._map_serializer(serializer, direction) | ||
# 4 cases: | ||
# 1. polymorphic container component -> use | ||
# 2. concrete component with properties -> use | ||
# 3. concrete component without properties -> prob. transactional so discard | ||
# 4. explicit list component -> demultiplexed at usage location so discard | ||
keep_component = ( | ||
any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf']) | ||
or component.schema.get('properties', {}) | ||
) | ||
if not keep_component: | ||
del self.registry[component] | ||
return ResolvedComponent(None, None) # sentinel | ||
return component | ||
with add_trace_message(serializer.__class__.__name__): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. my OCD complains here due to increasing indent and obfuscating the git blame, but what can you do if that is the proper thing to do. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, me too, but I couldn't see a way around it. This consideration was in fact one of the reasons why I didn't want to put |
||
component = ResolvedComponent( | ||
name=self._get_serializer_name(serializer, direction), | ||
type=ResolvedComponent.SCHEMA, | ||
object=serializer, | ||
) | ||
if component in self.registry: | ||
return self.registry[component] # return component with schema | ||
|
||
self.registry.register(component) | ||
component.schema = self._map_serializer(serializer, direction) | ||
# 4 cases: | ||
# 1. polymorphic container component -> use | ||
# 2. concrete component with properties -> use | ||
# 3. concrete component without properties -> prob. transactional so discard | ||
# 4. explicit list component -> demultiplexed at usage location so discard | ||
keep_component = ( | ||
any(nest_tag in component.schema for nest_tag in ['oneOf', 'allOf', 'anyOf']) | ||
or component.schema.get('properties', {}) | ||
) | ||
if not keep_component: | ||
del self.registry[component] | ||
return ResolvedComponent(None, None) # sentinel | ||
return component |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -119,9 +119,9 @@ def get_view_model(view): | |
return view.get_queryset().model | ||
except Exception as exc: | ||
warn( | ||
f'failed to obtain model through view\'s queryset due to raised exception. ' | ||
f'prevent this either by setting "queryset = Model.objects.none()" on the view, ' | ||
f'having an empty fallback in get_queryset() or by using @extend_schema. ' | ||
f'{view.__class__.__name__}: failed to obtain model through view\'s queryset due to ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. prefix can be dropped again (see other comment) |
||
f'raised exception. Prevent this either by setting "queryset = Model.objects.none()" ' | ||
f'on the view, having an empty fallback in get_queryset() or by using @extend_schema. ' | ||
f'(Exception: {exc})' | ||
) | ||
|
||
|
@@ -389,8 +389,8 @@ def follow_field_source(model, path): | |
except Exception as exc: | ||
warn( | ||
f'could not resolve field on model {model} with path "{".".join(path)}". ' | ||
f'this is likely a custom field that does some unknown magic. maybe ' | ||
f'consider annotating the field/property? defaulting to "string". (Exception: {exc})' | ||
f'This is likely a custom field that does some unknown magic. Maybe ' | ||
f'consider annotating the field/property? Defaulting to "string". (Exception: {exc})' | ||
) | ||
|
||
def dummy_property(obj) -> str: | ||
|
@@ -581,8 +581,8 @@ def load_enum_name_overrides(): | |
|
||
if len(spectacular_settings.ENUM_NAME_OVERRIDES) != len(overrides): | ||
error( | ||
'ENUM_NAME_OVERRIDES has duplication issues. encountered multiple names ' | ||
'for the same choice set. enum naming might be unexpected.' | ||
'ENUM_NAME_OVERRIDES has duplication issues. Encountered multiple names ' | ||
'for the same choice set. Enum naming might be unexpected.' | ||
) | ||
return overrides | ||
|
||
|
@@ -660,7 +660,7 @@ def modify_for_versioning(patterns, method, path, view, requested_version): | |
urlconf=tuple(detype_pattern(p) for p in patterns) | ||
).resolve(path) | ||
except Resolver404: | ||
error(f"namespace versioning path resolution failed for {path}. path will be ignored.") | ||
error(f"namespace versioning path resolution failed for {path}. Path will be ignored.") | ||
elif issubclass(view.versioning_class, versioning.AcceptHeaderVersioning): | ||
# Append the version into request accepted_media_type. | ||
# e.g "application/json; version=1.0" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ def assert_schema(schema, reference_filename, transforms=None): | |
|
||
if not os.path.exists(reference_filename): | ||
raise RuntimeError( | ||
f'{reference_filename} was not found for comparison. carefully inspect ' | ||
f'{reference_filename} was not found for comparison. Carefully inspect ' | ||
f'the generated {reference_filename.replace(".yml", "_out.yml")} and ' | ||
f'copy it to {reference_filename} to serve as new ground truth.' | ||
) | ||
|
@@ -51,23 +51,29 @@ def generate_schema(route, viewset=None, view=None, view_function=None): | |
from rest_framework import routers | ||
from rest_framework.viewsets import ViewSetMixin | ||
|
||
from drf_spectacular.drainage import add_trace_message | ||
from drf_spectacular.generators import SchemaGenerator | ||
|
||
patterns = [] | ||
trace_message = '' | ||
if viewset: | ||
assert issubclass(viewset, ViewSetMixin) | ||
router = routers.SimpleRouter() | ||
router.register(route, viewset, basename=route) | ||
patterns = router.urls | ||
trace_message = viewset.__name__ | ||
elif view: | ||
patterns = [path(route, view.as_view())] | ||
trace_message = view.__name__ | ||
elif view_function: | ||
patterns = [path(route, view_function)] | ||
trace_message = view_function.__name__ | ||
|
||
generator = SchemaGenerator(patterns=patterns) | ||
schema = generator.get_schema(request=None, public=True) | ||
validate_schema(schema) # make sure generated schemas are always valid | ||
return schema | ||
with add_trace_message(trace_message): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. usage of the context manager and manual traces can be dropped in favor of the other solution. this also keeps the tests closer to reality |
||
generator = SchemaGenerator(patterns=patterns) | ||
schema = generator.get_schema(request=None, public=True) | ||
validate_schema(schema) # make sure generated schemas are always valid | ||
return schema | ||
|
||
|
||
def get_response_schema(operation, status=None, content_type='application/json'): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefix can be dropped again (see other comment)