diff --git a/rest_framework/schemas/__init__.py b/rest_framework/schemas/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py deleted file mode 100644 index f59e25c21..000000000 --- a/rest_framework/schemas/generators.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -generators.py # Top-down schema generation - -See schemas.__init__.py for package overview. -""" -import re -from importlib import import_module - -from django.conf import settings -from django.contrib.admindocs.views import simplify_regex -from django.core.exceptions import PermissionDenied -from django.http import Http404 -from django.urls import URLPattern, URLResolver - -from rest_framework import exceptions -from rest_framework.request import clone_request -from rest_framework.settings import api_settings -from rest_framework.utils.model_meta import _get_pk - - -def get_pk_name(model): - meta = model._meta.concrete_model._meta - return _get_pk(meta).name - - -def is_api_view(callback): - """ - Return `True` if the given view callback is a REST framework view/viewset. - """ - # Avoid import cycle on APIView - from rest_framework.views import APIView - cls = getattr(callback, 'cls', None) - return (cls is not None) and issubclass(cls, APIView) - - -def endpoint_ordering(endpoint): - path, method, callback = endpoint - method_priority = { - 'GET': 0, - 'POST': 1, - 'PUT': 2, - 'PATCH': 3, - 'DELETE': 4 - }.get(method, 5) - return (method_priority,) - - -_PATH_PARAMETER_COMPONENT_RE = re.compile( - r'<(?:(?P[^>:]+):)?(?P\w+)>' -) - - -class EndpointEnumerator: - """ - A class to determine the available API endpoints that a project exposes. - """ - def __init__(self, patterns=None, urlconf=None): - if patterns is None: - if urlconf is None: - # Use the default Django URL conf - urlconf = settings.ROOT_URLCONF - - # Load the given URLconf module - if isinstance(urlconf, str): - urls = import_module(urlconf) - else: - urls = urlconf - patterns = urls.urlpatterns - - self.patterns = patterns - - def get_api_endpoints(self, patterns=None, prefix=''): - """ - Return a list of all available API endpoints by inspecting the URL conf. - """ - if patterns is None: - patterns = self.patterns - - api_endpoints = [] - - for pattern in patterns: - path_regex = prefix + str(pattern.pattern) - if isinstance(pattern, URLPattern): - path = self.get_path_from_regex(path_regex) - callback = pattern.callback - if self.should_include_endpoint(path, callback): - for method in self.get_allowed_methods(callback): - endpoint = (path, method, callback) - api_endpoints.append(endpoint) - - elif isinstance(pattern, URLResolver): - nested_endpoints = self.get_api_endpoints( - patterns=pattern.url_patterns, - prefix=path_regex - ) - api_endpoints.extend(nested_endpoints) - - return sorted(api_endpoints, key=endpoint_ordering) - - def get_path_from_regex(self, path_regex): - """ - Given a URL conf regex, return a URI template string. - """ - # ???: Would it be feasible to adjust this such that we generate the - # path, plus the kwargs, plus the type from the converter, such that we - # could feed that straight into the parameter schema object? - - path = simplify_regex(path_regex) - - # Strip Django 2.0 converters as they are incompatible with uritemplate format - return re.sub(_PATH_PARAMETER_COMPONENT_RE, r'{\g}', path) - - def should_include_endpoint(self, path, callback): - """ - Return `True` if the given endpoint should be included. - """ - if not is_api_view(callback): - return False # Ignore anything except REST framework views. - - if callback.cls.schema is None: - return False - - if 'schema' in callback.initkwargs: - if callback.initkwargs['schema'] is None: - return False - - if path.endswith('.{format}') or path.endswith('.{format}/'): - return False # Ignore .json style URLs. - - return True - - def get_allowed_methods(self, callback): - """ - Return a list of the valid HTTP methods for this endpoint. - """ - if hasattr(callback, 'actions'): - actions = set(callback.actions) - http_method_names = set(callback.cls.http_method_names) - methods = [method.upper() for method in actions & http_method_names] - else: - methods = callback.cls().allowed_methods - - return [method for method in methods if method not in ('OPTIONS', 'HEAD')] - - -class BaseSchemaGenerator: - endpoint_inspector_cls = EndpointEnumerator - - # 'pk' isn't great as an externally exposed name for an identifier, - # so by default we prefer to use the actual model field name for schemas. - # Set by 'SCHEMA_COERCE_PATH_PK'. - coerce_path_pk = None - - def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None): - if url and not url.endswith('/'): - url += '/' - - self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK - - self.patterns = patterns - self.urlconf = urlconf - self.title = title - self.description = description - self.version = version - self.url = url - self.endpoints = None - - def _initialise_endpoints(self): - if self.endpoints is None: - inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf) - self.endpoints = inspector.get_api_endpoints() - - def _get_paths_and_endpoints(self, request): - """ - Generate (path, method, view) given (path, method, callback) for paths. - """ - paths = [] - view_endpoints = [] - for path, method, callback in self.endpoints: - view = self.create_view(callback, method, request) - path = self.coerce_path(path, method, view) - paths.append(path) - view_endpoints.append((path, method, view)) - - return paths, view_endpoints - - def create_view(self, callback, method, request=None): - """ - Given a callback, return an actual view instance. - """ - view = callback.cls(**getattr(callback, 'initkwargs', {})) - view.args = () - view.kwargs = {} - view.format_kwarg = None - view.request = None - view.action_map = getattr(callback, 'actions', None) - - actions = getattr(callback, 'actions', None) - if actions is not None: - if method == 'OPTIONS': - view.action = 'metadata' - else: - view.action = actions.get(method.lower()) - - if request is not None: - view.request = clone_request(request, method) - - return view - - def coerce_path(self, path, method, view): - """ - Coerce {pk} path arguments into the name of the model field, - where possible. This is cleaner for an external representation. - (Ie. "this is an identifier", not "this is a database primary key") - """ - if not self.coerce_path_pk or '{pk}' not in path: - return path - model = getattr(getattr(view, 'queryset', None), 'model', None) - if model: - field_name = get_pk_name(model) - else: - field_name = 'id' - return path.replace('{pk}', '{%s}' % field_name) - - def get_schema(self, request=None, public=False): - raise NotImplementedError(".get_schema() must be implemented in subclasses.") - - def has_view_permissions(self, path, method, view): - """ - Return `True` if the incoming request has the correct view permissions. - """ - if view.request is None: - return True - - try: - view.check_permissions(view.request) - except (exceptions.APIException, Http404, PermissionDenied): - return False - return True diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py deleted file mode 100644 index e027b46a7..000000000 --- a/rest_framework/schemas/inspectors.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -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 diff --git a/rest_framework/schemas/utils.py b/rest_framework/schemas/utils.py deleted file mode 100644 index 60ed69829..000000000 --- a/rest_framework/schemas/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -utils.py # Shared helper functions - -See schemas.__init__.py for package overview. -""" -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from rest_framework.mixins import RetrieveModelMixin - - -def is_list_view(path, method, view): - """ - Return True if the given path/method appears to represent a list view. - """ - if hasattr(view, 'action'): - # Viewsets have an explicitly defined action, which we can inspect. - return view.action == 'list' - - if method.lower() != 'get': - return False - if isinstance(view, RetrieveModelMixin): - return False - path_components = path.strip('/').split('/') - if path_components and '{' in path_components[-1]: - return False - return True - - -def get_pk_description(model, model_field): - if isinstance(model_field, models.AutoField): - value_type = _('unique integer value') - elif isinstance(model_field, models.UUIDField): - value_type = _('UUID string') - else: - value_type = _('unique value') - - return _('A {value_type} identifying this {name}.').format( - value_type=value_type, - name=model._meta.verbose_name, - )