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:
Koudai Aono
2023-06-01 00:24:23 +09:00
committed by GitHub
parent 66be00317f
commit 35b892ce75
29 changed files with 1032 additions and 15 deletions

View File

@@ -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.
[![Build Status](https://github.com/koxudaxi/datamodel-code-generator/workflows/Test/badge.svg)](https://github.com/koxudaxi/datamodel-code-generator/actions?query=workflow%3ATest)
[![PyPI version](https://badge.fury.io/py/datamodel-code-generator.svg)](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)

View File

@@ -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,

View File

@@ -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:

View File

@@ -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

View File

@@ -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')

View File

@@ -0,0 +1,5 @@
{%- if is_functional_syntax %}
{% include 'TypedDictFunction.jinja2' %}
{%- else %}
{% include 'TypedDictClass.jinja2' %}
{%- endif %}

View File

@@ -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 -%}

View File

@@ -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 -%}
})

View 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,)

View File

@@ -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'

View File

@@ -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.
[![Build Status](https://github.com/koxudaxi/datamodel-code-generator/workflows/Test/badge.svg)](https://github.com/koxudaxi/datamodel-code-generator/actions?query=workflow%3ATest)
[![PyPI version](https://badge.fury.io/py/datamodel-code-generator.svg)](https://pypi.python.org/pypi/datamodel-code-generator)
[![Conda-forge](https://img.shields.io/conda/v/conda-forge/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)

View 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]]

View 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

View File

@@ -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]

View File

@@ -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]]

View 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]]

View 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]]]]

View File

@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: modular.yaml
# timestamp: 1985-10-26T08:21:00+00:00

View 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 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]

View File

@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: modular.yaml
# timestamp: 1985-10-26T08:21:00+00:00

View 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]]

View 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]]

View 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]]

View 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: 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]]

View 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]]

View File

@@ -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,
},
)

View File

@@ -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]],
},
)

View File

@@ -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"
}
}
}
}
}

View File

@@ -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()