pyopenapi-gen 2.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""pyopenapi_gen – Core package
|
|
2
|
+
|
|
3
|
+
This package provides the internal representation (IR) dataclasses that act as an
|
|
4
|
+
intermediate layer between the parsed OpenAPI specification and the code
|
|
5
|
+
emitters. The IR aims to be a *stable*, *fully‑typed* model that the rest of the
|
|
6
|
+
code‑generation pipeline can rely on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# Removed dataclasses, field, Enum, unique from here, they are in ir.py and http_types.py
|
|
14
|
+
from typing import (
|
|
15
|
+
TYPE_CHECKING,
|
|
16
|
+
Any,
|
|
17
|
+
List,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Kept Any, List, Optional, TYPE_CHECKING for __getattr__ and __dir__
|
|
21
|
+
# Import HTTPMethod from its canonical location
|
|
22
|
+
from .http_types import HTTPMethod
|
|
23
|
+
|
|
24
|
+
# Import IR classes from their canonical location
|
|
25
|
+
from .ir import (
|
|
26
|
+
IROperation,
|
|
27
|
+
IRParameter,
|
|
28
|
+
IRRequestBody,
|
|
29
|
+
IRResponse,
|
|
30
|
+
IRSchema,
|
|
31
|
+
IRSpec,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Main API
|
|
36
|
+
"generate_client",
|
|
37
|
+
"ClientGenerator",
|
|
38
|
+
"GenerationError",
|
|
39
|
+
# IR classes
|
|
40
|
+
"HTTPMethod",
|
|
41
|
+
"IRParameter",
|
|
42
|
+
"IRResponse",
|
|
43
|
+
"IROperation",
|
|
44
|
+
"IRSchema",
|
|
45
|
+
"IRSpec",
|
|
46
|
+
"IRRequestBody",
|
|
47
|
+
# Utilities
|
|
48
|
+
"load_ir_from_spec",
|
|
49
|
+
"WarningCollector",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Semantic version of the generator core – automatically managed by semantic-release.
|
|
53
|
+
__version__: str = "2.7.2"
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Lazy-loading and autocompletion support (This part remains)
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
# Imports for static analysis
|
|
60
|
+
from .core.loader.loader import load_ir_from_spec # noqa: F401
|
|
61
|
+
from .core.warning_collector import WarningCollector # noqa: F401
|
|
62
|
+
from .generator.client_generator import ClientGenerator, GenerationError # noqa: F401
|
|
63
|
+
|
|
64
|
+
# Expose loader and collector at package level
|
|
65
|
+
# __all__ is already updated above
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __getattr__(name: str) -> Any:
|
|
69
|
+
# Lazy-import attributes for runtime, supports IDE completion via TYPE_CHECKING
|
|
70
|
+
if name == "load_ir_from_spec":
|
|
71
|
+
from .core.loader.loader import load_ir_from_spec as _func
|
|
72
|
+
|
|
73
|
+
return _func
|
|
74
|
+
if name == "WarningCollector":
|
|
75
|
+
from .core.warning_collector import WarningCollector as _warning_cls
|
|
76
|
+
|
|
77
|
+
return _warning_cls
|
|
78
|
+
if name == "ClientGenerator":
|
|
79
|
+
from .generator.client_generator import ClientGenerator as _gen_cls
|
|
80
|
+
|
|
81
|
+
return _gen_cls
|
|
82
|
+
if name == "GenerationError":
|
|
83
|
+
from .generator.client_generator import GenerationError as _exc
|
|
84
|
+
|
|
85
|
+
return _exc
|
|
86
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def __dir__() -> List[str]:
|
|
90
|
+
# Ensure dir() and completion shows all exports
|
|
91
|
+
return __all__.copy()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Main Programmatic API
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_client(
|
|
100
|
+
spec_path: str,
|
|
101
|
+
project_root: str,
|
|
102
|
+
output_package: str,
|
|
103
|
+
core_package: str | None = None,
|
|
104
|
+
force: bool = False,
|
|
105
|
+
no_postprocess: bool = False,
|
|
106
|
+
verbose: bool = False,
|
|
107
|
+
) -> List[Path]:
|
|
108
|
+
"""Generate a Python client from an OpenAPI specification.
|
|
109
|
+
|
|
110
|
+
This is the main entry point for programmatic usage of pyopenapi_gen.
|
|
111
|
+
It provides a simple function-based API for generating OpenAPI clients
|
|
112
|
+
without needing to instantiate classes or understand internal structure.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
spec_path: Path or URL to the OpenAPI specification (YAML or JSON).
|
|
116
|
+
Can be a relative/absolute file path or HTTP(S) URL.
|
|
117
|
+
Examples: "input/spec.yaml", "https://api.example.com/openapi.json"
|
|
118
|
+
|
|
119
|
+
project_root: Root directory of your Python project where the generated
|
|
120
|
+
client will be placed. This is the directory that contains
|
|
121
|
+
your top-level Python packages.
|
|
122
|
+
|
|
123
|
+
output_package: Python package name for the generated client.
|
|
124
|
+
Uses dot notation (e.g., 'pyapis.my_client').
|
|
125
|
+
The client will be generated at:
|
|
126
|
+
{project_root}/{output_package_as_path}/
|
|
127
|
+
|
|
128
|
+
core_package: Optional Python package name for shared core runtime.
|
|
129
|
+
If not specified, defaults to {output_package}.core.
|
|
130
|
+
Use this when generating multiple clients that share
|
|
131
|
+
common runtime code (auth, config, http transport, etc.).
|
|
132
|
+
|
|
133
|
+
force: If True, overwrites existing output without diff checking.
|
|
134
|
+
If False (default), compares with existing output and only
|
|
135
|
+
updates if changes are detected.
|
|
136
|
+
|
|
137
|
+
no_postprocess: If True, skips post-processing (Black formatting and
|
|
138
|
+
mypy type checking). Useful for faster iteration during
|
|
139
|
+
development.
|
|
140
|
+
|
|
141
|
+
verbose: If True, prints detailed progress information during generation.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of Path objects for all generated files.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
GenerationError: If generation fails due to invalid spec, file I/O
|
|
148
|
+
errors, or other issues during code generation.
|
|
149
|
+
|
|
150
|
+
Examples:
|
|
151
|
+
Basic usage with default settings:
|
|
152
|
+
|
|
153
|
+
>>> from pyopenapi_gen import generate_client
|
|
154
|
+
>>>
|
|
155
|
+
>>> files = generate_client(
|
|
156
|
+
... spec_path="input/openapi.yaml",
|
|
157
|
+
... project_root=".",
|
|
158
|
+
... output_package="pyapis.my_client"
|
|
159
|
+
... )
|
|
160
|
+
>>> print(f"Generated {len(files)} files")
|
|
161
|
+
|
|
162
|
+
Generate multiple clients sharing a common core package:
|
|
163
|
+
|
|
164
|
+
>>> from pyopenapi_gen import generate_client
|
|
165
|
+
>>>
|
|
166
|
+
>>> # Generate first client (creates shared core)
|
|
167
|
+
>>> generate_client(
|
|
168
|
+
... spec_path="api_v1.yaml",
|
|
169
|
+
... project_root=".",
|
|
170
|
+
... output_package="pyapis.client_v1",
|
|
171
|
+
... core_package="pyapis.core"
|
|
172
|
+
... )
|
|
173
|
+
>>>
|
|
174
|
+
>>> # Generate second client (reuses core)
|
|
175
|
+
>>> generate_client(
|
|
176
|
+
... spec_path="api_v2.yaml",
|
|
177
|
+
... project_root=".",
|
|
178
|
+
... output_package="pyapis.client_v2",
|
|
179
|
+
... core_package="pyapis.core"
|
|
180
|
+
... )
|
|
181
|
+
|
|
182
|
+
Handle generation errors:
|
|
183
|
+
|
|
184
|
+
>>> from pyopenapi_gen import generate_client, GenerationError
|
|
185
|
+
>>>
|
|
186
|
+
>>> try:
|
|
187
|
+
... generate_client(
|
|
188
|
+
... spec_path="openapi.yaml",
|
|
189
|
+
... project_root=".",
|
|
190
|
+
... output_package="pyapis.my_client",
|
|
191
|
+
... verbose=True
|
|
192
|
+
... )
|
|
193
|
+
... except GenerationError as e:
|
|
194
|
+
... print(f"Generation failed: {e}")
|
|
195
|
+
|
|
196
|
+
Force regeneration with verbose output:
|
|
197
|
+
|
|
198
|
+
>>> generate_client(
|
|
199
|
+
... spec_path="openapi.yaml",
|
|
200
|
+
... project_root=".",
|
|
201
|
+
... output_package="pyapis.my_client",
|
|
202
|
+
... force=True,
|
|
203
|
+
... verbose=True
|
|
204
|
+
... )
|
|
205
|
+
|
|
206
|
+
Notes:
|
|
207
|
+
- Generated clients are completely self-contained and require no
|
|
208
|
+
runtime dependency on pyopenapi_gen
|
|
209
|
+
- All generated code is automatically formatted with Black and
|
|
210
|
+
type-checked with mypy (unless no_postprocess=True)
|
|
211
|
+
- The generated client uses modern async/await patterns with httpx
|
|
212
|
+
- Type hints are included for all generated code
|
|
213
|
+
"""
|
|
214
|
+
from .generator.client_generator import ClientGenerator
|
|
215
|
+
|
|
216
|
+
generator = ClientGenerator(verbose=verbose)
|
|
217
|
+
return generator.generate(
|
|
218
|
+
spec_path=spec_path,
|
|
219
|
+
project_root=Path(project_root),
|
|
220
|
+
output_package=output_package,
|
|
221
|
+
core_package=core_package,
|
|
222
|
+
force=force,
|
|
223
|
+
no_postprocess=no_postprocess,
|
|
224
|
+
)
|
pyopenapi_gen/cli.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .core.spec_fetcher import is_url
|
|
6
|
+
from .generator.client_generator import ClientGenerator, GenerationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(
|
|
10
|
+
spec: str = typer.Argument(..., help="Path or URL to OpenAPI spec"),
|
|
11
|
+
project_root: Path = typer.Option(
|
|
12
|
+
...,
|
|
13
|
+
"--project-root",
|
|
14
|
+
help=(
|
|
15
|
+
"Path to the directory containing your top-level Python packages. "
|
|
16
|
+
"Generated code will be placed at project-root + output-package path."
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
output_package: str = typer.Option(
|
|
20
|
+
..., "--output-package", help="Python package path for the generated client (e.g., 'pyapis.my_api_client')."
|
|
21
|
+
),
|
|
22
|
+
force: bool = typer.Option(False, "-f", "--force", help="Overwrite without diff check"),
|
|
23
|
+
no_postprocess: bool = typer.Option(False, "--no-postprocess", help="Skip post-processing (type checking, etc.)"),
|
|
24
|
+
core_package: str | None = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--core-package",
|
|
27
|
+
help=(
|
|
28
|
+
"Python package path for the core package (e.g., 'pyapis.core'). "
|
|
29
|
+
"If not set, defaults to <output-package>.core."
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Generate a Python OpenAPI client from a spec file or URL.
|
|
35
|
+
Only parses CLI arguments and delegates to ClientGenerator.
|
|
36
|
+
"""
|
|
37
|
+
if core_package is None:
|
|
38
|
+
core_package = output_package + ".core"
|
|
39
|
+
generator = ClientGenerator()
|
|
40
|
+
# Handle both URLs (pass as-is) and file paths (resolve to absolute)
|
|
41
|
+
spec_path = spec if is_url(spec) else str(Path(spec).resolve())
|
|
42
|
+
try:
|
|
43
|
+
generator.generate(
|
|
44
|
+
spec_path=spec_path,
|
|
45
|
+
project_root=project_root,
|
|
46
|
+
output_package=output_package,
|
|
47
|
+
force=force,
|
|
48
|
+
no_postprocess=no_postprocess,
|
|
49
|
+
core_package=core_package,
|
|
50
|
+
)
|
|
51
|
+
typer.echo("Client generation complete.")
|
|
52
|
+
except GenerationError as e:
|
|
53
|
+
typer.echo(f"Generation failed: {e}", err=True)
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
app = typer.Typer(help="PyOpenAPI Generator CLI - Generate Python clients from OpenAPI specs.")
|
|
58
|
+
app.command()(main)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
app()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# context/ - Rendering Context Management
|
|
2
|
+
|
|
3
|
+
## Why This Folder?
|
|
4
|
+
Manage stateful information during code generation: imports, templates, file paths, and rendering state. Provides clean interface between visitors and emitters.
|
|
5
|
+
|
|
6
|
+
## Key Dependencies
|
|
7
|
+
- **Input**: Path configuration, package names, template data
|
|
8
|
+
- **Output**: Import statements, resolved file paths, template rendering
|
|
9
|
+
- **Used by**: All visitors and emitters for consistent code generation
|
|
10
|
+
|
|
11
|
+
## Essential Architecture
|
|
12
|
+
|
|
13
|
+
### 1. Context Lifecycle
|
|
14
|
+
```python
|
|
15
|
+
# 1. Create context for generation session
|
|
16
|
+
context = RenderContext(project_root="/path/to/project", output_package="my_client")
|
|
17
|
+
|
|
18
|
+
# 2. Visitors use context for type resolution and imports
|
|
19
|
+
visitor.visit_schema(schema, context) # Registers imports
|
|
20
|
+
|
|
21
|
+
# 3. Emitters use context for file organization
|
|
22
|
+
emitter.emit_models(schemas, context) # Consumes imports
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. State Management
|
|
26
|
+
```python
|
|
27
|
+
# render_context.py
|
|
28
|
+
class RenderContext:
|
|
29
|
+
def __init__(self, project_root: Path, output_package: str):
|
|
30
|
+
self.import_collector = ImportCollector()
|
|
31
|
+
self.file_manager = FileManager(project_root)
|
|
32
|
+
self.template_vars = {}
|
|
33
|
+
self.output_package = output_package
|
|
34
|
+
self.forward_refs = set()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Critical Components
|
|
38
|
+
|
|
39
|
+
### render_context.py
|
|
40
|
+
**Purpose**: Main context object passed through generation pipeline
|
|
41
|
+
```python
|
|
42
|
+
class RenderContext:
|
|
43
|
+
def add_import(self, import_statement: str) -> None:
|
|
44
|
+
"""Register import for current file being generated"""
|
|
45
|
+
self.import_collector.add_import(import_statement)
|
|
46
|
+
|
|
47
|
+
def get_imports(self) -> List[str]:
|
|
48
|
+
"""Get sorted, deduplicated imports for current file"""
|
|
49
|
+
return self.import_collector.get_sorted_imports()
|
|
50
|
+
|
|
51
|
+
def clear_imports(self) -> None:
|
|
52
|
+
"""Clear imports for next file generation"""
|
|
53
|
+
self.import_collector.clear()
|
|
54
|
+
|
|
55
|
+
def resolve_relative_import(self, from_package: str, to_package: str) -> str:
|
|
56
|
+
"""Convert absolute import to relative import"""
|
|
57
|
+
return self.import_collector.make_relative_import(from_package, to_package)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### import_collector.py
|
|
61
|
+
**Purpose**: Collect and manage import statements during code generation
|
|
62
|
+
```python
|
|
63
|
+
class ImportCollector:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.imports: Set[str] = set()
|
|
66
|
+
self.from_imports: Dict[str, Set[str]] = defaultdict(set)
|
|
67
|
+
|
|
68
|
+
def add_import(self, import_statement: str) -> None:
|
|
69
|
+
"""Add import statement, handling both 'import' and 'from' forms"""
|
|
70
|
+
if import_statement.startswith("from "):
|
|
71
|
+
self.parse_from_import(import_statement)
|
|
72
|
+
else:
|
|
73
|
+
self.imports.add(import_statement)
|
|
74
|
+
|
|
75
|
+
def get_sorted_imports(self) -> List[str]:
|
|
76
|
+
"""Return sorted imports: stdlib, third-party, local"""
|
|
77
|
+
return self.sort_imports_by_category()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### file_manager.py
|
|
81
|
+
**Purpose**: Handle file operations and path resolution
|
|
82
|
+
```python
|
|
83
|
+
class FileManager:
|
|
84
|
+
def __init__(self, project_root: Path):
|
|
85
|
+
self.project_root = project_root
|
|
86
|
+
|
|
87
|
+
def write_file(self, file_path: Path, content: str) -> None:
|
|
88
|
+
"""Write file with proper directory creation"""
|
|
89
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
file_path.write_text(content)
|
|
91
|
+
|
|
92
|
+
def resolve_package_path(self, package_name: str) -> Path:
|
|
93
|
+
"""Convert package.name to file system path"""
|
|
94
|
+
parts = package_name.split(".")
|
|
95
|
+
return self.project_root / Path(*parts)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Import Management Patterns
|
|
99
|
+
|
|
100
|
+
### 1. Import Categories
|
|
101
|
+
```python
|
|
102
|
+
# import_collector.py
|
|
103
|
+
def categorize_import(self, import_statement: str) -> ImportCategory:
|
|
104
|
+
"""Categorize imports for proper sorting"""
|
|
105
|
+
if self.is_stdlib_import(import_statement):
|
|
106
|
+
return ImportCategory.STDLIB
|
|
107
|
+
elif self.is_third_party_import(import_statement):
|
|
108
|
+
return ImportCategory.THIRD_PARTY
|
|
109
|
+
else:
|
|
110
|
+
return ImportCategory.LOCAL
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. From Import Consolidation
|
|
114
|
+
```python
|
|
115
|
+
# Convert multiple from imports to single statement
|
|
116
|
+
# "from typing import List"
|
|
117
|
+
# "from typing import Dict"
|
|
118
|
+
# →
|
|
119
|
+
# "from typing import Dict, List"
|
|
120
|
+
|
|
121
|
+
def consolidate_from_imports(self) -> List[str]:
|
|
122
|
+
consolidated = []
|
|
123
|
+
for module, imports in self.from_imports.items():
|
|
124
|
+
sorted_imports = sorted(imports)
|
|
125
|
+
consolidated.append(f"from {module} import {', '.join(sorted_imports)}")
|
|
126
|
+
return consolidated
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 3. Relative Import Conversion
|
|
130
|
+
```python
|
|
131
|
+
def make_relative_import(self, from_package: str, to_package: str) -> str:
|
|
132
|
+
"""Convert absolute import to relative import"""
|
|
133
|
+
# from my_client.models.user import User
|
|
134
|
+
# →
|
|
135
|
+
# from ..models.user import User (when called from my_client.endpoints)
|
|
136
|
+
|
|
137
|
+
from_parts = from_package.split(".")
|
|
138
|
+
to_parts = to_package.split(".")
|
|
139
|
+
|
|
140
|
+
# Find common prefix
|
|
141
|
+
common_len = self.find_common_prefix_length(from_parts, to_parts)
|
|
142
|
+
|
|
143
|
+
# Calculate relative depth
|
|
144
|
+
relative_depth = len(from_parts) - common_len
|
|
145
|
+
prefix = "." * relative_depth
|
|
146
|
+
|
|
147
|
+
# Build relative import
|
|
148
|
+
remaining_path = ".".join(to_parts[common_len:])
|
|
149
|
+
return f"from {prefix}{remaining_path} import"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Template Management
|
|
153
|
+
|
|
154
|
+
### 1. Template Variables
|
|
155
|
+
```python
|
|
156
|
+
# Store template variables for consistent rendering
|
|
157
|
+
context.template_vars.update({
|
|
158
|
+
"client_name": "MyAPIClient",
|
|
159
|
+
"base_url": "https://api.example.com",
|
|
160
|
+
"version": "1.0.0",
|
|
161
|
+
"auth_type": "bearer"
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 2. Template Rendering
|
|
166
|
+
```python
|
|
167
|
+
def render_template(self, template: str, **kwargs) -> str:
|
|
168
|
+
"""Render template with context variables"""
|
|
169
|
+
all_vars = {**self.template_vars, **kwargs}
|
|
170
|
+
return template.format(**all_vars)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Dependencies on Other Systems
|
|
174
|
+
|
|
175
|
+
### From types/
|
|
176
|
+
- Implements `TypeContext` protocol for type resolution
|
|
177
|
+
- Provides import registration for complex types
|
|
178
|
+
|
|
179
|
+
### From visit/
|
|
180
|
+
- Receives import registration during code generation
|
|
181
|
+
- Provides path resolution for relative imports
|
|
182
|
+
|
|
183
|
+
### From emitters/
|
|
184
|
+
- Provides file writing capabilities
|
|
185
|
+
- Supplies consolidated imports for file headers
|
|
186
|
+
|
|
187
|
+
## Testing Requirements
|
|
188
|
+
|
|
189
|
+
### Import Management Tests
|
|
190
|
+
```python
|
|
191
|
+
def test_import_collector__multiple_from_imports__consolidates_correctly():
|
|
192
|
+
# Arrange
|
|
193
|
+
collector = ImportCollector()
|
|
194
|
+
collector.add_import("from typing import List")
|
|
195
|
+
collector.add_import("from typing import Dict")
|
|
196
|
+
|
|
197
|
+
# Act
|
|
198
|
+
imports = collector.get_sorted_imports()
|
|
199
|
+
|
|
200
|
+
# Assert
|
|
201
|
+
assert "from typing import Dict, List" in imports
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Path Resolution Tests
|
|
205
|
+
```python
|
|
206
|
+
def test_file_manager__package_path__resolves_correctly():
|
|
207
|
+
# Test package name to file path conversion
|
|
208
|
+
manager = FileManager(Path("/project"))
|
|
209
|
+
path = manager.resolve_package_path("my_client.models")
|
|
210
|
+
|
|
211
|
+
assert path == Path("/project/my_client/models")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Extension Points
|
|
215
|
+
|
|
216
|
+
### Custom Import Sorting
|
|
217
|
+
```python
|
|
218
|
+
class CustomImportCollector(ImportCollector):
|
|
219
|
+
def sort_imports_by_category(self) -> List[str]:
|
|
220
|
+
# Custom import sorting logic
|
|
221
|
+
# Example: Group all async imports together
|
|
222
|
+
pass
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Template System Integration
|
|
226
|
+
```python
|
|
227
|
+
def add_template_engine(self, engine: TemplateEngine) -> None:
|
|
228
|
+
"""Add custom template engine (Jinja2, etc.)"""
|
|
229
|
+
self.template_engine = engine
|
|
230
|
+
|
|
231
|
+
def render_template(self, template_name: str, **kwargs) -> str:
|
|
232
|
+
"""Render template using custom engine"""
|
|
233
|
+
return self.template_engine.render(template_name, **kwargs)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Critical Implementation Details
|
|
237
|
+
|
|
238
|
+
### Thread Safety
|
|
239
|
+
```python
|
|
240
|
+
# Context is NOT thread-safe by design
|
|
241
|
+
# Each generation session gets its own context instance
|
|
242
|
+
def create_context() -> RenderContext:
|
|
243
|
+
return RenderContext(project_root, output_package)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Memory Management
|
|
247
|
+
```python
|
|
248
|
+
# Clear context between files to prevent memory leaks
|
|
249
|
+
def emit_file(self, file_path: Path, generator_func: Callable) -> None:
|
|
250
|
+
self.context.clear_imports()
|
|
251
|
+
|
|
252
|
+
# Generate code
|
|
253
|
+
code = generator_func(self.context)
|
|
254
|
+
|
|
255
|
+
# Write file with imports
|
|
256
|
+
imports = self.context.get_imports()
|
|
257
|
+
final_code = self.combine_imports_and_code(imports, code)
|
|
258
|
+
|
|
259
|
+
self.file_manager.write_file(file_path, final_code)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Error Context
|
|
263
|
+
```python
|
|
264
|
+
# Always provide context in error messages
|
|
265
|
+
def add_import_with_context(self, import_statement: str, file_context: str) -> None:
|
|
266
|
+
try:
|
|
267
|
+
self.import_collector.add_import(import_statement)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise ImportError(f"Failed to add import '{import_statement}' in {file_context}: {e}")
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Common Pitfalls
|
|
273
|
+
|
|
274
|
+
1. **Import Leakage**: Not clearing imports between files
|
|
275
|
+
2. **Path Confusion**: Using absolute paths instead of relative
|
|
276
|
+
3. **State Mutation**: Modifying context from multiple threads
|
|
277
|
+
4. **Memory Leaks**: Not cleaning up context after generation
|
|
278
|
+
|
|
279
|
+
## Best Practices
|
|
280
|
+
|
|
281
|
+
1. **One Context Per Session**: Create new context for each generation
|
|
282
|
+
2. **Clear Between Files**: Always clear imports between file generations
|
|
283
|
+
3. **Use Relative Imports**: Convert absolute imports to relative
|
|
284
|
+
4. **Error Context**: Include file/operation context in errors
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FileManager: Manages file operations for code generation.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for creating directories and writing
|
|
5
|
+
generated Python code to files, with appropriate logging for debugging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileManager:
|
|
13
|
+
"""
|
|
14
|
+
Manages file operations for the code generation process.
|
|
15
|
+
|
|
16
|
+
This class provides methods to write content to files and ensure directories
|
|
17
|
+
exist, with built-in logging for debugging purposes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def write_file(self, path: str, content: str) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Write content to a file, ensuring the parent directory exists.
|
|
23
|
+
|
|
24
|
+
This method also logs the file path and first 10 lines of content
|
|
25
|
+
to a debug log for troubleshooting purposes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
path: The absolute path to the file to write
|
|
29
|
+
content: The string content to write to the file
|
|
30
|
+
"""
|
|
31
|
+
self.ensure_dir(os.path.dirname(path))
|
|
32
|
+
|
|
33
|
+
# Log the file path and first 10 lines of content for debugging
|
|
34
|
+
debug_log_path = os.path.join(tempfile.gettempdir(), "pyopenapi_gen_file_write_debug.log")
|
|
35
|
+
with open(debug_log_path, "a") as debug_log:
|
|
36
|
+
debug_log.write(f"WRITE FILE: {path}\n")
|
|
37
|
+
for line in content.splitlines()[:10]:
|
|
38
|
+
debug_log.write(line + "\n")
|
|
39
|
+
debug_log.write("---\n")
|
|
40
|
+
|
|
41
|
+
# Write the content to the file
|
|
42
|
+
with open(path, "w") as f:
|
|
43
|
+
f.write(content)
|
|
44
|
+
|
|
45
|
+
def ensure_dir(self, path: str) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Ensure a directory exists, creating it if necessary.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: The directory path to ensure exists
|
|
51
|
+
"""
|
|
52
|
+
os.makedirs(path, exist_ok=True)
|