inspectors.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """
  2. inspectors.py # Per-endpoint view introspection
  3. See schemas.__init__.py for package overview.
  4. """
  5. import re
  6. from weakref import WeakKeyDictionary
  7. from django.utils.encoding import smart_str
  8. from rest_framework.settings import api_settings
  9. from rest_framework.utils import formatting
  10. class ViewInspector:
  11. """
  12. Descriptor class on APIView.
  13. Provide subclass for per-view schema generation
  14. """
  15. # Used in _get_description_section()
  16. header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:')
  17. def __init__(self):
  18. self.instance_schemas = WeakKeyDictionary()
  19. def __get__(self, instance, owner):
  20. """
  21. Enables `ViewInspector` as a Python _Descriptor_.
  22. This is how `view.schema` knows about `view`.
  23. `__get__` is called when the descriptor is accessed on the owner.
  24. (That will be when view.schema is called in our case.)
  25. `owner` is always the owner class. (An APIView, or subclass for us.)
  26. `instance` is the view instance or `None` if accessed from the class,
  27. rather than an instance.
  28. See: https://docs.python.org/3/howto/descriptor.html for info on
  29. descriptor usage.
  30. """
  31. if instance in self.instance_schemas:
  32. return self.instance_schemas[instance]
  33. self.view = instance
  34. return self
  35. def __set__(self, instance, other):
  36. self.instance_schemas[instance] = other
  37. if other is not None:
  38. other.view = instance
  39. @property
  40. def view(self):
  41. """View property."""
  42. assert self._view is not None, (
  43. "Schema generation REQUIRES a view instance. (Hint: you accessed "
  44. "`schema` from the view class rather than an instance.)"
  45. )
  46. return self._view
  47. @view.setter
  48. def view(self, value):
  49. self._view = value
  50. @view.deleter
  51. def view(self):
  52. self._view = None
  53. def get_description(self, path, method):
  54. """
  55. Determine a path description.
  56. This will be based on the method docstring if one exists,
  57. or else the class docstring.
  58. """
  59. view = self.view
  60. method_name = getattr(view, 'action', method.lower())
  61. method_docstring = getattr(view, method_name, None).__doc__
  62. if method_docstring:
  63. # An explicit docstring on the method or action.
  64. return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring)))
  65. else:
  66. return self._get_description_section(view, getattr(view, 'action', method.lower()),
  67. view.get_view_description())
  68. def _get_description_section(self, view, header, description):
  69. lines = [line for line in description.splitlines()]
  70. current_section = ''
  71. sections = {'': ''}
  72. for line in lines:
  73. if self.header_regex.match(line):
  74. current_section, separator, lead = line.partition(':')
  75. sections[current_section] = lead.strip()
  76. else:
  77. sections[current_section] += '\n' + line
  78. # TODO: SCHEMA_COERCE_METHOD_NAMES appears here and in `SchemaGenerator.get_keys`
  79. coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES
  80. if header in sections:
  81. return sections[header].strip()
  82. if header in coerce_method_names:
  83. if coerce_method_names[header] in sections:
  84. return sections[coerce_method_names[header]].strip()
  85. return sections[''].strip()
  86. class DefaultSchema(ViewInspector):
  87. """Allows overriding AutoSchema using DEFAULT_SCHEMA_CLASS setting"""
  88. def __get__(self, instance, owner):
  89. result = super().__get__(instance, owner)
  90. if not isinstance(result, DefaultSchema):
  91. return result
  92. inspector_class = api_settings.DEFAULT_SCHEMA_CLASS
  93. assert issubclass(inspector_class, ViewInspector), (
  94. "DEFAULT_SCHEMA_CLASS must be set to a ViewInspector (usually an AutoSchema) subclass"
  95. )
  96. inspector = inspector_class()
  97. inspector.view = instance
  98. return inspector