From b724083b962bfa2fecb0d73b1a6252f28ff82cfa Mon Sep 17 00:00:00 2001 From: Dmytro Date: Thu, 1 Feb 2024 21:52:33 +0200 Subject: [PATCH] Pass annotated origin type to build OpenAPI docs (#475) * Pass annotated origin type to build OpenAPI docs * Make Python 3.8 happy --- blacksheep/server/openapi/v3.py | 10 + tests/test_openapi_v3.py | 385 ++++++++++++++++++++++++++++++-- 2 files changed, 378 insertions(+), 17 deletions(-) diff --git a/blacksheep/server/openapi/v3.py b/blacksheep/server/openapi/v3.py index 3ee247ce..c38f5359 100644 --- a/blacksheep/server/openapi/v3.py +++ b/blacksheep/server/openapi/v3.py @@ -1,11 +1,16 @@ import collections.abc as collections_abc import inspect +import sys import warnings from abc import ABC, abstractmethod from dataclasses import dataclass, fields, is_dataclass from datetime import date, datetime from enum import Enum, IntEnum from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union + +if sys.version_info >= (3, 9): + from typing import _AnnotatedAlias as AnnotatedAlias + from typing import _GenericAlias as GenericAlias from typing import get_type_hints from uuid import UUID @@ -649,6 +654,11 @@ def _get_schema_by_type( if stored_ref: # pragma: no cover return stored_ref + if sys.version_info >= (3, 9): + if isinstance(object_type, AnnotatedAlias): + # Replace Annotated object type with the original type + object_type = getattr(object_type, "__origin__") + if self._can_handle_class_type(object_type): return self._get_schema_for_class(object_type) diff --git a/tests/test_openapi_v3.py b/tests/test_openapi_v3.py index 53389e62..6d3c294c 100644 --- a/tests/test_openapi_v3.py +++ b/tests/test_openapi_v3.py @@ -1,3 +1,4 @@ +import sys from dataclasses import dataclass from datetime import date, datetime from enum import IntEnum @@ -22,7 +23,14 @@ ) from pydantic import VERSION as PYDANTIC_LIB_VERSION from pydantic import BaseModel, HttpUrl -from pydantic.types import NegativeFloat, PositiveInt, condecimal, confloat, conint +from pydantic.types import ( + UUID4, + NegativeFloat, + PositiveInt, + condecimal, + confloat, + conint, +) from blacksheep.server.application import Application from blacksheep.server.bindings import FromForm @@ -78,6 +86,9 @@ class PydCat(BaseModel): id: int name: str + if sys.version_info >= (3, 9): + childs: list[UUID4] + class PydPaginatedSetOfCat(BaseModel): items: List[PydCat] @@ -1048,9 +1059,10 @@ def home() -> PydPaginatedSetOfCat: yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + if sys.version_info >= (3, 9): + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1073,6 +1085,7 @@ def home() -> PydPaginatedSetOfCat: required: - id - name + - childs properties: id: type: integer @@ -1081,6 +1094,13 @@ def home() -> PydPaginatedSetOfCat: name: type: string nullable: false + childs: + type: array + nullable: false + items: + type: string + format: uuid + nullable: false PydPaginatedSetOfCat: type: object required: @@ -1098,7 +1118,59 @@ def home() -> PydPaginatedSetOfCat: nullable: false tags: [] """.strip() - ) + ) + else: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydPaginatedSetOfCat' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + PydPaginatedSetOfCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false +tags: [] +""".strip() + ) @pytest.mark.asyncio @@ -1116,9 +1188,10 @@ def home() -> PydTypeWithChildModels: yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + if sys.version_info >= (3, 9): + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1141,6 +1214,7 @@ def home() -> PydTypeWithChildModels: required: - id - name + - childs properties: id: type: integer @@ -1149,6 +1223,13 @@ def home() -> PydTypeWithChildModels: name: type: string nullable: false + childs: + type: array + nullable: false + items: + type: string + format: uuid + nullable: false PydPaginatedSetOfCat: type: object required: @@ -1186,8 +1267,81 @@ def home() -> PydTypeWithChildModels: friend: $ref: '#/components/schemas/PydExampleWithSpecificTypes' tags: [] -""".strip() - ) + """.strip() + ) + else: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydTypeWithChildModels' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + PydPaginatedSetOfCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false + PydExampleWithSpecificTypes: + type: object + required: + - url + properties: + url: + type: string + format: uri + maxLength: 2083 + minLength: 1 + nullable: false + PydTypeWithChildModels: + type: object + required: + - child + - friend + properties: + child: + $ref: '#/components/schemas/PydPaginatedSetOfCat' + friend: + $ref: '#/components/schemas/PydExampleWithSpecificTypes' +tags: [] + """.strip() + ) @pytest.mark.asyncio @@ -1205,9 +1359,10 @@ def home() -> PaginatedSet[PydCat]: yaml = serializer.to_yaml(docs.generate_documentation(app)) - assert ( - yaml.strip() - == """ + if sys.version_info >= (3, 9): + assert ( + yaml.strip() + == """ openapi: 3.0.3 info: title: Example @@ -1230,6 +1385,7 @@ def home() -> PaginatedSet[PydCat]: required: - id - name + - childs properties: id: type: integer @@ -1238,6 +1394,13 @@ def home() -> PaginatedSet[PydCat]: name: type: string nullable: false + childs: + type: array + nullable: false + items: + type: string + format: uuid + nullable: false PaginatedSetOfPydCat: type: object required: @@ -1254,8 +1417,60 @@ def home() -> PaginatedSet[PydCat]: format: int64 nullable: false tags: [] -""".strip() - ) + """.strip() + ) + else: + assert ( + yaml.strip() + == """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedSetOfPydCat' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + PaginatedSetOfPydCat: + type: object + required: + - items + - total + properties: + items: + type: array + nullable: false + items: + $ref: '#/components/schemas/PydCat' + total: + type: integer + format: int64 + nullable: false +tags: [] + """.strip() + ) @pytest.mark.asyncio @@ -1703,7 +1918,73 @@ def home() -> PydResponse[PydCat]: yaml = serializer.to_yaml(docs.generate_documentation(app)) if PYDANTIC_VERSION == 1: - expected_result = """ + if sys.version_info >= (3, 9): + expected_result = """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydResponse[PydCat]' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + - childs + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + childs: + type: array + nullable: false + items: + type: string + format: uuid + nullable: false + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int64 + nullable: false + message: + type: string + nullable: false + PydResponse[PydCat]: + type: object + required: + - data + - error + properties: + data: + $ref: '#/components/schemas/PydCat' + error: + $ref: '#/components/schemas/Error' +tags: [] +""".strip() + else: + expected_result = """ openapi: 3.0.3 info: title: Example @@ -1760,7 +2041,77 @@ def home() -> PydResponse[PydCat]: tags: [] """.strip() elif PYDANTIC_VERSION == 2: - expected_result = """ + if sys.version_info >= (3, 9): + expected_result = """ +openapi: 3.0.3 +info: + title: Example + version: 0.0.1 +paths: + /: + get: + responses: + '200': + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/PydResponse[PydCat]' + operationId: home +components: + schemas: + PydCat: + type: object + required: + - id + - name + - childs + properties: + id: + type: integer + format: int64 + nullable: false + name: + type: string + nullable: false + childs: + type: array + nullable: false + items: + type: string + format: uuid + nullable: false + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int64 + nullable: false + message: + type: string + nullable: false + PydResponse[PydCat]: + type: object + required: + - data + - error + properties: + data: + anyOf: + - $ref: '#/components/schemas/PydCat' + - type: 'null' + error: + anyOf: + - $ref: '#/components/schemas/Error' + - type: 'null' +tags: [] +""".strip() + else: + expected_result = """ openapi: 3.0.3 info: title: Example