Skip to content
This repository was archived by the owner on Apr 3, 2023. It is now read-only.

Commit 706b922

Browse files
authored
Support customizing component name for pydantic models (#12)
* support customizing pydantic model name in component schema * added constraint to pydantic * refactor
1 parent 397870a commit 706b922

File tree

8 files changed

+397
-196
lines changed

8 files changed

+397
-196
lines changed

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ repos:
1010
exclude: "\\.idea/(.)*"
1111
- id: trailing-whitespace
1212
- repo: https://github.com/asottile/pyupgrade
13-
rev: v2.37.3
13+
rev: v2.38.0
1414
hooks:
1515
- id: pyupgrade
1616
args: ["--py37-plus"]
@@ -19,7 +19,7 @@ repos:
1919
hooks:
2020
- id: isort
2121
- repo: https://github.com/psf/black
22-
rev: 22.6.0
22+
rev: 22.8.0
2323
hooks:
2424
- id: black
2525
args: [--config=./pyproject.toml]
@@ -73,11 +73,11 @@ repos:
7373
"flake8-noqa",
7474
]
7575
- repo: https://github.com/johnfraney/flake8-markdown
76-
rev: v0.3.0
76+
rev: v0.4.0
7777
hooks:
7878
- id: flake8-markdown
7979
- repo: https://github.com/pycqa/pylint
80-
rev: "v2.15.0"
80+
rev: "v2.15.2"
8181
hooks:
8282
- id: pylint
8383
exclude: "test_*|docs"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
[v1.3.0]
4+
5+
- add support for `__schema_name__` dunder attribute on pydantic models
6+
37
[v1.2.0]
48

59
- update to pydantic `v1.1.0`

poetry.lock

Lines changed: 88 additions & 80 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from typing import TYPE_CHECKING, Any, List, Optional, Set, Type, TypeVar, cast
1+
from typing import TYPE_CHECKING, Any, Set, Type, TypeVar, cast
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, create_model
44
from pydantic.schema import schema
55

66
from pydantic_openapi_schema import v3_1_0
@@ -9,6 +9,7 @@
99
from typing import Dict
1010

1111
REF_PREFIX = "#/components/schemas/"
12+
SCHEMA_NAME_ATTRIBUTE = "__schema_name__"
1213

1314
T = TypeVar("T", bound=v3_1_0.OpenAPI)
1415

@@ -22,45 +23,35 @@ class OpenAPI310PydanticSchema(v3_1_0.Schema):
2223

2324
def construct_open_api_with_schema_class(
2425
open_api_schema: T,
25-
schema_classes: Optional[List[Type[BaseModel]]] = None,
26-
scan_for_pydantic_schema_reference: bool = True,
27-
by_alias: bool = True,
2826
) -> T:
2927
"""Construct a new OpenAPI object, with the use of pydantic classes to
3028
produce JSON schemas.
3129
3230
Args:
33-
open_api_schema: the base `OpenAPI` object
34-
schema_classes: pydantic classes that their schema will be used as "#/components/schemas" values
35-
scan_for_pydantic_schema_reference: flag to indicate if scanning for `PydanticSchemaReference`
36-
class is needed for "#/components/schemas" value updates
37-
by_alias: construct schema by alias (default is True)
31+
open_api_schema: An instance of the OpenAPI model.
3832
3933
Returns:
4034
new OpenAPI object with "#/components/schemas" values updated. If there is no update in
4135
"#/components/schemas" values, the original `open_api` will be returned.
4236
"""
4337
copied_schema = open_api_schema.copy(deep=True)
44-
45-
if scan_for_pydantic_schema_reference:
46-
extracted_schema_classes = extract_pydantic_types_to_openapi_components(
47-
obj=copied_schema, ref_class=v3_1_0.Reference
48-
)
49-
schema_classes = list(
50-
{*schema_classes, *extracted_schema_classes} if schema_classes else extracted_schema_classes
51-
)
38+
schema_classes = list(extract_pydantic_types_to_openapi_components(obj=copied_schema, ref_class=v3_1_0.Reference))
5239

5340
if not schema_classes:
5441
return open_api_schema
5542

5643
if not copied_schema.components:
5744
copied_schema.components = v3_1_0.Components(schemas={})
58-
elif not copied_schema.components.schemas:
45+
if copied_schema.components.schemas is None: # pragma: no cover
5946
copied_schema.components.schemas = cast("Dict[str, Any]", {})
6047

