diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db0e625ab..9d27d4358 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.7' + rev: 'v0.4.8' hooks: - id: ruff files: "^datamodel_code_generator|^tests" diff --git a/README.md b/README.md index 5129c61d6..7bbb143c9 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,6 @@ and [msgspec.Struct](https://github.com/jcrist/msgspec) from an openapi file and ## Help See [documentation](https://koxudaxi.github.io/datamodel-code-generator) for more details. -## Sponsors -[![JetBrains](https://avatars.githubusercontent.com/u/60931315?s=200&v=4)](https://github.com/JetBrainsOfficial) - ## Quick Installation To install `datamodel-code-generator`: @@ -236,6 +233,45 @@ class Apis(BaseModel): ``` +## Supported input types +- OpenAPI 3 (YAML/JSON, [OpenAPI Data Type](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#data-types)); +- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html)); +- JSON/YAML/CSV Data (it will be converted to JSON Schema); +- Python dictionary (it will be converted to JSON Schema); +- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/)); + +## Supported output types +- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel; +- [pydantic_v2](https://docs.pydantic.dev/2.0/).BaseModel; +- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html); +- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict); +- [msgspec.Struct](https://github.com/jcrist/msgspec); +- Custom type from your [jinja2](https://jinja.palletsprojects.com/en/3.1.x/) template; + +## Sponsors + + + + + + +
+ + JetBrains Logo +

JetBrains

+
+
+ + Astral Logo +

Astral

+
+
+ + Datadog, Inc. Logo +

Datadog, Inc.

+
+
+ ## Projects that use datamodel-code-generator These OSS projects use datamodel-code-generator to generate many models. @@ -267,21 +303,6 @@ See the following linked projects for real world examples and inspiration. - [SeldonIO/MLServer](https://github.com/SeldonIO/MLServer) - *[generate-types.sh](https://github.com/SeldonIO/MLServer/blob/master/hack/generate-types.sh)* -## Supported input types -- OpenAPI 3 (YAML/JSON, [OpenAPI Data Type](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.2.md#data-types)); -- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html)); -- JSON/YAML/CSV Data (it will be converted to JSON Schema); -- Python dictionary (it will be converted to JSON Schema); -- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/)); - -## Supported output types -- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel; -- [pydantic_v2](https://docs.pydantic.dev/2.0/).BaseModel; -- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html); -- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict); -- [msgspec.Struct](https://github.com/jcrist/msgspec); -- Custom type from your [jinja2](https://jinja.palletsprojects.com/en/3.1.x/) template; - ## Installation To install `datamodel-code-generator`: @@ -319,6 +340,7 @@ This method needs the [http extra option](#http-extra-option) ## All Command Options The `datamodel-codegen` command: + ```bash usage: datamodel-codegen [options] diff --git a/datamodel_code_generator/parser/base.py b/datamodel_code_generator/parser/base.py index 3961208b6..8a8ea898c 100644 --- a/datamodel_code_generator/parser/base.py +++ b/datamodel_code_generator/parser/base.py @@ -700,6 +700,7 @@ def __change_from_import( from_, import_ = full_path = relative( model.module_name, data_type.full_name ) + import_ = import_.replace('-', '_') alias = scoped_model_resolver.add(full_path, import_).name @@ -778,8 +779,18 @@ def __apply_discriminator_type( discriminator_model.path.split('#/')[-1] != path.split('#/')[-1] ): - # TODO: support external reference - continue + if ( + path.startswith('#/') + or discriminator_model.path[:-1] + != path.split('/')[-1] + ): + t_path = path[str(path).find('/') + 1 :] + t_disc = discriminator_model.path[ + : str(discriminator_model.path).find('#') + ].lstrip('../') + t_disc_2 = '/'.join(t_disc.split('/')[1:]) + if t_path != t_disc and t_path != t_disc_2: + continue type_names.append(name) else: type_names = [discriminator_model.path.split('/')[-1]] @@ -1250,6 +1261,7 @@ class Processed(NamedTuple): init = True else: module = (*module[:-1], f'{module[-1]}.py') + module = tuple(part.replace('-', '_') for part in module) else: module = ('__init__.py',) diff --git a/docs/index.md b/docs/index.md index 071a6a9f5..6291035b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,9 +12,6 @@ This code generator creates [pydantic v1 and v2](https://docs.pydantic.dev/) mod [![Pydantic v1](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v1.json)](https://pydantic.dev) [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) -## Sponsors -[![JetBrains](https://avatars.githubusercontent.com/u/60931315?s=200&v=4)](https://github.com/JetBrainsOfficial) - ## Quick Installation To install `datamodel-code-generator`: @@ -232,6 +229,44 @@ class Apis(BaseModel): ``` +## Supported input types +- OpenAPI 3 (YAML/JSON, [OpenAPI Data Type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types)); +- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html)); +- JSON/YAML/CSV Data (it will be converted to JSON Schema); +- Python dictionary (it will be converted to JSON Schema); +- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/)); + +## Supported output types +- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel; +- [pydantic_v2](https://docs.pydantic.dev/2.0/).BaseModel; +- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html); +- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict); +- [msgspec.Struct](https://github.com/jcrist/msgspec); +- Custom type from your [jinja2](https://jinja.palletsprojects.com/en/3.1.x) template; + +## Sponsors + + + + + + +
+ + JetBrains Logo +

