openapi.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. import re
  2. import warnings
  3. from collections import OrderedDict
  4. from decimal import Decimal
  5. from operator import attrgetter
  6. from urllib.parse import urljoin
  7. from django.core.validators import (
  8. DecimalValidator, EmailValidator, MaxLengthValidator, MaxValueValidator,
  9. MinLengthValidator, MinValueValidator, RegexValidator, URLValidator
  10. )
  11. from django.db import models
  12. from django.utils.encoding import force_str
  13. from rest_framework import (
  14. RemovedInDRF314Warning, exceptions, renderers, serializers
  15. )
  16. from rest_framework.compat import uritemplate
  17. from rest_framework.fields import _UnvalidatedField, empty
  18. from rest_framework.settings import api_settings
  19. from .generators import BaseSchemaGenerator
  20. from .inspectors import ViewInspector
  21. from .utils import get_pk_description, is_list_view
  22. class SchemaGenerator(BaseSchemaGenerator):
  23. def get_info(self):
  24. # Title and version are required by openapi specification 3.x
  25. info = {
  26. 'title': self.title or '',
  27. 'version': self.version or ''
  28. }
  29. if self.description is not None:
  30. info['description'] = self.description
  31. return info
  32. def check_duplicate_operation_id(self, paths):
  33. ids = {}
  34. for route in paths:
  35. for method in paths[route]:
  36. if 'operationId' not in paths[route][method]:
  37. continue
  38. operation_id = paths[route][method]['operationId']
  39. if operation_id in ids:
  40. warnings.warn(
  41. 'You have a duplicated operationId in your OpenAPI schema: {operation_id}\n'
  42. '\tRoute: {route1}, Method: {method1}\n'
  43. '\tRoute: {route2}, Method: {method2}\n'
  44. '\tAn operationId has to be unique across your schema. Your schema may not work in other tools.'
  45. .format(
  46. route1=ids[operation_id]['route'],
  47. method1=ids[operation_id]['method'],
  48. route2=route,
  49. method2=method,
  50. operation_id=operation_id
  51. )
  52. )
  53. ids[operation_id] = {
  54. 'route': route,
  55. 'method': method
  56. }
  57. def get_schema(self, request=None, public=False):
  58. """
  59. Generate a OpenAPI schema.
  60. """
  61. self._initialise_endpoints()
  62. components_schemas = {}
  63. # Iterate endpoints generating per method path operations.
  64. paths = {}
  65. _, view_endpoints = self._get_paths_and_endpoints(None if public else request)
  66. for path, method, view in view_endpoints:
  67. if not self.has_view_permissions(path, method, view):
  68. continue
  69. operation = view.schema.get_operation(path, method)
  70. components = view.schema.get_components(path, method)
  71. for k in components.keys():
  72. if k not in components_schemas:
  73. continue
  74. if components_schemas[k] == components[k]:
  75. continue
  76. warnings.warn('Schema component "{}" has been overriden with a different value.'.format(k))
  77. components_schemas.update(components)
  78. # Normalise path for any provided mount url.
  79. if path.startswith('/'):
  80. path = path[1:]
  81. path = urljoin(self.url or '/', path)
  82. paths.setdefault(path, {})
  83. paths[path][method.lower()] = operation
  84. self.check_duplicate_operation_id(paths)
  85. # Compile final schema.
  86. schema = {
  87. 'openapi': '3.0.2',
  88. 'info': self.get_info(),
  89. 'paths': paths,
  90. }
  91. if len(components_schemas) > 0:
  92. schema['components'] = {
  93. 'schemas': components_schemas
  94. }
  95. return schema
  96. # View Inspectors
  97. class AutoSchema(ViewInspector):
  98. def __init__(self, tags=None, operation_id_base=None, component_name=None):
  99. """
  100. :param operation_id_base: user-defined name in operationId. If empty, it will be deducted from the Model/Serializer/View name.
  101. :param component_name: user-defined component's name. If empty, it will be deducted from the Serializer's class name.
  102. """
  103. if tags and not all(isinstance(tag, str) for tag in tags):
  104. raise ValueError('tags must be a list or tuple of string.')
  105. self._tags = tags
  106. self.operation_id_base = operation_id_base
  107. self.component_name = component_name
  108. super().__init__()
  109. request_media_types = []
  110. response_media_types = []
  111. method_mapping = {
  112. 'get': 'retrieve',
  113. 'post': 'create',
  114. 'put': 'update',
  115. 'patch': 'partialUpdate',
  116. 'delete': 'destroy',
  117. }
  118. def get_operation(self, path, method):
  119. operation = {}
  120. operation['operationId'] = self.get_operation_id(path, method)
  121. operation['description'] = self.get_description(path, method)
  122. parameters = []
  123. parameters += self.get_path_parameters(path, method)
  124. parameters += self.get_pagination_parameters(path, method)
  125. parameters += self.get_filter_parameters(path, method)
  126. operation['parameters'] = parameters
  127. request_body = self.get_request_body(path, method)
  128. if request_body:
  129. operation['requestBody'] = request_body
  130. operation['responses'] = self.get_responses(path, method)
  131. operation['tags'] = self.get_tags(path, method)
  132. return operation
  133. def get_component_name(self, serializer):
  134. """
  135. Compute the component's name from the serializer.
  136. Raise an exception if the serializer's class name is "Serializer" (case-insensitive).
  137. """
  138. if self.component_name is not None:
  139. return self.component_name
  140. # use the serializer's class name as the component name.
  141. component_name = serializer.__class__.__name__
  142. # We remove the "serializer" string from the class name.
  143. pattern = re.compile("serializer", re.IGNORECASE)
  144. component_name = pattern.sub("", component_name)
  145. if component_name == "":
  146. raise Exception(
  147. '"{}" is an invalid class name for schema generation. '
  148. 'Serializer\'s class name should be unique and explicit. e.g. "ItemSerializer"'
  149. .format(serializer.__class__.__name__)
  150. )
  151. return component_name
  152. def get_components(self, path, method):
  153. """
  154. Return components with their properties from the serializer.
  155. """
  156. if method.lower() == 'delete':
  157. return {}
  158. request_serializer = self.get_request_serializer(path, method)
  159. response_serializer = self.get_response_serializer(path, method)
  160. components = {}
  161. if isinstance(request_serializer, serializers.Serializer):
  162. component_name = self.get_component_name(request_serializer)
  163. content = self.map_serializer(request_serializer)
  164. components.setdefault(component_name, content)
  165. if isinstance(response_serializer, serializers.Serializer):
  166. component_name = self.get_component_name(response_serializer)
  167. content = self.map_serializer(response_serializer)
  168. components.setdefault(component_name, content)
  169. return components
  170. def _to_camel_case(self, snake_str):
  171. components = snake_str.split('_')
  172. # We capitalize the first letter of each component except the first one
  173. # with the 'title' method and join them together.
  174. return components[0] + ''.join(x.title() for x in components[1:])
  175. def get_operation_id_base(self, path, method, action):
  176. """
  177. Compute the base part for operation ID from the model, serializer or view name.
  178. """
  179. model = getattr(getattr(self.view, 'queryset', None), 'model', None)
  180. if self.operation_id_base is not None:
  181. name = self.operation_id_base
  182. # Try to deduce the ID from the view's model
  183. elif model is not None:
  184. name = model.__name__
  185. # Try with the serializer class name
  186. elif self.get_serializer(path, method) is not None:
  187. name = self.get_serializer(path, method).__class__.__name__
  188. if name.endswith('Serializer'):
  189. name = name[:-10]
  190. # Fallback to the view name
  191. else:
  192. name = self.view.__class__.__name__
  193. if name.endswith('APIView'):
  194. name = name[:-7]
  195. elif name.endswith('View'):
  196. name = name[:-4]
  197. # Due to camel-casing of classes and `action` being lowercase, apply title in order to find if action truly
  198. # comes at the end of the name
  199. if name.endswith(action.title()): # ListView, UpdateAPIView, ThingDelete ...
  200. name = name[:-len(action)]
  201. if action == 'list' and not name.endswith('s'): # listThings instead of listThing
  202. name += 's'
  203. return name
  204. def get_operation_id(self, path, method):
  205. """
  206. Compute an operation ID from the view type and get_operation_id_base method.
  207. """
  208. method_name = getattr(self.view, 'action', method.lower())
  209. if is_list_view(path, method, self.view):
  210. action = 'list'
  211. elif method_name not in self.method_mapping:
  212. action = self._to_camel_case(method_name)
  213. else:
  214. action = self.method_mapping[method.lower()]
  215. name = self.get_operation_id_base(path, method, action)
  216. return action + name
  217. def get_path_parameters(self, path, method):
  218. """
  219. Return a list of parameters from templated path variables.
  220. """
  221. assert uritemplate, '`uritemplate` must be installed for OpenAPI schema support.'
  222. model = getattr(getattr(self.view, 'queryset', None), 'model', None)
  223. parameters = []
  224. for variable in uritemplate.variables(path):
  225. description = ''
  226. if model is not None: # TODO: test this.
  227. # Attempt to infer a field description if possible.
  228. try:
  229. model_field = model._meta.get_field(variable)
  230. except Exception:
  231. model_field = None
  232. if model_field is not None and model_field.help_text:
  233. description = force_str(model_field.help_text)
  234. elif model_field is not None and model_field.primary_key:
  235. description = get_pk_description(model, model_field)
  236. parameter = {
  237. "name": variable,
  238. "in": "path",
  239. "required": True,
  240. "description": description,
  241. 'schema': {
  242. 'type': 'string', # TODO: integer, pattern, ...
  243. },
  244. }
  245. parameters.append(parameter)
  246. return parameters
  247. def get_filter_parameters(self, path, method):
  248. if not self.allows_filters(path, method):
  249. return []
  250. parameters = []
  251. for filter_backend in self.view.filter_backends:
  252. parameters += filter_backend().get_schema_operation_parameters(self.view)
  253. return parameters
  254. def allows_filters(self, path, method):
  255. """
  256. Determine whether to include filter Fields in schema.
  257. Default implementation looks for ModelViewSet or GenericAPIView
  258. actions/methods that cause filtering on the default implementation.
  259. """
  260. if getattr(self.view, 'filter_backends', None) is None:
  261. return False
  262. if hasattr(self.view, 'action'):
  263. return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
  264. return method.lower() in ["get", "put", "patch", "delete"]
  265. def get_pagination_parameters(self, path, method):
  266. view = self.view
  267. if not is_list_view(path, method, view):
  268. return []
  269. paginator = self.get_paginator()
  270. if not paginator:
  271. return []
  272. return paginator.get_schema_operation_parameters(view)
  273. def map_choicefield(self, field):
  274. choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates
  275. if all(isinstance(choice, bool) for choice in choices):
  276. type = 'boolean'
  277. elif all(isinstance(choice, int) for choice in choices):
  278. type = 'integer'
  279. elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
  280. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
  281. type = 'number'
  282. elif all(isinstance(choice, str) for choice in choices):
  283. type = 'string'
  284. else:
  285. type = None
  286. mapping = {
  287. # The value of `enum` keyword MUST be an array and SHOULD be unique.
  288. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20
  289. 'enum': choices
  290. }
  291. # If We figured out `type` then and only then we should set it. It must be a string.
  292. # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type
  293. # It is optional but it can not be null.
  294. # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
  295. if type:
  296. mapping['type'] = type
  297. return mapping
  298. def map_field(self, field):
  299. # Nested Serializers, `many` or not.
  300. if isinstance(field, serializers.ListSerializer):
  301. return {
  302. 'type': 'array',
  303. 'items': self.map_serializer(field.child)
  304. }
  305. if isinstance(field, serializers.Serializer):
  306. data = self.map_serializer(field)
  307. data['type'] = 'object'
  308. return data
  309. # Related fields.
  310. if isinstance(field, serializers.ManyRelatedField):
  311. return {
  312. 'type': 'array',
  313. 'items': self.map_field(field.child_relation)
  314. }
  315. if isinstance(field, serializers.PrimaryKeyRelatedField):
  316. model = getattr(field.queryset, 'model', None)
  317. if model is not None:
  318. model_field = model._meta.pk
  319. if isinstance(model_field, models.AutoField):
  320. return {'type': 'integer'}
  321. # ChoiceFields (single and multiple).
  322. # Q:
  323. # - Is 'type' required?
  324. # - can we determine the TYPE of a choicefield?
  325. if isinstance(field, serializers.MultipleChoiceField):
  326. return {
  327. 'type': 'array',
  328. 'items': self.map_choicefield(field)
  329. }
  330. if isinstance(field, serializers.ChoiceField):
  331. return self.map_choicefield(field)
  332. # ListField.
  333. if isinstance(field, serializers.ListField):
  334. mapping = {
  335. 'type': 'array',
  336. 'items': {},
  337. }
  338. if not isinstance(field.child, _UnvalidatedField):
  339. mapping['items'] = self.map_field(field.child)
  340. return mapping
  341. # DateField and DateTimeField type is string
  342. if isinstance(field, serializers.DateField):
  343. return {
  344. 'type': 'string',
  345. 'format': 'date',
  346. }
  347. if isinstance(field, serializers.DateTimeField):
  348. return {
  349. 'type': 'string',
  350. 'format': 'date-time',
  351. }
  352. # "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
  353. # see: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
  354. # see also: https://swagger.io/docs/specification/data-models/data-types/#string
  355. if isinstance(field, serializers.EmailField):
  356. return {
  357. 'type': 'string',
  358. 'format': 'email'
  359. }
  360. if isinstance(field, serializers.URLField):
  361. return {
  362. 'type': 'string',
  363. 'format': 'uri'
  364. }
  365. if isinstance(field, serializers.UUIDField):
  366. return {
  367. 'type': 'string',
  368. 'format': 'uuid'
  369. }
  370. if isinstance(field, serializers.IPAddressField):
  371. content = {
  372. 'type': 'string',
  373. }
  374. if field.protocol != 'both':
  375. content['format'] = field.protocol
  376. return content
  377. if isinstance(field, serializers.DecimalField):
  378. if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
  379. content = {
  380. 'type': 'string',
  381. 'format': 'decimal',
  382. }
  383. else:
  384. content = {
  385. 'type': 'number'
  386. }
  387. if field.decimal_places:
  388. content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1')
  389. if field.max_whole_digits:
  390. content['maximum'] = int(field.max_whole_digits * '9') + 1
  391. content['minimum'] = -content['maximum']
  392. self._map_min_max(field, content)
  393. return content
  394. if isinstance(field, serializers.FloatField):
  395. content = {
  396. 'type': 'number',
  397. }
  398. self._map_min_max(field, content)
  399. return content
  400. if isinstance(field, serializers.IntegerField):
  401. content = {
  402. 'type': 'integer'
  403. }
  404. self._map_min_max(field, content)
  405. # 2147483647 is max for int32_size, so we use int64 for format
  406. if int(content.get('maximum', 0)) > 2147483647 or int(content.get('minimum', 0)) > 2147483647:
  407. content['format'] = 'int64'
  408. return content
  409. if isinstance(field, serializers.FileField):
  410. return {
  411. 'type': 'string',
  412. 'format': 'binary'
  413. }
  414. # Simplest cases, default to 'string' type:
  415. FIELD_CLASS_SCHEMA_TYPE = {
  416. serializers.BooleanField: 'boolean',
  417. serializers.JSONField: 'object',
  418. serializers.DictField: 'object',
  419. serializers.HStoreField: 'object',
  420. }
  421. return {'type': FIELD_CLASS_SCHEMA_TYPE.get(field.__class__, 'string')}
  422. def _map_min_max(self, field, content):
  423. if field.max_value:
  424. content['maximum'] = field.max_value
  425. if field.min_value:
  426. content['minimum'] = field.min_value
  427. def map_serializer(self, serializer):
  428. # Assuming we have a valid serializer instance.
  429. required = []
  430. properties = {}
  431. for field in serializer.fields.values():
  432. if isinstance(field, serializers.HiddenField):
  433. continue
  434. if field.required:
  435. required.append(field.field_name)
  436. schema = self.map_field(field)
  437. if field.read_only:
  438. schema['readOnly'] = True
  439. if field.write_only:
  440. schema['writeOnly'] = True
  441. if field.allow_null:
  442. schema['nullable'] = True
  443. if field.default is not None and field.default != empty and not callable(field.default):
  444. schema['default'] = field.default
  445. if field.help_text:
  446. schema['description'] = str(field.help_text)
  447. self.map_field_validators(field, schema)
  448. properties[field.field_name] = schema
  449. result = {
  450. 'type': 'object',
  451. 'properties': properties
  452. }
  453. if required:
  454. result['required'] = required
  455. return result
  456. def map_field_validators(self, field, schema):
  457. """
  458. map field validators
  459. """
  460. for v in field.validators:
  461. # "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
  462. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
  463. if isinstance(v, EmailValidator):
  464. schema['format'] = 'email'
  465. if isinstance(v, URLValidator):
  466. schema['format'] = 'uri'
  467. if isinstance(v, RegexValidator):
  468. # In Python, the token \Z does what \z does in other engines.
  469. # https://stackoverflow.com/questions/53283160
  470. schema['pattern'] = v.regex.pattern.replace('\\Z', '\\z')
  471. elif isinstance(v, MaxLengthValidator):
  472. attr_name = 'maxLength'
  473. if isinstance(field, serializers.ListField):
  474. attr_name = 'maxItems'
  475. schema[attr_name] = v.limit_value
  476. elif isinstance(v, MinLengthValidator):
  477. attr_name = 'minLength'
  478. if isinstance(field, serializers.ListField):
  479. attr_name = 'minItems'
  480. schema[attr_name] = v.limit_value
  481. elif isinstance(v, MaxValueValidator):
  482. schema['maximum'] = v.limit_value
  483. elif isinstance(v, MinValueValidator):
  484. schema['minimum'] = v.limit_value
  485. elif isinstance(v, DecimalValidator) and \
  486. not getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
  487. if v.decimal_places:
  488. schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1')
  489. if v.max_digits:
  490. digits = v.max_digits
  491. if v.decimal_places is not None and v.decimal_places > 0:
  492. digits -= v.decimal_places
  493. schema['maximum'] = int(digits * '9') + 1
  494. schema['minimum'] = -schema['maximum']
  495. def get_paginator(self):
  496. pagination_class = getattr(self.view, 'pagination_class', None)
  497. if pagination_class:
  498. return pagination_class()
  499. return None
  500. def map_parsers(self, path, method):
  501. return list(map(attrgetter('media_type'), self.view.parser_classes))
  502. def map_renderers(self, path, method):
  503. media_types = []
  504. for renderer in self.view.renderer_classes:
  505. # BrowsableAPIRenderer not relevant to OpenAPI spec
  506. if issubclass(renderer, renderers.BrowsableAPIRenderer):
  507. continue
  508. media_types.append(renderer.media_type)
  509. return media_types
  510. def get_serializer(self, path, method):
  511. view = self.view
  512. if not hasattr(view, 'get_serializer'):
  513. return None
  514. try:
  515. return view.get_serializer()
  516. except exceptions.APIException:
  517. warnings.warn('{}.get_serializer() raised an exception during '
  518. 'schema generation. Serializer fields will not be '
  519. 'generated for {} {}.'
  520. .format(view.__class__.__name__, method, path))
  521. return None
  522. def get_request_serializer(self, path, method):
  523. """
  524. Override this method if your view uses a different serializer for
  525. handling request body.
  526. """
  527. return self.get_serializer(path, method)
  528. def get_response_serializer(self, path, method):
  529. """
  530. Override this method if your view uses a different serializer for
  531. populating response data.
  532. """
  533. return self.get_serializer(path, method)
  534. def _get_reference(self, serializer):
  535. return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
  536. def get_request_body(self, path, method):
  537. if method not in ('PUT', 'PATCH', 'POST'):
  538. return {}
  539. self.request_media_types = self.map_parsers(path, method)
  540. serializer = self.get_request_serializer(path, method)
  541. if not isinstance(serializer, serializers.Serializer):
  542. item_schema = {}
  543. else:
  544. item_schema = self._get_reference(serializer)
  545. return {
  546. 'content': {
  547. ct: {'schema': item_schema}
  548. for ct in self.request_media_types
  549. }
  550. }
  551. def get_responses(self, path, method):
  552. if method == 'DELETE':
  553. return {
  554. '204': {
  555. 'description': ''
  556. }
  557. }
  558. self.response_media_types = self.map_renderers(path, method)
  559. serializer = self.get_response_serializer(path, method)
  560. if not isinstance(serializer, serializers.Serializer):
  561. item_schema = {}
  562. else:
  563. item_schema = self._get_reference(serializer)
  564. if is_list_view(path, method, self.view):
  565. response_schema = {
  566. 'type': 'array',
  567. 'items': item_schema,
  568. }
  569. paginator = self.get_paginator()
  570. if paginator:
  571. response_schema = paginator.get_paginated_response_schema(response_schema)
  572. else:
  573. response_schema = item_schema
  574. status_code = '201' if method == 'POST' else '200'
  575. return {
  576. status_code: {
  577. 'content': {
  578. ct: {'schema': response_schema}
  579. for ct in self.response_media_types
  580. },
  581. # description is a mandatory property,
  582. # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject
  583. # TODO: put something meaningful into it
  584. 'description': ""
  585. }
  586. }
  587. def get_tags(self, path, method):
  588. # If user have specified tags, use them.
  589. if self._tags:
  590. return self._tags
  591. # First element of a specific path could be valid tag. This is a fallback solution.
  592. # PUT, PATCH, GET(Retrieve), DELETE: /user_profile/{id}/ tags = [user-profile]
  593. # POST, GET(List): /user_profile/ tags = [user-profile]
  594. if path.startswith('/'):
  595. path = path[1:]
  596. return [path.split('/')[0].replace('_', '-')]
  597. def _get_path_parameters(self, path, method):
  598. warnings.warn(
  599. "Method `_get_path_parameters()` has been renamed to `get_path_parameters()`. "
  600. "The old name will be removed in DRF v3.14.",
  601. RemovedInDRF314Warning, stacklevel=2
  602. )
  603. return self.get_path_parameters(path, method)
  604. def _get_filter_parameters(self, path, method):
  605. warnings.warn(
  606. "Method `_get_filter_parameters()` has been renamed to `get_filter_parameters()`. "
  607. "The old name will be removed in DRF v3.14.",
  608. RemovedInDRF314Warning, stacklevel=2
  609. )
  610. return self.get_filter_parameters(path, method)
  611. def _get_responses(self, path, method):
  612. warnings.warn(
  613. "Method `_get_responses()` has been renamed to `get_responses()`. "
  614. "The old name will be removed in DRF v3.14.",
  615. RemovedInDRF314Warning, stacklevel=2
  616. )
  617. return self.get_responses(path, method)
  618. def _get_request_body(self, path, method):
  619. warnings.warn(
  620. "Method `_get_request_body()` has been renamed to `get_request_body()`. "
  621. "The old name will be removed in DRF v3.14.",
  622. RemovedInDRF314Warning, stacklevel=2
  623. )
  624. return self.get_request_body(path, method)
  625. def _get_serializer(self, path, method):
  626. warnings.warn(
  627. "Method `_get_serializer()` has been renamed to `get_serializer()`. "
  628. "The old name will be removed in DRF v3.14.",
  629. RemovedInDRF314Warning, stacklevel=2
  630. )
  631. return self.get_serializer(path, method)
  632. def _get_paginator(self):
  633. warnings.warn(
  634. "Method `_get_paginator()` has been renamed to `get_paginator()`. "
  635. "The old name will be removed in DRF v3.14.",
  636. RemovedInDRF314Warning, stacklevel=2
  637. )
  638. return self.get_paginator()
  639. def _map_field_validators(self, field, schema):
  640. warnings.warn(
  641. "Method `_map_field_validators()` has been renamed to `map_field_validators()`. "
  642. "The old name will be removed in DRF v3.14.",
  643. RemovedInDRF314Warning, stacklevel=2
  644. )
  645. return self.map_field_validators(field, schema)
  646. def _map_serializer(self, serializer):
  647. warnings.warn(
  648. "Method `_map_serializer()` has been renamed to `map_serializer()`. "
  649. "The old name will be removed in DRF v3.14.",
  650. RemovedInDRF314Warning, stacklevel=2
  651. )
  652. return self.map_serializer(serializer)
  653. def _map_field(self, field):
  654. warnings.warn(
  655. "Method `_map_field()` has been renamed to `map_field()`. "
  656. "The old name will be removed in DRF v3.14.",
  657. RemovedInDRF314Warning, stacklevel=2
  658. )
  659. return self.map_field(field)
  660. def _map_choicefield(self, field):
  661. warnings.warn(
  662. "Method `_map_choicefield()` has been renamed to `map_choicefield()`. "
  663. "The old name will be removed in DRF v3.14.",
  664. RemovedInDRF314Warning, stacklevel=2
  665. )
  666. return self.map_choicefield(field)
  667. def _get_pagination_parameters(self, path, method):
  668. warnings.warn(
  669. "Method `_get_pagination_parameters()` has been renamed to `get_pagination_parameters()`. "
  670. "The old name will be removed in DRF v3.14.",
  671. RemovedInDRF314Warning, stacklevel=2
  672. )
  673. return self.get_pagination_parameters(path, method)
  674. def _allows_filters(self, path, method):
  675. warnings.warn(
  676. "Method `_allows_filters()` has been renamed to `allows_filters()`. "
  677. "The old name will be removed in DRF v3.14.",
  678. RemovedInDRF314Warning, stacklevel=2
  679. )
  680. return self.allows_filters(path, method)