48+
schema_classes = [
49+
cls if not hasattr(cls, "__schema_name__") else create_model(getattr(cls, SCHEMA_NAME_ATTRIBUTE), __base__=cls)
50+
for cls in schema_classes
51+
]
6152
schema_classes.sort(key=lambda x: x.__name__)
62-
schema_definitions = schema(schema_classes, by_alias=by_alias, ref_prefix=REF_PREFIX)["definitions"]
63-
copied_schema.components.schemas.update( # type: ignore
53+
schema_definitions = schema(schema_classes, ref_prefix=REF_PREFIX)["definitions"]
54+
copied_schema.components.schemas.update(
6455
{key: v3_1_0.Schema.parse_obj(schema_dict) for key, schema_dict in schema_definitions.items()}
6556
)
6657
return copied_schema
@@ -84,22 +75,34 @@ def extract_pydantic_types_to_openapi_components(obj: Any, ref_class: Type[v3_1_
8475
for field in fields:
8576
child_obj = getattr(obj, field)
8677
if isinstance(child_obj, OpenAPI310PydanticSchema):
87-
setattr(obj, field, ref_class(ref=REF_PREFIX + child_obj.schema_class.__name__))
78+
setattr(obj, field, ref_class(ref=create_ref_prefix(child_obj.schema_class)))
8879
pydantic_schemas.add(child_obj.schema_class)
8980
else:
9081
pydantic_schemas.update(extract_pydantic_types_to_openapi_components(child_obj, ref_class=ref_class))
9182
elif isinstance(obj, list):
9283
for index, elem in enumerate(obj):
9384
if isinstance(elem, OpenAPI310PydanticSchema):
94-
obj[index] = ref_class(ref=REF_PREFIX + elem.schema_class.__name__)
85+
obj[index] = ref_class(ref=create_ref_prefix(elem.schema_class))
9586
pydantic_schemas.add(elem.schema_class)
9687
else:
9788
pydantic_schemas.update(extract_pydantic_types_to_openapi_components(elem, ref_class=ref_class))
9889
elif isinstance(obj, dict):
9990
for key, value in obj.items():
10091
if isinstance(value, OpenAPI310PydanticSchema):
101-
obj[key] = ref_class(ref=REF_PREFIX + value.schema_class.__name__)
92+
obj[key] = ref_class(ref=create_ref_prefix(value.schema_class))
10293
pydantic_schemas.add(value.schema_class)
10394
else:
10495
pydantic_schemas.update(extract_pydantic_types_to_openapi_components(value, ref_class=ref_class))
10596
return pydantic_schemas
97+
98+
99+
def create_ref_prefix(model: Type[BaseModel]) -> str:
100+
"""
101+
102+
Args:
103+
model: Pydantic model instance.
104+
105+
Returns:
106+
A prefixed name.
107+
"""
108+
return REF_PREFIX + getattr(model, SCHEMA_NAME_ATTRIBUTE, model.__name__)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pydantic-openapi-schema"
3-
version = "1.2.0"
3+
version = "1.3.0"
44
description = "OpenAPI Schema using pydantic. Forked for Starlite-API from 'openapi-schema-pydantic'."
55
authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
66
maintainers = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>", "Peter Schutt <peter.github@proton.me>", "Cody Fincher <cody.fincher@gmail.com>"]
@@ -28,7 +28,7 @@ packages = [
2828
]
2929
[tool.poetry.dependencies]
3030
python = ">=3.7"
31-
pydantic = "*"
31+
pydantic = ">=1.10.0"
3232
email-validator = "*"
3333

3434
[tool.poetry.dev-dependencies]

tests/v3_1_0/test_schema.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
from pydantic import BaseModel, Extra
2-
from pydantic.schema import schema
3-
41
from pydantic_openapi_schema.v3_1_0 import Reference, Schema
52

63

7-
def test_schema() -> None:
4+
def test_schema_parse_obj() -> None:
85
parsed_schema = Schema.parse_obj(
96
{
107
"title": "reference list",
@@ -16,30 +13,3 @@ def test_schema() -> None:
1613
assert isinstance(parsed_schema.allOf, list)
1714
assert isinstance(parsed_schema.allOf[0], Reference)
1815
assert parsed_schema.allOf[0].ref == "#/definitions/TestType"
19-
20-
21-
def test_issue_4() -> None:
22-
"""https://github.com/kuimono/openapi-schema-pydantic/issues/4."""
23-
24-
class TestModel(BaseModel):
25-
test_field: str
26-
27-
class Config:
28-
extra = Extra.forbid
29-
30-
schema_definition = schema([TestModel])
31-
assert schema_definition == {
32-
"definitions": {
33-
"TestModel": {
34-
"title": "TestModel",
35-
"type": "object",
36-
"properties": {"test_field": {"title": "Test Field", "type": "string"}},
37-
"required": ["test_field"],
38-
"additionalProperties": False,
39-
}
40-
}
41-
}
42-
43-
# allow "additionalProperties" to have boolean value
44-
result = Schema.parse_obj(schema_definition["definitions"]["TestModel"])
45-
assert result.additionalProperties is False

0 commit comments

Comments
 (0)