mirror of
https://github.com/koxudaxi/datamodel-code-generator.git
synced 2024-03-18 14:54:37 +03:00
Support options on pyproject.toml (#190)
* support for pyproject.toml * add unittest * remove comments * fix format * update documents
This commit is contained in:
@@ -10,10 +10,14 @@ import sys
|
|||||||
from argparse import ArgumentParser, FileType, Namespace
|
from argparse import ArgumentParser, FileType, Namespace
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
from io import TextIOBase
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, DefaultDict, Dict, Optional, Sequence
|
from typing import Any, DefaultDict, Dict, Mapping, Optional, Sequence, TextIO
|
||||||
|
|
||||||
import argcomplete
|
import argcomplete
|
||||||
|
import black
|
||||||
|
import toml
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from datamodel_code_generator import (
|
from datamodel_code_generator import (
|
||||||
DEFAULT_BASE_CLASS,
|
DEFAULT_BASE_CLASS,
|
||||||
@@ -43,23 +47,16 @@ signal.signal(signal.SIGINT, sig_int_handler)
|
|||||||
|
|
||||||
arg_parser = ArgumentParser()
|
arg_parser = ArgumentParser()
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'--input',
|
'--input', help='Input file (default: stdin)', type=FileType('rt'),
|
||||||
help='Input file (default: stdin)',
|
|
||||||
type=FileType('rt'),
|
|
||||||
default=sys.stdin,
|
|
||||||
)
|
)
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'--input-file-type',
|
'--input-file-type',
|
||||||
help='Input file type (default: auto)',
|
help='Input file type (default: auto)',
|
||||||
choices=[i.value for i in InputFileType],
|
choices=[i.value for i in InputFileType],
|
||||||
default='auto',
|
|
||||||
)
|
)
|
||||||
arg_parser.add_argument('--output', help='Output file (default: stdout)')
|
arg_parser.add_argument('--output', help='Output file (default: stdout)')
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'--base-class',
|
'--base-class', help='Base Class (default: pydantic.BaseModel)', type=str,
|
||||||
help='Base Class (default: pydantic.BaseModel)',
|
|
||||||
type=str,
|
|
||||||
default=DEFAULT_BASE_CLASS,
|
|
||||||
)
|
)
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'--field-constraints',
|
'--field-constraints',
|
||||||
@@ -85,7 +82,6 @@ arg_parser.add_argument(
|
|||||||
'--target-python-version',
|
'--target-python-version',
|
||||||
help='target python version (default: 3.7)',
|
help='target python version (default: 3.7)',
|
||||||
choices=['3.6', '3.7'],
|
choices=['3.6', '3.7'],
|
||||||
default='3.7',
|
|
||||||
)
|
)
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
'--validation', help='Enable validation (Only OpenAPI)', action='store_true'
|
'--validation', help='Enable validation (Only OpenAPI)', action='store_true'
|
||||||
@@ -94,6 +90,32 @@ arg_parser.add_argument('--debug', help='show debug message', action='store_true
|
|||||||
arg_parser.add_argument('--version', help='show version', action='store_true')
|
arg_parser.add_argument('--version', help='show version', action='store_true')
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
class Config:
|
||||||
|
validate_assignment = True
|
||||||
|
arbitrary_types_allowed = (TextIOBase,)
|
||||||
|
|
||||||
|
input_file_type: InputFileType = InputFileType.Auto
|
||||||
|
output: Optional[Path]
|
||||||
|
debug: bool = False
|
||||||
|
target_python_version: PythonVersion = PythonVersion.PY_37
|
||||||
|
base_class: str = DEFAULT_BASE_CLASS
|
||||||
|
custom_template_dir: Optional[str]
|
||||||
|
extra_template_data: Optional[TextIOBase]
|
||||||
|
validation: bool = False
|
||||||
|
field_constraints: bool = False
|
||||||
|
snake_case_field: bool = False
|
||||||
|
strip_default_none: bool = False
|
||||||
|
aliases: Optional[TextIOBase]
|
||||||
|
|
||||||
|
def merge_args(self, args: Namespace) -> None:
|
||||||
|
for field_name in self.__fields__:
|
||||||
|
arg = getattr(args, field_name)
|
||||||
|
if arg is None:
|
||||||
|
continue
|
||||||
|
setattr(self, field_name, arg)
|
||||||
|
|
||||||
|
|
||||||
def main(args: Optional[Sequence[str]] = None) -> Exit:
|
def main(args: Optional[Sequence[str]] = None) -> Exit:
|
||||||
"""Main function."""
|
"""Main function."""
|
||||||
|
|
||||||
@@ -111,12 +133,39 @@ def main(args: Optional[Sequence[str]] = None) -> Exit:
|
|||||||
print(version)
|
print(version)
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
if namespace.debug: # pragma: no cover
|
root = black.find_project_root((Path().resolve(),))
|
||||||
|
pyproject_toml_path = root / "pyproject.toml"
|
||||||
|
if pyproject_toml_path.is_file():
|
||||||
|
pyproject_toml: Dict[str, Any] = {
|
||||||
|
k.replace('-', '_'): v
|
||||||
|
for k, v in toml.load(str(pyproject_toml_path))
|
||||||
|
.get('tool', {})
|
||||||
|
.get('datamodel-codegen', {})
|
||||||
|
.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
pyproject_toml = {}
|
||||||
|
config = Config.parse_obj(pyproject_toml)
|
||||||
|
|
||||||
|
config.merge_args(namespace)
|
||||||
|
|
||||||
|
if namespace.input:
|
||||||
|
input_name: str = namespace.input.name
|
||||||
|
input_text: str = namespace.input.read()
|
||||||
|
elif 'input' in pyproject_toml:
|
||||||
|
input_path = Path(pyproject_toml['input'])
|
||||||
|
input_name = input_path.name
|
||||||
|
input_text = input_path.read_text()
|
||||||
|
else:
|
||||||
|
input_name = '<stdin>'
|
||||||
|
input_text = sys.stdin.read()
|
||||||
|
|
||||||
|
if config.debug: # pragma: no cover
|
||||||
enable_debug_message()
|
enable_debug_message()
|
||||||
|
|
||||||
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]]
|
extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]]
|
||||||
if namespace.extra_template_data is not None:
|
if config.extra_template_data is not None:
|
||||||
with namespace.extra_template_data as data:
|
with config.extra_template_data as data:
|
||||||
try:
|
try:
|
||||||
extra_template_data = json.load(
|
extra_template_data = json.load(
|
||||||
data, object_hook=lambda d: defaultdict(dict, **d)
|
data, object_hook=lambda d: defaultdict(dict, **d)
|
||||||
@@ -127,8 +176,8 @@ def main(args: Optional[Sequence[str]] = None) -> Exit:
|
|||||||
else:
|
else:
|
||||||
extra_template_data = None
|
extra_template_data = None
|
||||||
|
|
||||||
if namespace.aliases is not None:
|
if config.aliases is not None:
|
||||||
with namespace.aliases as data:
|
with config.aliases as data:
|
||||||
try:
|
try:
|
||||||
aliases = json.load(data)
|
aliases = json.load(data)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
@@ -147,18 +196,18 @@ def main(args: Optional[Sequence[str]] = None) -> Exit:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
generate(
|
generate(
|
||||||
input_name=namespace.input.name,
|
input_name=input_name,
|
||||||
input_text=namespace.input.read(),
|
input_text=input_text,
|
||||||
input_file_type=InputFileType(namespace.input_file_type),
|
input_file_type=config.input_file_type,
|
||||||
output=Path(namespace.output) if namespace.output is not None else None,
|
output=Path(config.output) if config.output is not None else None,
|
||||||
target_python_version=PythonVersion(namespace.target_python_version),
|
target_python_version=config.target_python_version,
|
||||||
base_class=namespace.base_class,
|
base_class=config.base_class,
|
||||||
custom_template_dir=namespace.custom_template_dir,
|
custom_template_dir=config.custom_template_dir,
|
||||||
extra_template_data=extra_template_data,
|
extra_template_data=extra_template_data,
|
||||||
validation=namespace.validation,
|
validation=config.validation,
|
||||||
field_constraints=namespace.field_constraints,
|
field_constraints=config.field_constraints,
|
||||||
snake_case_field=namespace.snake_case_field,
|
snake_case_field=config.snake_case_field,
|
||||||
strip_default_none=namespace.strip_default_none,
|
strip_default_none=config.strip_default_none,
|
||||||
aliases=aliases,
|
aliases=aliases,
|
||||||
)
|
)
|
||||||
return Exit.OK
|
return Exit.OK
|
||||||
|
|||||||
12
docs/pyproject_toml.md
Normal file
12
docs/pyproject_toml.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
datamodel-code-generator has a lot of command-line options.
|
||||||
|
|
||||||
|
The options are supported on `pyproject.toml`.
|
||||||
|
|
||||||
|
Example `pyproject.toml`:
|
||||||
|
```toml
|
||||||
|
[tool.datamodel-codegen]
|
||||||
|
field-constraints = true
|
||||||
|
snake-case-field = true
|
||||||
|
strip-default-none = false
|
||||||
|
target-python-version = "3.7"
|
||||||
|
```
|
||||||
@@ -27,6 +27,7 @@ nav:
|
|||||||
- Generate from JSON Data: jsondata.md
|
- Generate from JSON Data: jsondata.md
|
||||||
- Formatting: formatting.md
|
- Formatting: formatting.md
|
||||||
- Field Constraints: field-constraints.md
|
- Field Constraints: field-constraints.md
|
||||||
|
- pyproject.toml: pyproject_toml.md
|
||||||
- Development-Contributing: development-contributing.md
|
- Development-Contributing: development-contributing.md
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
# filename: api.yaml
|
# filename: api.yaml
|
||||||
# timestamp: 2019-07-26T00:00:00+00:00
|
# timestamp: 2019-07-26T00:00:00+00:00
|
||||||
|
|
||||||
from __future__ import (
|
|
||||||
annotations,
|
|
||||||
)
|
|
||||||
|
|
||||||
from typing import (
|
from typing import (
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -25,7 +21,7 @@ class Pet(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Pets(BaseModel):
|
class Pets(BaseModel):
|
||||||
__root__: List[Pet]
|
__root__: List["Pet"]
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
@@ -35,7 +31,7 @@ class User(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Users(BaseModel):
|
class Users(BaseModel):
|
||||||
__root__: List[User]
|
__root__: List["User"]
|
||||||
|
|
||||||
|
|
||||||
class Id(BaseModel):
|
class Id(BaseModel):
|
||||||
@@ -79,7 +75,7 @@ class Api(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Apis(BaseModel):
|
class Apis(BaseModel):
|
||||||
__root__: List[Api]
|
__root__: List["Api"]
|
||||||
|
|
||||||
|
|
||||||
class Event(BaseModel):
|
class Event(BaseModel):
|
||||||
@@ -88,5 +84,5 @@ class Event(BaseModel):
|
|||||||
|
|
||||||
class Result(BaseModel):
|
class Result(BaseModel):
|
||||||
event: Optional[
|
event: Optional[
|
||||||
Event
|
"Event"
|
||||||
] = None
|
] = None
|
||||||
|
|||||||
69
tests/data/expected/main/pyproject_not_found/output.py
Normal file
69
tests/data/expected/main/pyproject_not_found/output.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 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 pydantic import AnyUrl, BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Pet(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tag: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Pets(BaseModel):
|
||||||
|
__root__: List[Pet]
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tag: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Users(BaseModel):
|
||||||
|
__root__: List[User]
|
||||||
|
|
||||||
|
|
||||||
|
class Id(BaseModel):
|
||||||
|
__root__: str
|
||||||
|
|
||||||
|
|
||||||
|
class Rules(BaseModel):
|
||||||
|
__root__: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Error(BaseModel):
|
||||||
|
code: int
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class Api(BaseModel):
|
||||||
|
apiKey: Optional[str] = Field(
|
||||||
|
None, description='To be used as a dataset parameter value'
|
||||||
|
)
|
||||||
|
apiVersionNumber: Optional[str] = Field(
|
||||||
|
None, description='To be used as a version parameter value'
|
||||||
|
)
|
||||||
|
apiUrl: Optional[AnyUrl] = Field(
|
||||||
|
None, description="The URL describing the dataset's fields"
|
||||||
|
)
|
||||||
|
apiDocumentationUrl: Optional[AnyUrl] = Field(
|
||||||
|
None, description='A URL to the API console for each API'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Apis(BaseModel):
|
||||||
|
__root__: List[Api]
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Result(BaseModel):
|
||||||
|
event: Optional[Event] = None
|
||||||
69
tests/data/expected/main/stdin/output.py
Normal file
69
tests/data/expected/main/stdin/output.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: <stdin>
|
||||||
|
# timestamp: 2019-07-26T00:00:00+00:00
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import AnyUrl, BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class Pet(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tag: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Pets(BaseModel):
|
||||||
|
__root__: List[Pet]
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
tag: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Users(BaseModel):
|
||||||
|
__root__: List[User]
|
||||||
|
|
||||||
|
|
||||||
|
class Id(BaseModel):
|
||||||
|
__root__: str
|
||||||
|
|
||||||
|
|
||||||
|
class Rules(BaseModel):
|
||||||
|
__root__: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class Error(BaseModel):
|
||||||
|
code: int
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class Api(BaseModel):
|
||||||
|
apiKey: Optional[str] = Field(
|
||||||
|
None, description='To be used as a dataset parameter value'
|
||||||
|
)
|
||||||
|
apiVersionNumber: Optional[str] = Field(
|
||||||
|
None, description='To be used as a version parameter value'
|
||||||
|
)
|
||||||
|
apiUrl: Optional[AnyUrl] = Field(
|
||||||
|
None, description="The URL describing the dataset's fields"
|
||||||
|
)
|
||||||
|
apiDocumentationUrl: Optional[AnyUrl] = Field(
|
||||||
|
None, description='A URL to the API console for each API'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Apis(BaseModel):
|
||||||
|
__root__: List[Api]
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Result(BaseModel):
|
||||||
|
event: Optional[Event] = None
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
[tool.black]
|
[tool.black]
|
||||||
skip-string-normalization = false
|
skip-string-normalization = false
|
||||||
line-length = 30
|
line-length = 30
|
||||||
|
|
||||||
|
[tool.datamodel-codegen]
|
||||||
|
input = "INPUT_PATH"
|
||||||
|
output = "OUTPUT_PATH"
|
||||||
|
input_file_type = 'openapi'
|
||||||
|
validation = true
|
||||||
|
field-constraints = true
|
||||||
|
snake-case-field = true
|
||||||
|
strip-default-none = true
|
||||||
|
target-python-version = "3.6"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@@ -385,11 +386,37 @@ def test_main_custom_template_dir(capsys: CaptureFixture) -> None:
|
|||||||
|
|
||||||
@freeze_time('2019-07-26')
|
@freeze_time('2019-07-26')
|
||||||
def test_pyproject():
|
def test_pyproject():
|
||||||
|
current_dir = os.getcwd()
|
||||||
with TemporaryDirectory() as output_dir:
|
with TemporaryDirectory() as output_dir:
|
||||||
output_dir = Path(output_dir)
|
output_dir = Path(output_dir)
|
||||||
pyproject_toml = Path(DATA_PATH) / "project" / "pyproject.toml"
|
|
||||||
shutil.copy(pyproject_toml, output_dir)
|
|
||||||
output_file: Path = output_dir / 'output.py'
|
output_file: Path = output_dir / 'output.py'
|
||||||
|
pyproject_toml_path = Path(DATA_PATH) / "project" / "pyproject.toml"
|
||||||
|
pyproject_toml = (
|
||||||
|
pyproject_toml_path.read_text()
|
||||||
|
.replace('INPUT_PATH', str(OPEN_API_DATA_PATH / 'api.yaml'))
|
||||||
|
.replace('OUTPUT_PATH', str(output_file))
|
||||||
|
)
|
||||||
|
(output_dir / 'pyproject.toml').write_text(pyproject_toml)
|
||||||
|
|
||||||
|
os.chdir(output_dir)
|
||||||
|
return_code: Exit = main([])
|
||||||
|
assert return_code == Exit.OK
|
||||||
|
assert (
|
||||||
|
output_file.read_text()
|
||||||
|
== (EXPECTED_MAIN_PATH / 'pyproject' / 'output.py').read_text()
|
||||||
|
)
|
||||||
|
os.chdir(current_dir)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time('2019-07-26')
|
||||||
|
def test_pyproject_not_found():
|
||||||
|
current_dir = os.getcwd()
|
||||||
|
with TemporaryDirectory() as output_dir:
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_file: Path = output_dir / 'output.py'
|
||||||
|
os.chdir(output_dir)
|
||||||
return_code: Exit = main(
|
return_code: Exit = main(
|
||||||
[
|
[
|
||||||
'--input',
|
'--input',
|
||||||
@@ -401,11 +428,25 @@ def test_pyproject():
|
|||||||
assert return_code == Exit.OK
|
assert return_code == Exit.OK
|
||||||
assert (
|
assert (
|
||||||
output_file.read_text()
|
output_file.read_text()
|
||||||
== (EXPECTED_MAIN_PATH / 'pyproject' / 'output.py').read_text()
|
== (EXPECTED_MAIN_PATH / 'pyproject_not_found' / 'output.py').read_text()
|
||||||
)
|
)
|
||||||
|
os.chdir(current_dir)
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
|
||||||
main()
|
@freeze_time('2019-07-26')
|
||||||
|
def test_stdin(monkeypatch):
|
||||||
|
with TemporaryDirectory() as output_dir:
|
||||||
|
output_dir = Path(output_dir)
|
||||||
|
output_file: Path = output_dir / 'output.py'
|
||||||
|
monkeypatch.setattr('sys.stdin', (OPEN_API_DATA_PATH / 'api.yaml').open())
|
||||||
|
return_code: Exit = main(
|
||||||
|
['--output', str(output_file),]
|
||||||
|
)
|
||||||
|
assert return_code == Exit.OK
|
||||||
|
assert (
|
||||||
|
output_file.read_text()
|
||||||
|
== (EXPECTED_MAIN_PATH / 'stdin' / 'output.py').read_text()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@freeze_time('2019-07-26')
|
@freeze_time('2019-07-26')
|
||||||
|
|||||||
Reference in New Issue
Block a user