JetBrains

+
+
+ + Astral Logo +

Astral

+
+
+ + Datadog, Inc. Logo +

Datadog, Inc.

+
+
## Projects that use datamodel-code-generator These OSS projects use datamodel-code-generator to generate many models. @@ -263,21 +298,6 @@ See the following linked projects for real world examples and inspiration. - [SeldonIO/MLServer](https://github.com/SeldonIO/MLServer) - *[generate-types.sh](https://github.com/SeldonIO/MLServer/blob/master/hack/generate-types.sh)* -## Supported input types -- OpenAPI 3 (YAML/JSON, [OpenAPI Data Type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types)); -- JSON Schema ([JSON Schema Core](http://json-schema.org/draft/2019-09/json-schema-validation.html)/[JSON Schema Validation](http://json-schema.org/draft/2019-09/json-schema-validation.html)); -- JSON/YAML/CSV Data (it will be converted to JSON Schema); -- Python dictionary (it will be converted to JSON Schema); -- GraphQL schema ([GraphQL Schemas and Types](https://graphql.org/learn/schema/)); - -## Supported output types -- [pydantic](https://docs.pydantic.dev/1.10/).BaseModel; -- [pydantic_v2](https://docs.pydantic.dev/2.0/).BaseModel; -- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html); -- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict); -- [msgspec.Struct](https://github.com/jcrist/msgspec); -- Custom type from your [jinja2](https://jinja.palletsprojects.com/en/3.1.x) template; - ## Installation To install `datamodel-code-generator`: diff --git a/tests/data/expected/main/discriminator_with_external_reference/output.py b/tests/data/expected/main/discriminator_with_external_reference/output.py new file mode 100644 index 000000000..8d87f2bba --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_reference/output.py @@ -0,0 +1,37 @@ +# generated by datamodel-codegen: +# filename: schema.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Optional, Union + +from pydantic import BaseModel, Field +from typing_extensions import Literal + + +class Type1(BaseModel): + type_: Literal['a'] = Field('a', title='Type ') + + +class Type2(BaseModel): + type_: Literal['b'] = Field('b', title='Type ') + ref_type: Optional[Type1] = Field(None, description='A referenced type.') + + +class Type4(BaseModel): + type_: Literal['d'] = Field('d', title='Type ') + + +class Type5(BaseModel): + type_: Literal['e'] = Field('e', title='Type ') + + +class Type3(BaseModel): + type_: Literal['c'] = Field('c', title='Type ') + + +class Response(BaseModel): + inner: Union[Type1, Type2, Type3, Type4, Type5] = Field( + ..., discriminator='type_', title='Inner' + ) diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/__init__.py b/tests/data/expected/main/discriminator_with_external_references_folder/__init__.py new file mode 100644 index 000000000..abc855210 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: discriminator_with_external_reference +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/__init__.py b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/__init__.py new file mode 100644 index 000000000..abc855210 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: discriminator_with_external_reference +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/__init__.py b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/__init__.py new file mode 100644 index 000000000..abc855210 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: discriminator_with_external_reference +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/type_1.py b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/type_1.py new file mode 100644 index 000000000..8df555769 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/artificial_folder/type_1.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: inner_folder/artificial_folder/type-1.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel, Field +from typing_extensions import Literal + + +class Type1(BaseModel): + type_: Literal['a'] = Field(..., const=True, title='Type ') diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/schema.py b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/schema.py new file mode 100644 index 000000000..21806d292 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/schema.py @@ -0,0 +1,25 @@ +# generated by datamodel-codegen: +# filename: inner_folder/schema.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Union + +from pydantic import BaseModel, Field +from typing_extensions import Literal + +from .. import type_4 +from ..subfolder import type_5 +from . import type_2 +from .artificial_folder import type_1 + + +class Type3(BaseModel): + type_: Literal['c'] = Field(..., const=True, title='Type ') + + +class Response(BaseModel): + inner: Union[type_1.Type1, type_2.Type2, Type3, type_4.Type4, type_5.Type5] = Field( + ..., discriminator='type_', title='Inner' + ) diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/type_2.py b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/type_2.py new file mode 100644 index 000000000..95342e63d --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/inner_folder/type_2.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: inner_folder/type-2.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field + +from .artificial_folder import type_1 + + +class Type2(BaseModel): + type_: Literal['b'] = Field(..., const=True, title='Type ') + ref_type: Optional[type_1.Type1] = Field(None, description='A referenced type.') diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/__init__.py b/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/__init__.py new file mode 100644 index 000000000..abc855210 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: discriminator_with_external_reference +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/type_5.py b/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/type_5.py new file mode 100644 index 000000000..2a1361794 --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/subfolder/type_5.py @@ -0,0 +1,11 @@ +# generated by datamodel-codegen: +# filename: subfolder/type-5.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class Type5(BaseModel): + type_: Literal['e'] = Field(..., const=True, title='Type ') diff --git a/tests/data/expected/main/discriminator_with_external_references_folder/type_4.py b/tests/data/expected/main/discriminator_with_external_references_folder/type_4.py new file mode 100644 index 000000000..abad814bf --- /dev/null +++ b/tests/data/expected/main/discriminator_with_external_references_folder/type_4.py @@ -0,0 +1,11 @@ +# generated by datamodel-codegen: +# filename: type-4.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class Type4(BaseModel): + type_: Literal['d'] = Field(..., const=True, title='Type ') diff --git a/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/artificial_folder/type-1.json b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/artificial_folder/type-1.json new file mode 100644 index 000000000..e7da9f40c --- /dev/null +++ b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/artificial_folder/type-1.json @@ -0,0 +1,11 @@ +{ + "properties": { + "type_": { + "const": "a", + "default": "a", + "title": "Type " + } + }, + "title": "Type1", + "type": "object" +} \ No newline at end of file diff --git a/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/schema.json b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/schema.json new file mode 100644 index 000000000..0fce2310c --- /dev/null +++ b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/schema.json @@ -0,0 +1,52 @@ +{ + "$def": { + "Type3": { + "properties": { + "type_": { + "const": "c", + "default": "c", + "title": "Type " + } + }, + "title": "Type3", + "type": "object" + } + }, + "properties": { + "inner": { + "discriminator": { + "mapping": { + "a": "./artificial_folder/type-1.json", + "b": "./type-2.json", + "c": "#/$def/Type3", + "d": "../type-4.json", + "e": "../subfolder/type-5.json" + }, + "propertyName": "type_" + }, + "oneOf": [ + { + "$ref": "./artificial_folder/type-1.json" + }, + { + "$ref": "./type-2.json" + }, + { + "$ref": "#/$def/Type3" + }, + { + "$ref": "../type-4.json" + }, + { + "$ref": "../subfolder/type-5.json" + } + ], + "title": "Inner" + } + }, + "required": [ + "inner" + ], + "title": "Response", + "type": "object" +} diff --git a/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/type-2.json b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/type-2.json new file mode 100644 index 000000000..b76c25b14 --- /dev/null +++ b/tests/data/jsonschema/discriminator_with_external_reference/inner_folder/type-2.json @@ -0,0 +1,15 @@ +{ + "properties": { + "type_": { + "const": "b", + "default": "b", + "title": "Type " + }, + "ref_type": { + "$ref": "./artificial_folder/type-1.json", + "description": "A referenced type." + } + }, + "title": "Type2", + "type": "object" +} \ No newline at end of file diff --git a/tests/data/jsonschema/discriminator_with_external_reference/subfolder/type-5.json b/tests/data/jsonschema/discriminator_with_external_reference/subfolder/type-5.json new file mode 100644 index 000000000..ada842093 --- /dev/null +++ b/tests/data/jsonschema/discriminator_with_external_reference/subfolder/type-5.json @@ -0,0 +1,11 @@ +{ + "properties": { + "type_": { + "const": "e", + "default": "e", + "title": "Type " + } + }, + "title": "Type5", + "type": "object" +} \ No newline at end of file diff --git a/tests/data/jsonschema/discriminator_with_external_reference/type-4.json b/tests/data/jsonschema/discriminator_with_external_reference/type-4.json new file mode 100644 index 000000000..4c357a275 --- /dev/null +++ b/tests/data/jsonschema/discriminator_with_external_reference/type-4.json @@ -0,0 +1,11 @@ +{ + "properties": { + "type_": { + "const": "d", + "default": "d", + "title": "Type " + } + }, + "title": "Type4", + "type": "object" +} \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 3854bc591..49bc9e0bd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6123,6 +6123,59 @@ def test_main_jsonschema_discriminator_literals(): ) +@freeze_time('2019-07-26') +def test_main_jsonschema_external_discriminator(): + with TemporaryDirectory() as output_dir: + output_file: Path = Path(output_dir) / 'output.py' + return_code: Exit = main( + [ + '--input', + str( + JSON_SCHEMA_DATA_PATH + / 'discriminator_with_external_reference' + / 'inner_folder' + / 'schema.json' + ), + '--output', + str(output_file), + '--output-model-type', + 'pydantic_v2.BaseModel', + ] + ) + assert return_code == Exit.OK + assert ( + output_file.read_text() + == ( + EXPECTED_MAIN_PATH + / 'discriminator_with_external_reference' + / 'output.py' + ).read_text() + ) + + +@freeze_time('2019-07-26') +def test_main_jsonschema_external_discriminator_folder(): + with TemporaryDirectory() as output_dir: + output_path: Path = Path(output_dir) + return_code: Exit = main( + [ + '--input', + str(JSON_SCHEMA_DATA_PATH / 'discriminator_with_external_reference'), + '--output', + str(output_path), + ] + ) + assert return_code == Exit.OK + main_modular_dir = ( + EXPECTED_MAIN_PATH / 'discriminator_with_external_references_folder' + ) + for path in main_modular_dir.rglob('*.py'): + result = output_path.joinpath( + path.relative_to(main_modular_dir) + ).read_text() + assert result == path.read_text() + + @freeze_time('2019-07-26') @pytest.mark.skipif( black.__version__.split('.')[0] == '19',