mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-24 10:34:03 +03:00
b25028ac8f
* Add support for Python 3.13 * Fix extracting tox env with -dev Python versions * Fix view description inspection in Python 3.13 Python 3.13 introduced docstrings for None: https://github.com/python/cpython/pull/117813 In Python 3.12, this is an empty string: ``` ➜ python3.12 Python 3.12.6 (main, Sep 10 2024, 19:06:17) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> d = None >>> d.__doc__ >>> ``` In Python 3.13, it's no longer empty: ``` ➜ python3.13 Python 3.13.0rc2+ (heads/3.13:660baa1, Sep 10 2024, 18:57:50) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> d = None >>> d.__doc__ 'The type of the None singleton.' >>> ``` Adding a check in the inspector that get the view description out the view function docstring to catch this edge case.
127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
"""
|
|
inspectors.py # Per-endpoint view introspection
|
|
|
|
See schemas.__init__.py for package overview.
|
|
"""
|
|
import re
|
|
from weakref import WeakKeyDictionary
|
|
|
|
from django.utils.encoding import smart_str
|
|
|
|
from rest_framework.settings import api_settings
|
|
from rest_framework.utils import formatting
|
|
|
|
|
|
class ViewInspector:
|
|
"""
|
|
Descriptor class on APIView.
|
|
|
|
Provide subclass for per-view schema generation
|
|
"""
|
|
|
|
# Used in _get_description_section()
|
|
header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
|
|
|
|
def __init__(self):
|
|
self.instance_schemas = WeakKeyDictionary()
|
|
|
|
def __get__(self, instance, owner):
|
|
"""
|
|
Enables `ViewInspector` as a Python _Descriptor_.
|
|
|
|
This is how `view.schema` knows about `view`.
|
|
|
|
`__get__` is called when the descriptor is accessed on the owner.
|
|
(That will be when view.schema is called in our case.)
|
|
|
|
`owner` is always the owner class. (An APIView, or subclass for us.)
|
|
`instance` is the view instance or `None` if accessed from the class,
|
|
rather than an instance.
|
|
|
|
See: https://docs.python.org/3/howto/descriptor.html for info on
|
|
descriptor usage.
|
|
"""
|
|
if instance in self.instance_schemas:
|
|
return self.instance_schemas[instance]
|
|
|
|
self.view = instance
|
|
return self
|
|
|
|
def __set__(self, instance, other):
|
|
self.instance_schemas[instance] = other
|
|
if other is not None:
|
|
other.view = instance
|
|
|
|
@property
|
|
def view(self):
|
|
"""View property."""
|
|
assert self._view is not None, (
|
|
"Schema generation REQUIRES a view instance. (Hint: you accessed "
|
|
"`schema` from the view class rather than an instance.)"
|
|
)
|
|
return self._view
|
|
|
|
@view.setter
|
|
def view(self, value):
|
|
self._view = value
|
|
|
|
@view.deleter
|
|
def view(self):
|
|
self._view = None
|
|
|
|
def get_description(self, path, method):
|
|
"""
|
|
Determine a path description.
|
|
|
|
This will be based on the method docstring if one exists,
|
|
or else the class docstring.
|
|
"""
|
|
view = self.view
|
|
|
|
method_name = getattr(view, 'action', method.lower())
|
|
method_func = getattr(view, method_name, None)
|
|
method_docstring = method_func.__doc__
|
|
if method_func and method_docstring:
|
|
# An explicit docstring on the method or action.
|
|
return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
|
|
else:
|
|
return self._get_description_section(view, getattr(view, 'action', method.lower()),
|
|
view.get_view_description())
|
|
|
|
def _get_description_section(self, view, header, description):
|
|
lines = description.splitlines()
|
|
current_section = ''
|
|
sections = {'': ''}
|
|
|
|
for line in lines:
|
|
if self.header_regex.match(line):
|
|
current_section, separator, lead = line.partition(':')
|
|
sections[current_section] = lead.strip()
|
|
else:
|
|
sections[current_section] += '\n' + line
|
|
|
|
# TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
|
|
coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
|
|
if header in sections:
|
|
return sections[header].strip()
|
|
if header in coerce_method_names:
|
|
if coerce_method_names[header] in sections:
|
|
return sections[coerce_method_names[header]].strip()
|
|
return sections[''].strip()
|
|
|
|
|
|
class DefaultSchema(ViewInspector):
|
|
"""Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
|
|
def __get__(self, instance, owner):
|
|
result = super().__get__(instance, owner)
|
|
if not isinstance(result, DefaultSchema):
|
|
return result
|
|
|
|
inspector_class = api_settings.DEFAULT_SCHEMA_CLASS
|
|
assert issubclass(inspector_class, ViewInspector), (
|
|
"DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass"
|
|
)
|
|
inspector = inspector_class()
|
|
inspector.view = instance
|
|
return inspector
|