Skip to content

Commit 934bed7

Browse files
🚧 Render all dynamic modules, including empty __init__ from a single template.
1 parent 70a9379 commit 934bed7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+225
-45
lines changed

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ repos:
88
hooks:
99
- id: trailing-whitespace
1010
- id: end-of-file-fixer
11+
exclude: ^src/lapidary/render/templates/render/includes/header.txt$|^tests/e2e/expected/
1112
- id: check-added-large-files
1213
- id: check-toml
1314
- id: debug-statements

‎src/lapidary/render/main.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async def render_project(project_root: anyio.Path) -> None:
113113
model=model,
114114
get_version=importlib.metadata.version,
115115
),
116-
pathlib.Path(project_root),
116+
pathlib.Path(project_root) / 'gen',
117117
event_sink=event_sink,
118118
remove_stale=True,
119119
)
@@ -122,8 +122,8 @@ async def render_project(project_root: anyio.Path) -> None:
122122
class RenderProgressBar(rybak.EventSink):
123123
def __init__(self, model: python.ClientModel) -> None:
124124
self._progress_bar = click.progressbar(
125-
model.schemas,
126-
label='Rendering schemas',
125+
model.modules,
126+
label='Rendering modules',
127127
item_show_func=lambda item: item or '',
128128
show_pos=True,
129129
)
@@ -136,8 +136,9 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> bool | None:
136136
return self._progress_bar.__exit__(exc_type, exc_val, exc_tb)
137137

138138
def writing_file(self, template: pathlib.PurePath, target: pathlib.Path) -> None:
139-
if str(template) == 'gen/{{loop_over(model.schemas).path.to_path()}}.jinja':
140-
self._progress_bar.update(1, str(target).split('/')[-3])
139+
super().writing_file(template, target)
140+
if str(template) == '{{model.package}}/{{loop_over(model.modules).rel_path}}.py.jinja':
141+
self._progress_bar.update(1, str(target).split('/')[-2])
141142

142143

143144
async def dump_model(project_root: anyio.Path, process: bool, output: TextIO):

‎src/lapidary/render/model/python/__init__.py‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
)
3232

3333
import dataclasses as dc
34-
from collections.abc import Iterable, MutableMapping, MutableSequence
34+
from collections.abc import Iterable, MutableMapping, MutableSequence, MutableSet, Sequence
35+
from functools import cached_property
3536
from typing import Self
3637

3738
from ..openapi import ParameterLocation as ParamLocation
@@ -56,7 +57,7 @@
5657
SchemaClass,
5758
SecurityRequirements,
5859
)
59-
from .module import AuthModule, ClientModule, SchemaModule
60+
from .module import AbstractModule, AuthModule, ClientModule, EmptyModule, SchemaModule
6061
from .module_path import ModulePath
6162
from .type_hint import NONE, BuiltinTypeHint, GenericTypeHint, TypeHint, type_hint_or_union
6263

@@ -71,13 +72,22 @@ class ClientModel:
7172
def packages(self: Self) -> Iterable[ModulePath]:
7273
# Used to create __init__.py files in otherwise empty packages
7374

74-
packages = {ModulePath(self.package)}
75+
known_packages: MutableSet[ModulePath] = {ModulePath.root()}
7576

7677
# for each schema module get its package
7778
for schema in self.schemas:
78-
path = schema.path
79-
while path_ := path.parent():
80-
packages.add(path_)
81-
path = path_
79+
path: ModulePath | None = schema.path
80+
while path := path.parent():
81+
if path in known_packages:
82+
continue
83+
yield path
84+
known_packages.add(path)
8285

83-
return packages
86+
@cached_property
87+
def modules(self) -> Sequence[AbstractModule]:
88+
return list(self._modules())
89+
90+
def _modules(self) -> Iterable[AbstractModule]:
91+
yield from self.schemas
92+
for package in self.packages():
93+
yield EmptyModule(path=package, body=None)

‎src/lapidary/render/model/python/module.py‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,29 @@
1414

1515

1616
@dc.dataclass(frozen=True, kw_only=True)
17-
class AbstractModule(abc.ABC):
18-
path: ModulePath
17+
class AbstractModule[Body](abc.ABC):
18+
path: ModulePath = dc.field()
19+
module_type: str
20+
body: Body = dc.field()
1921

2022
@property
2123
@abc.abstractmethod
2224
def imports(self) -> Iterable[str]:
2325
pass
2426

27+
@property
28+
def rel_path(self) -> str:
29+
return self.path.to_path()
30+
2531

2632
@dc.dataclass(frozen=True, kw_only=True)
27-
class SchemaModule(AbstractModule):
33+
class SchemaModule(AbstractModule[Iterable[SchemaClass]]):
2834
"""
2935
One schema module per schema element directly under #/components/schemas, containing that schema and all non-reference schemas.
3036
One schema module for inline request and for response body for each operation
3137
"""
3238

33-
body: list[SchemaClass] = dc.field(default_factory=list)
34-
model_type: str = 'schema'
39+
module_type: str = 'schema'
3540

3641
@property
3742
def imports(self) -> Iterable[str]:
@@ -47,19 +52,33 @@ def imports(self) -> Iterable[str]:
4752

4853

