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