From 70f3ed0adb3310235b8e87989034e30a00647976 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 7 Dec 2020 18:49:58 +0900 Subject: [PATCH] Support nested directory (#275) * Support nested directory * improve test pattern * fix expected code --- datamodel_code_generator/model/enum.py | 8 +++++- datamodel_code_generator/parser/base.py | 11 +++++++- datamodel_code_generator/parser/jsonschema.py | 12 ++++++--- datamodel_code_generator/reference.py | 8 +++++- .../output.py | 13 +++++++++- .../main/main_nested_directory/__init__.py | 3 +++ .../definitions/__init__.py | 3 +++ .../definitions/drink/__init__.py | 3 +++ .../definitions/drink/coffee.py | 12 +++++++++ .../definitions/drink/tea.py | 12 +++++++++ .../main_nested_directory/definitions/fur.py | 12 +++++++++ .../main_nested_directory/definitions/pet.py | 17 +++++++++++++ .../main/main_nested_directory/person.py | 21 ++++++++++++++++ .../definitions/drink/coffee.json | 10 ++++++++ .../definitions/drink/tea.json | 10 ++++++++ .../external_files_in_directory/person.json | 11 ++++++++ tests/test_main.py | 25 +++++++++++++++++++ 17 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 tests/data/expected/main/main_nested_directory/__init__.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/__init__.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/drink/__init__.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/drink/coffee.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/drink/tea.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/fur.py create mode 100644 tests/data/expected/main/main_nested_directory/definitions/pet.py create mode 100644 tests/data/expected/main/main_nested_directory/person.py create mode 100644 tests/data/jsonschema/external_files_in_directory/definitions/drink/coffee.json create mode 100644 tests/data/jsonschema/external_files_in_directory/definitions/drink/tea.json diff --git a/datamodel_code_generator/model/enum.py b/datamodel_code_generator/model/enum.py index 54282878d..5fe26cc6d 100644 --- a/datamodel_code_generator/model/enum.py +++ b/datamodel_code_generator/model/enum.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any, List, Optional from datamodel_code_generator.imports import IMPORT_ENUM @@ -14,9 +15,14 @@ def __init__( name: str, fields: List[DataModelFieldBase], decorators: Optional[List[str]] = None, + path: Optional[Path] = None, ): super().__init__( - name=name, fields=fields, decorators=decorators, auto_import=False + name=name, + fields=fields, + decorators=decorators, + auto_import=False, + path=path, ) self.imports.append(IMPORT_ENUM) diff --git a/datamodel_code_generator/parser/base.py b/datamodel_code_generator/parser/base.py index 084b9cbb5..be4c450fc 100644 --- a/datamodel_code_generator/parser/base.py +++ b/datamodel_code_generator/parser/base.py @@ -291,6 +291,7 @@ def parse( imports = Imports() models_to_update: List[str] = [] scoped_model_resolver = ModelResolver() + import_map: Dict[str, Tuple[str, str]] = {} for model in models: alias_map: Dict[str, Optional[str]] = {} if model.name in require_update_action_models: @@ -341,13 +342,21 @@ def parse( ).name alias_map[full_path] = None if alias == import_ else alias new_name = f'{alias}.{name}' if from_ and import_ else name + if data_type.module_name and not type_.startswith(from_): + import_map[new_name] = ( + f'.{type_[:len(new_name) * - 1 - 1]}', + new_name.split('.')[0], + ) if name in model.reference_classes: model.reference_classes.remove(name) model.reference_classes.add(new_name) data_type.type = new_name for ref_name in model.reference_classes: - from_, import_ = relative(module_path, ref_name) + if ref_name in import_map: + from_, import_ = import_map[ref_name] + else: + from_, import_ = relative(module_path, ref_name) if init: from_ += "." if from_ and import_: diff --git a/datamodel_code_generator/parser/jsonschema.py b/datamodel_code_generator/parser/jsonschema.py index b1746354e..9018dff01 100644 --- a/datamodel_code_generator/parser/jsonschema.py +++ b/datamodel_code_generator/parser/jsonschema.py @@ -670,7 +670,7 @@ def parse_enum( singular_name_suffix='Enum', unique=unique, ).name - enum = Enum(enum_name, fields=enum_fields) + enum = Enum(enum_name, fields=enum_fields, path=self.current_source_path) self.append_result(enum) return enum @@ -697,7 +697,10 @@ def _get_ref_body(self, ref: str) -> Dict[Any, Any]: else: # Remote Reference – $ref: 'document.json' Uses the whole document located on the same server and in # the same location. TODO treat edge case - full_path = self.base_path / ref + if self.current_source_path and len(self.current_source_path.parts) > 1: + full_path = self.base_path / self.current_source_path.parent / ref + else: + full_path = self.base_path / ref # yaml loader can parse json data. with full_path.open() as f: ref_body = yaml.safe_load(f) @@ -742,11 +745,12 @@ def parse_ref(self, obj: JsonSchemaObject, path: List[str]) -> None: self.base_path = (self.base_path / relative_path).parent else: previous_base_path = None + relative_paths = relative_path.split('/') self._parse_file( models, model_name, - [relative_path, '#', *object_paths], - [relative_path], + [*relative_paths, '#', *object_paths], + relative_paths, ) if previous_base_path: self.base_path = previous_base_path diff --git a/datamodel_code_generator/reference.py b/datamodel_code_generator/reference.py index 265651956..1b3eacfb0 100644 --- a/datamodel_code_generator/reference.py +++ b/datamodel_code_generator/reference.py @@ -21,7 +21,11 @@ def module_name(self) -> Optional[str]: return None # TODO: Support file:/// path = Path(self.path.split('#')[0]) - module_name = f'{".".join(path.parts[:-1][1:])}.{path.stem}' + + # workaround: If a file name has dot then, this method uses first part. + module_name = f'{".".join(path.parts[:-1])}.{path.stem.split(".")[0]}' + if module_name.startswith(f'.{self.name.split(".", 1)[0]}'): + return None if module_name == '.': return None return module_name @@ -69,6 +73,8 @@ def _get_path(self, path: List[str]) -> str: return f'{joined_path}#/' def is_after_load(self, ref: str) -> bool: + if self.current_root and len(self.current_root) > 1: + ref = f"{'/'.join(self.current_root[:-1])}/{ref}" if self.is_external_ref(ref): return ref.split('#/', 1)[0] in self.after_load_files elif self.is_external_root_ref(ref): diff --git a/tests/data/expected/main/main_external_files_in_directory/output.py b/tests/data/expected/main/main_external_files_in_directory/output.py index 803e4f221..aaa89e04a 100644 --- a/tests/data/expected/main/main_external_files_in_directory/output.py +++ b/tests/data/expected/main/main_external_files_in_directory/output.py @@ -5,7 +5,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, List, Optional +from typing import Any, List, Optional, Union from pydantic import BaseModel, Field, conint @@ -15,6 +15,16 @@ class Fur(Enum): Long_hair = 'Long hair' +class Coffee(Enum): + Black = 'Black' + Espresso = 'Espresso' + + +class Tea(Enum): + Oolong = 'Oolong' + Green = 'Green' + + class Pet(BaseModel): name: Optional[str] = None age: Optional[int] = None @@ -27,3 +37,4 @@ class Person(BaseModel): age: Optional[conint(ge=0)] = Field(None, description='Age in years.') pets: Optional[List[Pet]] = None comment: Optional[Any] = None + drink: Optional[List[Union[Coffee, Tea]]] = None diff --git a/tests/data/expected/main/main_nested_directory/__init__.py b/tests/data/expected/main/main_nested_directory/__init__.py new file mode 100644 index 000000000..4146e0ec1 --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: external_files_in_directory +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/main_nested_directory/definitions/__init__.py b/tests/data/expected/main/main_nested_directory/definitions/__init__.py new file mode 100644 index 000000000..4146e0ec1 --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: external_files_in_directory +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/main_nested_directory/definitions/drink/__init__.py b/tests/data/expected/main/main_nested_directory/definitions/drink/__init__.py new file mode 100644 index 000000000..4146e0ec1 --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/drink/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: external_files_in_directory +# timestamp: 2019-07-26T00:00:00+00:00 diff --git a/tests/data/expected/main/main_nested_directory/definitions/drink/coffee.py b/tests/data/expected/main/main_nested_directory/definitions/drink/coffee.py new file mode 100644 index 000000000..deed3ce45 --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/drink/coffee.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: definitions/drink/coffee.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from enum import Enum + + +class Coffee(Enum): + Black = 'Black' + Espresso = 'Espresso' diff --git a/tests/data/expected/main/main_nested_directory/definitions/drink/tea.py b/tests/data/expected/main/main_nested_directory/definitions/drink/tea.py new file mode 100644 index 000000000..a7d5f30a1 --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/drink/tea.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: definitions/drink/tea.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from enum import Enum + + +class Tea(Enum): + Oolong = 'Oolong' + Green = 'Green' diff --git a/tests/data/expected/main/main_nested_directory/definitions/fur.py b/tests/data/expected/main/main_nested_directory/definitions/fur.py new file mode 100644 index 000000000..0adb6df9f --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/fur.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: definitions/fur.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from enum import Enum + + +class Fur(Enum): + Short_hair = 'Short hair' + Long_hair = 'Long hair' diff --git a/tests/data/expected/main/main_nested_directory/definitions/pet.py b/tests/data/expected/main/main_nested_directory/definitions/pet.py new file mode 100644 index 000000000..c54b8925d --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/definitions/pet.py @@ -0,0 +1,17 @@ +# generated by datamodel-codegen: +# filename: definitions/pet.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel + +from . import fur + + +class Pet(BaseModel): + name: Optional[str] = None + age: Optional[int] = None + fur: Optional[fur.Fur] = None diff --git a/tests/data/expected/main/main_nested_directory/person.py b/tests/data/expected/main/main_nested_directory/person.py new file mode 100644 index 000000000..cfa28feac --- /dev/null +++ b/tests/data/expected/main/main_nested_directory/person.py @@ -0,0 +1,21 @@ +# generated by datamodel-codegen: +# filename: person.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Any, List, Optional, Union + +from pydantic import BaseModel, Field, conint + +from .definitions import pet +from .definitions.drink import coffee, tea + + +class Person(BaseModel): + first_name: str = Field(..., description="The person's first name.") + last_name: str = Field(..., description="The person's last name.") + age: Optional[conint(ge=0)] = Field(None, description='Age in years.') + pets: Optional[List[pet.Pet]] = None + comment: Optional[Any] = None + drink: Optional[List[Union[coffee.Coffee, tea.Tea]]] = None diff --git a/tests/data/jsonschema/external_files_in_directory/definitions/drink/coffee.json b/tests/data/jsonschema/external_files_in_directory/definitions/drink/coffee.json new file mode 100644 index 000000000..0bac30be6 --- /dev/null +++ b/tests/data/jsonschema/external_files_in_directory/definitions/drink/coffee.json @@ -0,0 +1,10 @@ +{ + "$id": "coffee.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Coffee", + "type": "string", + "enum": [ + "Black", + "Espresso" + ] +} \ No newline at end of file diff --git a/tests/data/jsonschema/external_files_in_directory/definitions/drink/tea.json b/tests/data/jsonschema/external_files_in_directory/definitions/drink/tea.json new file mode 100644 index 000000000..f637a2b8e --- /dev/null +++ b/tests/data/jsonschema/external_files_in_directory/definitions/drink/tea.json @@ -0,0 +1,10 @@ +{ + "$id": "tea.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tea", + "type": "string", + "enum": [ + "Oolong", + "Green" + ] +} \ No newline at end of file diff --git a/tests/data/jsonschema/external_files_in_directory/person.json b/tests/data/jsonschema/external_files_in_directory/person.json index f49a0d2e9..9eb15c07d 100644 --- a/tests/data/jsonschema/external_files_in_directory/person.json +++ b/tests/data/jsonschema/external_files_in_directory/person.json @@ -27,6 +27,17 @@ }, "comment": { "type": "null" + }, + "drink": { + "type": "array", + "items": [ + { + "$ref": "definitions/drink/coffee.json#" + }, + { + "$ref": "definitions/drink/tea.json#" + } + ] } }, "required": [ diff --git a/tests/test_main.py b/tests/test_main.py index 0918cdaa1..ca6ebf1e9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1143,3 +1143,28 @@ def test_main_external_files_in_directory(tmpdir_factory: TempdirFactory) -> Non ) with pytest.raises(SystemExit): main() + + +@freeze_time('2019-07-26') +def test_main_nested_directory(tmpdir_factory: TempdirFactory) -> None: + output_directory = Path(tmpdir_factory.mktemp('output')) + + output_path = output_directory / 'model' + return_code: Exit = main( + [ + '--input', + str(JSON_SCHEMA_DATA_PATH / 'external_files_in_directory'), + '--output', + str(output_path), + '--input-file-type', + 'jsonschema', + ] + ) + assert return_code == Exit.OK + main_nested_directory = EXPECTED_MAIN_PATH / 'main_nested_directory' + + for path in main_nested_directory.rglob('*.py'): + result = output_path.joinpath( + path.relative_to(main_nested_directory) + ).read_text() + assert result == path.read_text()