Skip to content

Commit

Permalink
Support nested directory (#275)
Browse files Browse the repository at this point in the history
* Support nested directory

* improve test pattern

* fix expected code
  • Loading branch information
koxudaxi authored Dec 7, 2020
1 parent 43bdc62 commit 70f3ed0
Show file tree
Hide file tree
Showing 17 changed files with 183 additions and 8 deletions.
8 changes: 7 additions & 1 deletion datamodel_code_generator/model/enum.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any, List, Optional

from datamodel_code_generator.imports import IMPORT_ENUM
Expand All @@ -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)

Expand Down
11 changes: 10 additions & 1 deletion datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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_:
Expand Down
12 changes: 8 additions & 4 deletions datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion datamodel_code_generator/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions tests/data/expected/main/main_nested_directory/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: external_files_in_directory
# timestamp: 2019-07-26T00:00:00+00:00
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: external_files_in_directory
# timestamp: 2019-07-26T00:00:00+00:00
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# generated by datamodel-codegen:
# filename: external_files_in_directory
# timestamp: 2019-07-26T00:00:00+00:00
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -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'
12 changes: 12 additions & 0 deletions tests/data/expected/main/main_nested_directory/definitions/fur.py
Original file line number Diff line number Diff line change
@@ -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'
17 changes: 17 additions & 0 deletions tests/data/expected/main/main_nested_directory/definitions/pet.py
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions tests/data/expected/main/main_nested_directory/person.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$id": "coffee.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Coffee",
"type": "string",
"enum": [
"Black",
"Espresso"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$id": "tea.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Tea",
"type": "string",
"enum": [
"Oolong",
"Green"
]
}
11 changes: 11 additions & 0 deletions tests/data/jsonschema/external_files_in_directory/person.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
},
"comment": {
"type": "null"
},
"drink": {
"type": "array",
"items": [
{
"$ref": "definitions/drink/coffee.json#"
},
{
"$ref": "definitions/drink/tea.json#"
}
]
}
},
"required": [
Expand Down
25 changes: 25 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

0 comments on commit 70f3ed0

Please sign in to comment.