mirror of
https://github.com/koxudaxi/datamodel-code-generator.git
synced 2024-03-18 14:54:37 +03:00
Support TypedDict as output type (#1309)
* Support TypedDict as output type * Addd unittest * Fix lint * Support functional typeddict * Add unittest * Update unittest * Add comment * Update documents * Improve typedDict keys * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix unittest * Fix unittest * Fix lint * Fix unittest * Fix unittest --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# datamodel-code-generator
|
||||
|
||||
This code generator creates [pydantic](https://docs.pydantic.dev/) model and [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html) from an openapi file and others.
|
||||
This code generator creates [pydantic](https://docs.pydantic.dev/) model, [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html) and [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict) from an openapi file and others.
|
||||
|
||||
[](https://github.com/koxudaxi/datamodel-code-generator/actions?query=workflow%3ATest)
|
||||
[](https://pypi.python.org/pypi/datamodel-code-generator)
|
||||
@@ -260,6 +260,7 @@ These OSS projects use datamodel-code-generator to generate many models. See the
|
||||
## Supported output types
|
||||
- [pydantic](https://docs.pydantic.dev/).BaseModel
|
||||
- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html)
|
||||
- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -296,7 +297,7 @@ usage: datamodel-codegen [-h] [--input INPUT] [--url URL]
|
||||
[--http-headers HTTP_HEADER [HTTP_HEADER ...]]
|
||||
[--http-ignore-tls]
|
||||
[--input-file-type {auto,openapi,jsonschema,json,yaml,dict,csv}]
|
||||
[--output-model-type {pydantic.BaseModel,dataclasses.dataclass}]
|
||||
[--output-model-type {pydantic.BaseModel,dataclasses.dataclass,typing.TypedDict}]
|
||||
[--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]]
|
||||
[--output OUTPUT] [--base-class BASE_CLASS]
|
||||
[--field-constraints] [--use-annotated]
|
||||
@@ -347,7 +348,7 @@ options:
|
||||
certificate
|
||||
--input-file-type {auto,openapi,jsonschema,json,yaml,dict,csv}
|
||||
Input file type (default: auto)
|
||||
--output-model-type {pydantic.BaseModel,dataclasses.dataclass}
|
||||
--output-model-type {pydantic.BaseModel,dataclasses.dataclass,typing.TypedDict}
|
||||
Output model type (default: pydantic.BaseModel)
|
||||
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
|
||||
Scopes of OpenAPI model generation (default: schemas)
|
||||
|
||||
@@ -220,6 +220,7 @@ RAW_DATA_TYPES: List[InputFileType] = [
|
||||
class DataModelType(Enum):
|
||||
PydanticBaseModel = 'pydantic.BaseModel'
|
||||
DataclassesDataclass = 'dataclasses.dataclass'
|
||||
TypingTypedDict = 'typing.TypedDict'
|
||||
|
||||
|
||||
class OpenAPIScope(Enum):
|
||||
@@ -397,7 +398,7 @@ def generate(
|
||||
|
||||
from datamodel_code_generator.model import get_data_model_types
|
||||
|
||||
data_model_types = get_data_model_types(output_model_type)
|
||||
data_model_types = get_data_model_types(output_model_type, target_python_version)
|
||||
parser = parser_class(
|
||||
source=input_text or input_,
|
||||
data_model_type=data_model_types.data_model,
|
||||
@@ -427,7 +428,9 @@ def generate(
|
||||
use_field_description=use_field_description,
|
||||
use_default_kwarg=use_default_kwarg,
|
||||
reuse_model=reuse_model,
|
||||
enum_field_as_literal=enum_field_as_literal,
|
||||
enum_field_as_literal=LiteralType.All
|
||||
if output_model_type == DataModelType.TypingTypedDict
|
||||
else enum_field_as_literal,
|
||||
use_one_literal_as_default=use_one_literal_as_default,
|
||||
set_default_enum_member=set_default_enum_member,
|
||||
use_subclass_enum=use_subclass_enum,
|
||||
|
||||
@@ -9,6 +9,8 @@ import black
|
||||
import isort
|
||||
import toml
|
||||
|
||||
from datamodel_code_generator import cached_property
|
||||
|
||||
|
||||
class PythonVersion(Enum):
|
||||
PY_36 = '3.6'
|
||||
@@ -18,17 +20,41 @@ class PythonVersion(Enum):
|
||||
PY_310 = '3.10'
|
||||
PY_311 = '3.11'
|
||||
|
||||
@cached_property
|
||||
def _is_py_38_or_later(self) -> bool: # pragma: no cover
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value} # type: ignore
|
||||
|
||||
@cached_property
|
||||
def _is_py_39_or_later(self) -> bool: # pragma: no cover
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value, self.PY_38.value} # type: ignore
|
||||
|
||||
@cached_property
|
||||
def _is_py_310_or_later(self) -> bool: # pragma: no cover
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value, self.PY_38.value, self.PY_39.value} # type: ignore
|
||||
|
||||
@cached_property
|
||||
def _is_py_311_or_later(self) -> bool: # pragma: no cover
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value, self.PY_38.value, self.PY_39.value, self.PY_310.value} # type: ignore
|
||||
|
||||
@property
|
||||
def has_literal_type(self) -> bool:
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value} # type: ignore
|
||||
return self._is_py_38_or_later
|
||||
|
||||
@property
|
||||
def has_union_operator(self) -> bool: # pragma: no cover
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value, self.PY_38.value, self.PY_39.value} # type: ignore
|
||||
return self._is_py_310_or_later
|
||||
|
||||
@property
|
||||
def has_annotated_type(self) -> bool:
|
||||
return self.value not in {self.PY_36.value, self.PY_37.value, self.PY_38.value} # type: ignore
|
||||
return self._is_py_39_or_later
|
||||
|
||||
@property
|
||||
def has_typed_dict(self) -> bool:
|
||||
return self._is_py_38_or_later
|
||||
|
||||
@property
|
||||
def has_typed_dict_non_required(self) -> bool:
|
||||
return self._is_py_311_or_later
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -6,7 +6,7 @@ from ..types import DataTypeManager as DataTypeManagerABC
|
||||
from .base import ConstraintsBase, DataModel, DataModelFieldBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import DataModelType
|
||||
from .. import DataModelType, PythonVersion
|
||||
|
||||
|
||||
class DataModelSet(NamedTuple):
|
||||
@@ -17,9 +17,11 @@ class DataModelSet(NamedTuple):
|
||||
dump_resolve_reference_action: Optional[Callable[[Iterable[str]], str]]
|
||||
|
||||
|
||||
def get_data_model_types(data_model_type: DataModelType) -> DataModelSet:
|
||||
def get_data_model_types(
|
||||
data_model_type: DataModelType, target_python_version: PythonVersion
|
||||
) -> DataModelSet:
|
||||
from .. import DataModelType
|
||||
from . import dataclass, pydantic, rootmodel
|
||||
from . import dataclass, pydantic, rootmodel, typed_dict
|
||||
from .types import DataTypeManager
|
||||
|
||||
if data_model_type == DataModelType.PydanticBaseModel:
|
||||
@@ -38,6 +40,18 @@ def get_data_model_types(data_model_type: DataModelType) -> DataModelSet:
|
||||
data_type_manager=DataTypeManager,
|
||||
dump_resolve_reference_action=None,
|
||||
)
|
||||
elif data_model_type == DataModelType.TypingTypedDict:
|
||||
return DataModelSet(
|
||||
data_model=typed_dict.TypedDict
|
||||
if target_python_version.has_typed_dict
|
||||
else typed_dict.TypedDictBackport,
|
||||
root_model=rootmodel.RootModel,
|
||||
field_model=typed_dict.DataModelField
|
||||
if target_python_version.has_typed_dict_non_required
|
||||
else typed_dict.DataModelFieldBackport,
|
||||
data_type_manager=DataTypeManager,
|
||||
dump_resolve_reference_action=None,
|
||||
)
|
||||
raise ValueError(
|
||||
f'{data_model_type} is unsupported data model type'
|
||||
) # pragma: no cover
|
||||
|
||||
@@ -2,3 +2,7 @@ from datamodel_code_generator.imports import Import
|
||||
|
||||
IMPORT_DATACLASS = Import.from_full_path('dataclasses.dataclass')
|
||||
IMPORT_FIELD = Import.from_full_path('dataclasses.field')
|
||||
IMPORT_TYPED_DICT = Import.from_full_path('typing.TypedDict')
|
||||
IMPORT_TYPED_DICT_BACKPORT = Import.from_full_path('typing_extensions.TypedDict')
|
||||
IMPORT_NOT_REQUIRED = Import.from_full_path('typing.NotRequired')
|
||||
IMPORT_NOT_REQUIRED_BACKPORT = Import.from_full_path('typing_extensions.NotRequired')
|
||||
|
||||
5
datamodel_code_generator/model/template/TypedDict.jinja2
Normal file
5
datamodel_code_generator/model/template/TypedDict.jinja2
Normal file
@@ -0,0 +1,5 @@
|
||||
{%- if is_functional_syntax %}
|
||||
{% include 'TypedDictFunction.jinja2' %}
|
||||
{%- else %}
|
||||
{% include 'TypedDictClass.jinja2' %}
|
||||
{%- endif %}
|
||||
@@ -0,0 +1,17 @@
|
||||
class {{ class_name }}({{ base_class }}):
|
||||
{%- if description %}
|
||||
"""
|
||||
{{ description | indent(4) }}
|
||||
"""
|
||||
{%- endif %}
|
||||
{%- if not fields and not description %}
|
||||
pass
|
||||
{%- endif %}
|
||||
{%- for field in fields %}
|
||||
{{ field.name }}: {{ field.type_hint }}
|
||||
{%- if field.docstring %}
|
||||
"""
|
||||
{{ field.docstring | indent(4) }}
|
||||
"""
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
@@ -0,0 +1,16 @@
|
||||
{%- if description %}
|
||||
"""
|
||||
{{ description | indent(4) }}
|
||||
"""
|
||||
{%- endif %}
|
||||
{{ class_name }} = TypedDict('{{ class_name }}', {
|
||||
{%- for field in all_fields %}
|
||||
'{{ field.key }}': {{ field.type_hint }},
|
||||
{%- if field.docstring %}
|
||||
"""
|
||||
{{ field.docstring | indent(4) }}
|
||||
"""
|
||||
{%- endif %}
|
||||
{%- endfor -%}
|
||||
})
|
||||
|
||||
151
datamodel_code_generator/model/typed_dict.py
Normal file
151
datamodel_code_generator/model/typed_dict.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import keyword
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from datamodel_code_generator.imports import Import
|
||||
from datamodel_code_generator.model import DataModel, DataModelFieldBase
|
||||
from datamodel_code_generator.model.base import UNDEFINED
|
||||
from datamodel_code_generator.model.improts import (
|
||||
IMPORT_NOT_REQUIRED,
|
||||
IMPORT_NOT_REQUIRED_BACKPORT,
|
||||
IMPORT_TYPED_DICT,
|
||||
IMPORT_TYPED_DICT_BACKPORT,
|
||||
)
|
||||
from datamodel_code_generator.reference import Reference
|
||||
from datamodel_code_generator.types import NOT_REQUIRED_PREFIX
|
||||
|
||||
escape_characters = str.maketrans(
|
||||
{
|
||||
'\\': r'\\',
|
||||
"'": r"\'",
|
||||
'\b': r'\b',
|
||||
'\f': r'\f',
|
||||
'\n': r'\n',
|
||||
'\r': r'\r',
|
||||
'\t': r'\t',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_valid_field_name(field: DataModelFieldBase) -> bool:
|
||||
name = field.original_name or field.name
|
||||
if name is None: # pragma: no cover
|
||||
return False
|
||||
return name.isidentifier() and not keyword.iskeyword(name)
|
||||
|
||||
|
||||
class TypedDict(DataModel):
|
||||
TEMPLATE_FILE_PATH: ClassVar[str] = 'TypedDict.jinja2'
|
||||
BASE_CLASS: ClassVar[str] = 'typing.TypedDict'
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (IMPORT_TYPED_DICT,)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
reference: Reference,
|
||||
fields: List[DataModelFieldBase],
|
||||
decorators: Optional[List[str]] = None,
|
||||
base_classes: Optional[List[Reference]] = None,
|
||||
custom_base_class: Optional[str] = None,
|
||||
custom_template_dir: Optional[Path] = None,
|
||||
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
|
||||
methods: Optional[List[str]] = None,
|
||||
path: Optional[Path] = None,
|
||||
description: Optional[str] = None,
|
||||
default: Any = UNDEFINED,
|
||||
nullable: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
reference=reference,
|
||||
fields=fields,
|
||||
decorators=decorators,
|
||||
base_classes=base_classes,
|
||||
custom_base_class=custom_base_class,
|
||||
custom_template_dir=custom_template_dir,
|
||||
extra_template_data=extra_template_data,
|
||||
methods=methods,
|
||||
path=path,
|
||||
description=description,
|
||||
default=default,
|
||||
nullable=nullable,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_functional_syntax(self) -> bool:
|
||||
return any(not _is_valid_field_name(f) for f in self.fields)
|
||||
|
||||
@property
|
||||
def all_fields(self) -> Iterator[DataModelFieldBase]:
|
||||
for base_class in self.base_classes:
|
||||
if base_class.reference is None: # pragma: no cover
|
||||
continue
|
||||
data_model = base_class.reference.source
|
||||
if not isinstance(data_model, DataModel): # pragma: no cover
|
||||
continue
|
||||
|
||||
if isinstance(data_model, TypedDict): # pragma: no cover
|
||||
yield from data_model.all_fields
|
||||
|
||||
yield from self.fields
|
||||
|
||||
def render(self, *, class_name: Optional[str] = None) -> str:
|
||||
response = self._render(
|
||||
class_name=class_name or self.class_name,
|
||||
fields=self.fields,
|
||||
decorators=self.decorators,
|
||||
base_class=self.base_class,
|
||||
methods=self.methods,
|
||||
description=self.description,
|
||||
is_functional_syntax=self.is_functional_syntax,
|
||||
all_fields=self.all_fields,
|
||||
**self.extra_template_data,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class TypedDictBackport(TypedDict):
|
||||
BASE_CLASS: ClassVar[str] = 'typing_extensions.TypedDict'
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (IMPORT_TYPED_DICT_BACKPORT,)
|
||||
|
||||
|
||||
class DataModelField(DataModelFieldBase):
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (IMPORT_NOT_REQUIRED,)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return (self.original_name or self.name or '').translate( # pragma: no cover
|
||||
escape_characters
|
||||
)
|
||||
|
||||
@property
|
||||
def type_hint(self) -> str:
|
||||
type_hint = super().type_hint
|
||||
if self._not_required:
|
||||
return f'{NOT_REQUIRED_PREFIX}{type_hint}]'
|
||||
return type_hint
|
||||
|
||||
@property
|
||||
def _not_required(self) -> bool:
|
||||
return not self.required and isinstance(self.parent, TypedDict)
|
||||
|
||||
@property
|
||||
def imports(self) -> Tuple[Import, ...]:
|
||||
return (
|
||||
*super().imports,
|
||||
*(self.DEFAULT_IMPORTS if self._not_required else ()),
|
||||
)
|
||||
|
||||
|
||||
class DataModelFieldBackport(DataModelField):
|
||||
DEFAULT_IMPORTS: ClassVar[Tuple[Import, ...]] = (IMPORT_NOT_REQUIRED_BACKPORT,)
|
||||
@@ -68,6 +68,9 @@ STANDARD_DICT = 'dict'
|
||||
STANDARD_LIST = 'list'
|
||||
STR = 'str'
|
||||
|
||||
NOT_REQUIRED = 'NotRequired'
|
||||
NOT_REQUIRED_PREFIX = f'{NOT_REQUIRED}['
|
||||
|
||||
|
||||
class StrictTypes(Enum):
|
||||
str = 'str'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# datamodel-code-generator
|
||||
|
||||
This code generator creates [pydantic](https://docs.pydantic.dev/) model and [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html) from an openapi file and others.
|
||||
|
||||
This code generator creates [pydantic](https://docs.pydantic.dev/) model, [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html) and [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict) from an openapi file and others.
|
||||
[](https://github.com/koxudaxi/datamodel-code-generator/actions?query=workflow%3ATest)
|
||||
[](https://pypi.python.org/pypi/datamodel-code-generator)
|
||||
[](https://anaconda.org/conda-forge/datamodel-code-generator)
|
||||
@@ -257,6 +256,7 @@ These OSS projects use datamodel-code-generator to generate many models. See the
|
||||
## Supported output types
|
||||
- [pydantic](https://docs.pydantic.dev/).BaseModel
|
||||
- [dataclasses.dataclass](https://docs.python.org/3/library/dataclasses.html)
|
||||
- [typing.TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -293,7 +293,7 @@ usage: datamodel-codegen [-h] [--input INPUT] [--url URL]
|
||||
[--http-headers HTTP_HEADER [HTTP_HEADER ...]]
|
||||
[--http-ignore-tls]
|
||||
[--input-file-type {auto,openapi,jsonschema,json,yaml,dict,csv}]
|
||||
[--output-model-type {pydantic.BaseModel,dataclasses.dataclass}]
|
||||
[--output-model-type {pydantic.BaseModel,dataclasses.dataclass,typing.TypedDict}]
|
||||
[--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]]
|
||||
[--output OUTPUT] [--base-class BASE_CLASS]
|
||||
[--field-constraints] [--use-annotated]
|
||||
@@ -344,7 +344,7 @@ options:
|
||||
certificate
|
||||
--input-file-type {auto,openapi,jsonschema,json,yaml,dict,csv}
|
||||
Input file type (default: auto)
|
||||
--output-model-type {pydantic.BaseModel,dataclasses.dataclass}
|
||||
--output-model-type {pydantic.BaseModel,dataclasses.dataclass,typing.TypedDict}
|
||||
Output model type (default: pydantic.BaseModel)
|
||||
--openapi-scopes {schemas,paths,tags,parameters} [{schemas,paths,tags,parameters} ...]
|
||||
Scopes of OpenAPI model generation (default: schemas)
|
||||
|
||||
33
tests/data/expected/main/main_modular_typed_dict/__init__.py
Normal file
33
tests/data/expected/main/main_modular_typed_dict/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NotRequired, Optional, TypedDict
|
||||
|
||||
from . import foo, models
|
||||
from .nested import foo as foo_1
|
||||
|
||||
OptionalModel = str
|
||||
|
||||
|
||||
Id = str
|
||||
|
||||
|
||||
class Error(TypedDict):
|
||||
code: int
|
||||
message: str
|
||||
|
||||
|
||||
class Result(TypedDict):
|
||||
event: NotRequired[Optional[models.Event]]
|
||||
|
||||
|
||||
class Source(TypedDict):
|
||||
country: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class DifferentTea(TypedDict):
|
||||
foo: NotRequired[Optional[foo.Tea]]
|
||||
nested: NotRequired[Optional[foo_1.Tea]]
|
||||
7
tests/data/expected/main/main_modular_typed_dict/bar.py
Normal file
7
tests/data/expected/main/main_modular_typed_dict/bar.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
Field = str
|
||||
@@ -0,0 +1,28 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, NotRequired, Optional, TypedDict
|
||||
|
||||
from . import models
|
||||
|
||||
Pets = List[models.Pet]
|
||||
|
||||
|
||||
Users = List[models.User]
|
||||
|
||||
|
||||
Rules = List[str]
|
||||
|
||||
|
||||
class Api(TypedDict):
|
||||
apiKey: NotRequired[Optional[str]]
|
||||
apiVersionNumber: NotRequired[Optional[str]]
|
||||
apiUrl: NotRequired[Optional[str]]
|
||||
apiDocumentationUrl: NotRequired[Optional[str]]
|
||||
stage: NotRequired[Optional[Literal['test', 'dev', 'stg', 'prod']]]
|
||||
|
||||
|
||||
Apis = List[Api]
|
||||
@@ -0,0 +1,18 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NotRequired, Optional, TypedDict
|
||||
|
||||
from .. import Id
|
||||
|
||||
|
||||
class Tea(TypedDict):
|
||||
flavour: NotRequired[Optional[str]]
|
||||
id: NotRequired[Optional[Id]]
|
||||
|
||||
|
||||
class Cocoa(TypedDict):
|
||||
quality: NotRequired[Optional[int]]
|
||||
23
tests/data/expected/main/main_modular_typed_dict/foo/bar.py
Normal file
23
tests/data/expected/main/main_modular_typed_dict/foo/bar.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, NotRequired, Optional, TypedDict
|
||||
|
||||
|
||||
class Thing(TypedDict):
|
||||
attributes: NotRequired[Optional[Dict[str, Any]]]
|
||||
|
||||
|
||||
class Thang(TypedDict):
|
||||
attributes: NotRequired[Optional[List[Dict[str, Any]]]]
|
||||
|
||||
|
||||
class Others(TypedDict):
|
||||
name: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class Clone(Thing):
|
||||
others: NotRequired[Optional[Others]]
|
||||
26
tests/data/expected/main/main_modular_typed_dict/models.py
Normal file
26
tests/data/expected/main/main_modular_typed_dict/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Literal, NotRequired, Optional, TypedDict, Union
|
||||
|
||||
Species = Literal['dog', 'cat', 'snake']
|
||||
|
||||
|
||||
class Pet(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
species: NotRequired[Optional[Species]]
|
||||
|
||||
|
||||
class User(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class Event(TypedDict):
|
||||
name: NotRequired[Optional[Union[str, float, int, bool, Dict[str, Any], List[str]]]]
|
||||
@@ -0,0 +1,3 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
@@ -0,0 +1,26 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, NotRequired, Optional, TypedDict
|
||||
|
||||
from .. import Id, OptionalModel
|
||||
|
||||
|
||||
class Tea(TypedDict):
|
||||
flavour: NotRequired[Optional[str]]
|
||||
id: NotRequired[Optional[Id]]
|
||||
self: NotRequired[Optional[Tea]]
|
||||
optional: NotRequired[Optional[List[OptionalModel]]]
|
||||
|
||||
|
||||
class TeaClone(TypedDict):
|
||||
flavour: NotRequired[Optional[str]]
|
||||
id: NotRequired[Optional[Id]]
|
||||
self: NotRequired[Optional[Tea]]
|
||||
optional: NotRequired[Optional[List[OptionalModel]]]
|
||||
|
||||
|
||||
ListModel = List[Tea]
|
||||
@@ -0,0 +1,3 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
16
tests/data/expected/main/main_modular_typed_dict/woo/boo.py
Normal file
16
tests/data/expected/main/main_modular_typed_dict/woo/boo.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: modular.yaml
|
||||
# timestamp: 1985-10-26T08:21:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NotRequired, Optional, TypedDict
|
||||
|
||||
from .. import Source, bar, foo
|
||||
|
||||
|
||||
class Chocolate(TypedDict):
|
||||
flavour: NotRequired[Optional[str]]
|
||||
source: NotRequired[Optional[Source]]
|
||||
cocoa: NotRequired[Optional[foo.Cocoa]]
|
||||
field: NotRequired[Optional[bar.Field]]
|
||||
56
tests/data/expected/main/main_typed_dict/output.py
Normal file
56
tests/data/expected/main/main_typed_dict/output.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: api.yaml
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
||||
class Pet(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Pets = List[Pet]
|
||||
|
||||
|
||||
class User(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Users = List[User]
|
||||
|
||||
|
||||
Id = str
|
||||
|
||||
|
||||
Rules = List[str]
|
||||
|
||||
|
||||
class Error(TypedDict):
|
||||
code: int
|
||||
message: str
|
||||
|
||||
|
||||
class Api(TypedDict):
|
||||
apiKey: NotRequired[Optional[str]]
|
||||
apiVersionNumber: NotRequired[Optional[str]]
|
||||
apiUrl: NotRequired[Optional[str]]
|
||||
apiDocumentationUrl: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Apis = List[Api]
|
||||
|
||||
|
||||
class Event(TypedDict):
|
||||
name: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class Result(TypedDict):
|
||||
event: NotRequired[Optional[Event]]
|
||||
62
tests/data/expected/main/main_typed_dict_nullable/output.py
Normal file
62
tests/data/expected/main/main_typed_dict_nullable/output.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: nullable.yaml
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, NotRequired, Optional, TypedDict
|
||||
|
||||
|
||||
class Cursors(TypedDict):
|
||||
prev: str
|
||||
next: NotRequired[Optional[str]]
|
||||
index: float
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class TopLevel(TypedDict):
|
||||
cursors: Cursors
|
||||
|
||||
|
||||
class Info(TypedDict):
|
||||
name: str
|
||||
|
||||
|
||||
class User(TypedDict):
|
||||
info: Info
|
||||
|
||||
|
||||
class Api(TypedDict):
|
||||
apiKey: NotRequired[Optional[str]]
|
||||
apiVersionNumber: NotRequired[Optional[str]]
|
||||
apiUrl: NotRequired[Optional[str]]
|
||||
apiDocumentationUrl: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Apis = Optional[List[Api]]
|
||||
|
||||
|
||||
class EmailItem(TypedDict):
|
||||
author: str
|
||||
address: str
|
||||
description: NotRequired[Optional[str]]
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Email = List[EmailItem]
|
||||
|
||||
|
||||
Id = int
|
||||
|
||||
|
||||
Description = Optional[str]
|
||||
|
||||
|
||||
Name = Optional[str]
|
||||
|
||||
|
||||
Tag = str
|
||||
|
||||
|
||||
class Notes(TypedDict):
|
||||
comments: NotRequired[List[str]]
|
||||
@@ -0,0 +1,62 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: nullable.yaml
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, NotRequired, Optional, TypedDict
|
||||
|
||||
|
||||
class Cursors(TypedDict):
|
||||
prev: Optional[str]
|
||||
next: NotRequired[str]
|
||||
index: float
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class TopLevel(TypedDict):
|
||||
cursors: Cursors
|
||||
|
||||
|
||||
class Info(TypedDict):
|
||||
name: str
|
||||
|
||||
|
||||
class User(TypedDict):
|
||||
info: Info
|
||||
|
||||
|
||||
class Api(TypedDict):
|
||||
apiKey: NotRequired[Optional[str]]
|
||||
apiVersionNumber: NotRequired[Optional[str]]
|
||||
apiUrl: NotRequired[Optional[str]]
|
||||
apiDocumentationUrl: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Apis = Optional[List[Api]]
|
||||
|
||||
|
||||
class EmailItem(TypedDict):
|
||||
author: str
|
||||
address: str
|
||||
description: NotRequired[str]
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Email = List[EmailItem]
|
||||
|
||||
|
||||
Id = int
|
||||
|
||||
|
||||
Description = Optional[str]
|
||||
|
||||
|
||||
Name = Optional[str]
|
||||
|
||||
|
||||
Tag = str
|
||||
|
||||
|
||||
class Notes(TypedDict):
|
||||
comments: NotRequired[List[str]]
|
||||
56
tests/data/expected/main/main_typed_dict_py_38/output.py
Normal file
56
tests/data/expected/main/main_typed_dict_py_38/output.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: api.yaml
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
|
||||
class Pet(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Pets = List[Pet]
|
||||
|
||||
|
||||
class User(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
tag: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Users = List[User]
|
||||
|
||||
|
||||
Id = str
|
||||
|
||||
|
||||
Rules = List[str]
|
||||
|
||||
|
||||
class Error(TypedDict):
|
||||
code: int
|
||||
message: str
|
||||
|
||||
|
||||
class Api(TypedDict):
|
||||
apiKey: NotRequired[Optional[str]]
|
||||
apiVersionNumber: NotRequired[Optional[str]]
|
||||
apiUrl: NotRequired[Optional[str]]
|
||||
apiDocumentationUrl: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
Apis = List[Api]
|
||||
|
||||
|
||||
class Event(TypedDict):
|
||||
name: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class Result(TypedDict):
|
||||
event: NotRequired[Optional[Event]]
|
||||
@@ -0,0 +1,58 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: space_and_special_characters.json
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class InitialParameters(TypedDict):
|
||||
V1: int
|
||||
V2: int
|
||||
|
||||
|
||||
Data = TypedDict(
|
||||
'Data',
|
||||
{
|
||||
'Length (m)': float,
|
||||
'Symmetric deviation (%)': float,
|
||||
'Total running time (s)': int,
|
||||
'Mass (kg)': float,
|
||||
'Initial parameters': InitialParameters,
|
||||
'class': str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Values = TypedDict(
|
||||
'Values',
|
||||
{
|
||||
'1 Step': str,
|
||||
'2 Step': str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Recursive1(TypedDict):
|
||||
value: float
|
||||
|
||||
|
||||
class Sub(TypedDict):
|
||||
recursive: Recursive1
|
||||
|
||||
|
||||
class Recursive(TypedDict):
|
||||
sub: Sub
|
||||
|
||||
|
||||
Model = TypedDict(
|
||||
'Model',
|
||||
{
|
||||
'Serial Number': str,
|
||||
'Timestamp': str,
|
||||
'Data': Data,
|
||||
'values': Values,
|
||||
'recursive': Recursive,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: special_field_name_with_inheritance_model.json
|
||||
# timestamp: 2019-07-26T00:00:00+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NotRequired, Optional, TypedDict
|
||||
|
||||
|
||||
class NestedBase(TypedDict):
|
||||
age: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
class Base(NestedBase):
|
||||
name: NotRequired[Optional[str]]
|
||||
|
||||
|
||||
SpecialField = TypedDict(
|
||||
'SpecialField',
|
||||
{
|
||||
'age': NotRequired[Optional[str]],
|
||||
'name': NotRequired[Optional[str]],
|
||||
'global': NotRequired[Optional[str]],
|
||||
'with': NotRequired[Optional[str]],
|
||||
'class': NotRequired[Optional[int]],
|
||||
'class\'s': NotRequired[Optional[int]],
|
||||
'class-s': NotRequired[Optional[str]],
|
||||
'#': NotRequired[Optional[str]],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SpecialField",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"global": {
|
||||
"type": "string"
|
||||
},
|
||||
"with": {
|
||||
"type": "string"
|
||||
},
|
||||
"class": {
|
||||
"type": "integer"
|
||||
},
|
||||
"class's": {
|
||||
"type": "integer"
|
||||
},
|
||||
"class-s": {
|
||||
"type": "string"
|
||||
},
|
||||
"#": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/base"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
"base": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/nestedBase"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nestedBase": {
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5376,3 +5376,222 @@ def test_main_null():
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(OPEN_API_DATA_PATH / 'api.yaml'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (EXPECTED_MAIN_PATH / 'main_typed_dict' / 'output.py').read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict_py_38():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(OPEN_API_DATA_PATH / 'api.yaml'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.8',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (EXPECTED_MAIN_PATH / 'main_typed_dict_py_38' / 'output.py').read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(black.__version__) < version.parse('23.3.0'),
|
||||
reason='Require Black version 23.3.0 or later ',
|
||||
)
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict_space_and_special_characters():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(JSON_DATA_PATH / 'space_and_special_characters.json'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.11',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH
|
||||
/ 'main_typed_dict_space_and_special_characters'
|
||||
/ 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(black.__version__) < version.parse('23.3.0'),
|
||||
reason='Require Black version 23.3.0 or later ',
|
||||
)
|
||||
def test_main_modular_typed_dict(tmpdir_factory: TempdirFactory) -> None:
|
||||
"""Test main function on modular file."""
|
||||
|
||||
output_directory = Path(tmpdir_factory.mktemp('output'))
|
||||
|
||||
input_filename = OPEN_API_DATA_PATH / 'modular.yaml'
|
||||
output_path = output_directory / 'model'
|
||||
|
||||
with freeze_time(TIMESTAMP):
|
||||
main(
|
||||
[
|
||||
'--input',
|
||||
str(input_filename),
|
||||
'--output',
|
||||
str(output_path),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.11',
|
||||
]
|
||||
)
|
||||
main_modular_dir = EXPECTED_MAIN_PATH / 'main_modular_typed_dict'
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(black.__version__) < version.parse('23.3.0'),
|
||||
reason='Require Black version 23.3.0 or later ',
|
||||
)
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict_special_field_name_with_inheritance_model():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(
|
||||
JSON_SCHEMA_DATA_PATH
|
||||
/ 'special_field_name_with_inheritance_model.json'
|
||||
),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.11',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH
|
||||
/ 'main_typed_dict_special_field_name_with_inheritance_model'
|
||||
/ 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(black.__version__) < version.parse('23.3.0'),
|
||||
reason='Require Black version 23.3.0 or later ',
|
||||
)
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict_nullable():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(OPEN_API_DATA_PATH / 'nullable.yaml'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.11',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH / 'main_typed_dict_nullable' / 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
version.parse(black.__version__) < version.parse('23.3.0'),
|
||||
reason='Require Black version 23.3.0 or later ',
|
||||
)
|
||||
@freeze_time('2019-07-26')
|
||||
def test_main_typed_dict_nullable_strict_nullable():
|
||||
with TemporaryDirectory() as output_dir:
|
||||
output_file: Path = Path(output_dir) / 'output.py'
|
||||
return_code: Exit = main(
|
||||
[
|
||||
'--input',
|
||||
str(OPEN_API_DATA_PATH / 'nullable.yaml'),
|
||||
'--output',
|
||||
str(output_file),
|
||||
'--output-model-type',
|
||||
'typing.TypedDict',
|
||||
'--target-python-version',
|
||||
'3.11',
|
||||
'--strict-nullable',
|
||||
]
|
||||
)
|
||||
assert return_code == Exit.OK
|
||||
assert (
|
||||
output_file.read_text()
|
||||
== (
|
||||
EXPECTED_MAIN_PATH
|
||||
/ 'main_typed_dict_nullable_strict_nullable'
|
||||
/ 'output.py'
|
||||
).read_text()
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user