4954
@dc.dataclass(frozen=True, kw_only=True)
50-
class AuthModule(AbstractModule):
51-
schemes: Mapping[str, TypeHint] = dc.field()
52-
model_type = 'auth'
55+
class AuthModule(AbstractModule[Mapping[str, TypeHint]]):
56+
module_type = 'auth'
57+
58+
@property
59+
def imports(self) -> Iterable[str]:
60+
yield from ()
5361

5462

5563
@dc.dataclass(frozen=True, kw_only=True)
56-
class ClientModule(AbstractModule):
57-
body: ClientClass = dc.field()
58-
model_type = 'client'
64+
class ClientModule(AbstractModule[ClientClass]):
65+
module_type: str = dc.field(default='client')
5966

6067
@property
6168
def imports(self) -> Iterable[str]:
6269
dependencies = GenericTypeHint.union_of(*self.body.dependencies).args # flatten unions
6370
imports = sorted({imp for dep in dependencies if dep for imp in dep.imports() if imp not in template_imports})
6471

6572
return imports
73+
74+
75+
@dc.dataclass(frozen=True, kw_only=True)
76+
class EmptyModule(AbstractModule[None]):
77+
"""Module used to generate empty __init__.py files"""
78+
79+
module_type: str = dc.field(default='empty')
80+
body: None = None
81+
82+
@property
83+
def imports(self) -> Iterable[str]:
84+
yield from ()

‎src/lapidary/render/model/python/module_path.py‎

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import typing
22
from collections.abc import Iterable
33
from pathlib import PurePath
4+
from typing import Self
45

56

67
class ModulePath:
78
_SEP = '.'
89

9-
def __init__(self, module: str | Iterable[str]):
10+
def __init__(self, module: str | Iterable[str], is_module: bool = True):
1011
if isinstance(module, str):
1112
module = module.strip()
12-
if module == '' or module.strip() != module:
13-
raise ValueError()
13+
if module.strip() != module:
14+
raise ValueError(f'"{module}"')
1415
parts: Iterable[str] = module.split(ModulePath._SEP)
1516
else:
1617
parts = module
@@ -22,19 +23,22 @@ def __init__(self, module: str | Iterable[str]):
2223
else:
2324
raise ValueError(module)
2425

25-
def to_path(self, root: PurePath | None = None, is_module=True):
26-
path = (root or PurePath()).joinpath(*self.parts)
27-
if is_module:
28-
name = self.parts[-1]
29-
dot_idx = name.rfind('.')
30-
suffix = name[dot_idx:] if dot_idx != -1 else '.py'
31-
path = path.with_suffix(suffix)
32-
return path
26+
self._is_module = is_module
27+
28+
def to_path(self, root: PurePath | None = None):
29+
parts = list(self.parts)
30+
if not self._is_module:
31+
parts.append('__init__')
32+
return (root or PurePath()).joinpath(*parts)
3333

3434
def parent(self) -> typing.Self | None:
3535
if len(self.parts) == 1:
3636
return None
37-
return ModulePath(self.parts[:-1])
37+
return ModulePath(self.parts[:-1], False)
38+
39+
@classmethod
40+
def root(cls) -> Self:
41+
return cls('', is_module=False)
3842

3943
def __truediv__(self, other: str | Iterable[str]):
4044
if isinstance(other, str):
@@ -51,3 +55,12 @@ def __eq__(self, other: object):
5155

5256
def __hash__(self) -> int:
5357
return hash(self.__str__())
58+
59+
def __matmul__(self, other):
60+
if not isinstance(other, ModulePath):
61+
return NotImplemented
62+
63+
if self.parts[: len(other.parts)] != other.parts:
64+
raise ValueError('Not related')
65+
66+
return ModulePath(self.parts[len(other.parts) :])

‎src/lapidary/render/model/python/type_hint.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def __hash__(self) -> int:
118118

119119
class NoneTypeHint(TypeHint):
120120
def __init__(self):
121-
pass
121+
super().__init__(module=None, name=None)
122122

123123
def full_name(self):
124124
return 'None'

‎src/lapidary/render/model/schema.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ def schema_modules(self) -> Iterable[python.SchemaModule]:
141141
modules: dict[python.ModulePath, list[python.SchemaClass]] = defaultdict(list)
142142
for pointer, schema_class_type in self.schema_types.items():
143143
schema_class, hint = schema_class_type
144-
modules[python.ModulePath(hint.module) / '__init__'].append(schema_class)
144+
modules[python.ModulePath(hint.module, True)].append(schema_class)
145145
return [
146146
python.SchemaModule(
147-
path=module,
147+
path=module @ self.root_package,
148148
body=classes,
149149
)
150150
for module, classes in modules.items()

src/lapidary/render/templates/render/gen/{{loop_over(model.packages()).to_path(is_module=False)}}/__init__.py renamed to src/lapidary/render/templates/render/includes/module/empty.py.jinja

File renamed without changes.

src/lapidary/render/templates/render/gen/{{loop_over(model.schemas).path.to_path()}}.jinja renamed to src/lapidary/render/templates/render/includes/module/schema.py.jinja

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# {% include 'includes/header.txt' %}
21
from __future__ import annotations
32

43
import typing
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# {% include 'includes/header.txt' %}
2+
3+
from .client import ApiClient

0 commit comments

Comments
 (0)