From 6f3a656fe26874a6296e7fe8aeb4056453afcc9a Mon Sep 17 00:00:00 2001 From: Eric Butler Date: Thu, 18 Jan 2024 15:29:12 -0500 Subject: [PATCH 1/3] fix: extend_schema_field with dict param and oas 3.1 the dict was being mutated, so every subsequent use of the field would have more stuff appended to the schema --- drf_spectacular/plumbing.py | 4 ++ tests/test_extend_schema.py | 22 +++++++ ..._extend_schema_field_with_dict_oas_3_1.yml | 64 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 tests/test_extend_schema_field_with_dict_oas_3_1.yml diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index 249083e0..e03b866b 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -1,4 +1,5 @@ import collections +import copy import functools import hashlib import inspect @@ -525,6 +526,9 @@ def safe_ref(schema: _SchemaType) -> _SchemaType: def append_meta(schema: _SchemaType, meta: _SchemaType) -> _SchemaType: if spectacular_settings.OAS_VERSION.startswith('3.1'): + schema = copy.deepcopy(schema) + meta = copy.deepcopy(meta) + schema_nullable = meta.pop('nullable', None) meta_nullable = schema.pop('nullable', None) diff --git a/tests/test_extend_schema.py b/tests/test_extend_schema.py index a9c64fe0..3dfedcf5 100644 --- a/tests/test_extend_schema.py +++ b/tests/test_extend_schema.py @@ -232,6 +232,28 @@ def test_extend_schema(no_warnings): ) +@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') +def test_extend_schema_field_with_dict_oas_3_1(no_warnings): + @extend_schema_field({"type": "string"}) + class CustomField(serializers.CharField): + pass + + class XSerializer(serializers.Serializer): + field1 = CustomField(read_only=True, allow_null=True) + field2 = CustomField(read_only=True, allow_null=True) + field3 = CustomField(read_only=True, allow_null=True) + + @extend_schema(request=XSerializer, responses=XSerializer) + @api_view(['POST']) + def view_func(request, format=None): + pass # pragma: no cover + + assert_schema( + generate_schema('x', view_function=view_func), + 'tests/test_extend_schema_field_with_dict_oas_3_1.yml' + ) + + def test_layered_extend_schema_on_view_and_method_with_meta(no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField() diff --git a/tests/test_extend_schema_field_with_dict_oas_3_1.yml b/tests/test_extend_schema_field_with_dict_oas_3_1.yml new file mode 100644 index 00000000..ae8d8395 --- /dev/null +++ b/tests/test_extend_schema_field_with_dict_oas_3_1.yml @@ -0,0 +1,64 @@ +openapi: 3.1.0 +info: + title: '' + version: 0.0.0 +paths: + /x: + post: + operationId: x_create + tags: + - x + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/X' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/X' + multipart/form-data: + schema: + $ref: '#/components/schemas/X' + security: + - cookieAuth: [] + - basicAuth: [] + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/X' + description: '' +components: + schemas: + X: + type: object + properties: + field1: + type: + - string + - 'null' + readOnly: true + field2: + type: + - string + - 'null' + readOnly: true + field3: + type: + - string + - 'null' + readOnly: true + required: + - field1 + - field2 + - field3 + securitySchemes: + basicAuth: + type: http + scheme: basic + cookieAuth: + type: apiKey + in: cookie + name: sessionid From 58039cb9de3329161878d31e27e3eda7d27de122 Mon Sep 17 00:00:00 2001 From: "T. Franzel" Date: Thu, 3 Oct 2024 16:36:41 +0200 Subject: [PATCH 2/3] some minors (resolves #1147) Co-authored-by: Eric Butler --- drf_spectacular/plumbing.py | 4 +- tests/test_extend_schema.py | 11 ++-- ..._extend_schema_field_with_dict_oas_3_1.yml | 64 ------------------- 3 files changed, 9 insertions(+), 70 deletions(-) delete mode 100644 tests/test_extend_schema_field_with_dict_oas_3_1.yml diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index e03b866b..e3c1b1cc 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -526,8 +526,8 @@ def safe_ref(schema: _SchemaType) -> _SchemaType: def append_meta(schema: _SchemaType, meta: _SchemaType) -> _SchemaType: if spectacular_settings.OAS_VERSION.startswith('3.1'): - schema = copy.deepcopy(schema) - meta = copy.deepcopy(meta) + schema = schema.copy() + meta = meta.copy() schema_nullable = meta.pop('nullable', None) meta_nullable = schema.pop('nullable', None) diff --git a/tests/test_extend_schema.py b/tests/test_extend_schema.py index 3dfedcf5..9a561b9d 100644 --- a/tests/test_extend_schema.py +++ b/tests/test_extend_schema.py @@ -248,10 +248,13 @@ class XSerializer(serializers.Serializer): def view_func(request, format=None): pass # pragma: no cover - assert_schema( - generate_schema('x', view_function=view_func), - 'tests/test_extend_schema_field_with_dict_oas_3_1.yml' - ) + schema = generate_schema('x', view_function=view_func) + + assert schema['components']['schemas']['X']['properties'] == { + 'field1': {'readOnly': True, 'type': ['string', 'null']}, + 'field2': {'readOnly': True, 'type': ['string', 'null']}, + 'field3': {'readOnly': True, 'type': ['string', 'null']} + } def test_layered_extend_schema_on_view_and_method_with_meta(no_warnings): diff --git a/tests/test_extend_schema_field_with_dict_oas_3_1.yml b/tests/test_extend_schema_field_with_dict_oas_3_1.yml deleted file mode 100644 index ae8d8395..00000000 --- a/tests/test_extend_schema_field_with_dict_oas_3_1.yml +++ /dev/null @@ -1,64 +0,0 @@ -openapi: 3.1.0 -info: - title: '' - version: 0.0.0 -paths: - /x: - post: - operationId: x_create - tags: - - x - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/X' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/X' - multipart/form-data: - schema: - $ref: '#/components/schemas/X' - security: - - cookieAuth: [] - - basicAuth: [] - - {} - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/X' - description: '' -components: - schemas: - X: - type: object - properties: - field1: - type: - - string - - 'null' - readOnly: true - field2: - type: - - string - - 'null' - readOnly: true - field3: - type: - - string - - 'null' - readOnly: true - required: - - field1 - - field2 - - field3 - securitySchemes: - basicAuth: - type: http - scheme: basic - cookieAuth: - type: apiKey - in: cookie - name: sessionid From 508a246cadd98a8796903633a5ec038893992731 Mon Sep 17 00:00:00 2001 From: "T. Franzel" Date: Thu, 3 Oct 2024 16:45:30 +0200 Subject: [PATCH 3/3] bugfix @extend_schema_field raw schema already in OAS3.1 --- drf_spectacular/plumbing.py | 6 ++++-- tests/test_extend_schema.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index e3c1b1cc..497b4d53 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -1,5 +1,4 @@ import collections -import copy import functools import hashlib import inspect @@ -534,7 +533,10 @@ def append_meta(schema: _SchemaType, meta: _SchemaType) -> _SchemaType: if schema_nullable or meta_nullable: if 'type' in schema: - schema['type'] = [schema['type'], 'null'] + if isinstance(schema['type'], str): + schema['type'] = [schema['type'], 'null'] + else: + schema['type'] = [*schema['type'], 'null'] elif '$ref' in schema: schema = {'oneOf': [schema, {'type': 'null'}]} elif len(schema) == 1 and 'oneOf' in schema: diff --git a/tests/test_extend_schema.py b/tests/test_extend_schema.py index 9a561b9d..f142eef2 100644 --- a/tests/test_extend_schema.py +++ b/tests/test_extend_schema.py @@ -257,6 +257,29 @@ def view_func(request, format=None): } +@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0') +def test_extend_schema_field_with_schema_as_oas_3_1(no_warnings): + @extend_schema_field({'type': ['string', 'integer']}) + class CustomField(serializers.CharField): + pass + + class XSerializer(serializers.Serializer): + field1 = CustomField(read_only=True, allow_null=True) + field2 = CustomField(read_only=True, allow_null=True) + + @extend_schema(request=XSerializer, responses=XSerializer) + @api_view(['POST']) + def view_func(request, format=None): + pass # pragma: no cover + + schema = generate_schema('x', view_function=view_func) + + assert schema['components']['schemas']['X']['properties'] == { + 'field1': {'readOnly': True, 'type': ['string', 'integer', 'null']}, + 'field2': {'readOnly': True, 'type': ['string', 'integer', 'null']}, + } + + def test_layered_extend_schema_on_view_and_method_with_meta(no_warnings): class XSerializer(serializers.Serializer): field = serializers.IntegerField()