Skip to content

Commit 91f5e28

Browse files
committed
wire native types generation into the CLI
1 parent 97a11c2 commit 91f5e28

14 files changed

Lines changed: 609 additions & 118 deletions

File tree

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
{
22
"files": {
3+
"README.md": "03146affd4cdec7675387b787ff852400641bad4c0cfc5e80bb860d576acee4b",
4+
"pyproject.toml": "8b49dfb34a75d368f1c264a8369fb4093af3b9618ec4a93173b28315578c808e",
5+
"src/example_types/__init__.py": "6d153749d98df8adfe6264fb74d642917cbf2d6922b9211719dee35483dab943",
6+
"src/example_types/_private/__init__.py": "5384bfdb2df380b6557cc7a71d16891415bccaa87699406e236f752c6415389f",
37
"src/example_types/_private/schemas.py": "d0f35e8979c4aa9d49a1d9c0c8efeb860420bc0ceab3ba3edcc9488c053e9110",
48
"src/example_types/models.py": "6042e2e6454b40fef929d96cb7608ae8c591ac7205577f14337435718bb7c077",
5-
"src/example_types/models.pyi": "bb3d59e91999a62438fce52fe3be551c63f998565e4f9640e4fb1e275b972b14"
9+
"src/example_types/models.pyi": "bb3d59e91999a62438fce52fe3be551c63f998565e4f9640e4fb1e275b972b14",
10+
"src/example_types/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
611
},
712
"version": 1
813
}

‎codegen-output-py/types/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- Code generated by smithy-python-codegen DO NOT EDIT. -->
2+
3+
## example_types Types Package
4+
5+
Example native Python codegen output
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Code generated by smithy-python-codegen DO NOT EDIT.
2+
3+
[project]
4+
name = "example_types"
5+
version = "1.0.0"
6+
description = "Example native Python codegen output"
7+
readme = "README.md"
8+
requires-python = ">=3.12"
9+
keywords = ["smithy", "example_types"]
10+
license = {text = "Apache-2.0"}
11+
dependencies = [
12+
"smithy-core~=0.10.0",
13+
]
14+
15+
[build-system]
16+
requires = [
17+
"hatchling",
18+
]
19+
build-backend = "hatchling.build"
20+
21+
[tool.hatch.build.targets.wheel]
22+
packages = [
23+
"src/example_types",
24+
]
25+
26+
[tool.pyright]
27+
typeCheckingMode = "strict"
28+
reportPrivateUsage = false
29+
reportUnusedFunction = false
30+
reportUnusedVariable = false
31+
reportUnnecessaryComparison = false
32+
reportUnusedClass = false
33+
enableExperimentalFeatures = true
34+
exclude = ["**/models.py"]
35+
36+
[tool.ruff]
37+
target-version = "py312"
38+
39+
[tool.ruff.lint]
40+
ignore = ["F841"]
41+
42+
[tool.ruff.format]
43+
skip-magic-trailing-comma = true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import annotations
2+
3+
__version__: str = "1.0.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from __future__ import annotations

‎codegen-output-py/types/src/example_types/py.typed‎

Whitespace-only changes.

‎packages/smithy-python/README.md‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
# smithy-python
22

3-
> [!WARNING]
4-
> This package is an experimental scaffold. It does not generate code yet. The
5-
> Java generator in the repository's `codegen` directory remains authoritative.
6-
7-
`smithy-python` will provide Python-native code generation for Smithy models.
8-
The initial command-line interface exposes the planned client and types generation
9-
commands so that their top-level shape can be developed independently from the
10-
generator implementation.
3+
`smithy-python` provides Python-native code generation for Smithy models.
4+
Standalone types generation is available; client generation remains under
5+
development.
116

127
```console
138
smithy-python generate client [OPTIONS]
149
smithy-python generate types [OPTIONS]
1510
```
1611

17-
After validating their invocation options, both generation commands currently exit
18-
with an error explaining that generation has not been implemented. The package is
19-
included in workspace builds to validate its packaging and entry points.
12+
`generate types` produces an installable package containing compact schemas, lazy
13+
runtime models, and a complete static typing surface. Run
14+
`smithy-python generate types --help` for artifact settings.
15+
16+
`generate client` currently exits with an error explaining that client generation
17+
has not been implemented.

‎packages/smithy-python/src/smithy_python/cli.py‎

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,20 @@
1212
from typing import BinaryIO, Final
1313

1414
from . import __version__
15+
from . import dependencies as _dependencies
16+
from . import files as _files
17+
from . import generation as _generation
18+
from . import model as _model
19+
from . import models as _models
20+
from . import selection as _selection
21+
from . import symbols as _symbols
22+
from . import types as _types
1523
from .environment import PluginEnvironment
16-
from .exceptions import CodegenError, InvalidInvocationError
17-
from .model import load_model
18-
from .selection import select_shapes
24+
from .exceptions import (
25+
CodegenError,
26+
InvalidInvocationError,
27+
SelectorSyntaxError,
28+
)
1929

2030
__all__ = ["main"]
2131

@@ -26,10 +36,8 @@
2636

2737
@dataclass(frozen=True, slots=True)
2838
class _Invocation:
29-
artifact: str
3039
model_source: bytes
3140
output_dir: Path
32-
environment: PluginEnvironment
3341

3442

3543
def main(
@@ -48,32 +56,29 @@ def main(
4856

4957
try:
5058
invocation = _resolve_invocation(args, environ=environ, stdin=stdin)
59+
model = _model.load_model(invocation.model_source)
60+
if args.artifact == "types":
61+
selection = _generate_types(args, invocation.output_dir, model)
62+
else:
63+
selection = _selection.select_shapes(
64+
model,
65+
service_id=args.service,
66+
require_service=True,
67+
)
5168
except InvalidInvocationError as error:
5269
sys.stderr.write(f"smithy-python: error: {error}\n")
5370
return 2
5471
except (CodegenError, OSError) as error:
5572
sys.stderr.write(f"smithy-python: error: {error}\n")
5673
return 1
5774

58-
try:
59-
model = load_model(invocation.model_source)
60-
selection = select_shapes(
61-
model,
62-
service_id=args.service,
63-
require_service=invocation.artifact == "client",
64-
)
65-
except InvalidInvocationError as error:
66-
sys.stderr.write(f"smithy-python: error: {error}\n")
67-
return 2
68-
except CodegenError as error:
69-
sys.stderr.write(f"smithy-python: error: {error}\n")
70-
return 1
71-
7275
if selection.excluded_count and selection.service is not None:
7376
sys.stderr.write(
7477
f"smithy-python: excluded {selection.excluded_count} eligible data shape(s) "
7578
f"outside the closure of service {selection.service.id}\n"
7679
)
80+
if args.artifact == "types":
81+
return 0
7782
sys.stderr.write(_GENERATION_NOT_IMPLEMENTED.format(artifact=args.artifact))
7883
return 1
7984

@@ -93,11 +98,36 @@ def _create_parser() -> argparse.ArgumentParser:
9398
generate = commands.add_parser("generate", help="Generate Python source")
9499
artifacts = generate.add_subparsers(dest="artifact", required=True)
95100
common = _common_artifact_options()
96-
for name, help_text in (
97-
("client", "Generate a client package"),
98-
("types", "Generate a standalone types package"),
99-
):
100-
artifacts.add_parser(name, help=help_text, parents=[common])
101+
artifacts.add_parser("client", help="Generate a client package", parents=[common])
102+
types = artifacts.add_parser(
103+
"types", help="Generate a standalone types package", parents=[common]
104+
)
105+
types.add_argument(
106+
"--package",
107+
required=True,
108+
help="Import package and distribution name for the generated artifact.",
109+
)
110+
types.add_argument(
111+
"--package-version",
112+
required=True,
113+
type=_non_empty,
114+
help="Version for the generated package.",
115+
)
116+
types.add_argument(
117+
"--package-description",
118+
help='Package description; defaults to "<package> types package".',
119+
)
120+
types.add_argument(
121+
"--selector",
122+
type=_parse_selector,
123+
default=_selection.Selector.identity(),
124+
help="Smithy selector used to narrow generated declaration roots.",
125+
)
126+
types.add_argument(
127+
"--generate-inputs-and-outputs",
128+
action="store_true",
129+
help="Include operation input and output structures as generation roots.",
130+
)
101131

102132
return parser
103133

@@ -175,8 +205,65 @@ def _resolve_invocation(
175205
raise InvalidInvocationError("Expected a Smithy JSON AST model")
176206

177207
return _Invocation(
178-
artifact=args.artifact,
179208
model_source=model_source,
180209
output_dir=output_dir,
181-
environment=environment,
182210
)
211+
212+
213+
def _generate_types(
214+
args: argparse.Namespace,
215+
output_dir: Path,
216+
model: _model.Model,
217+
) -> _selection.Selection:
218+
description = args.package_description or f"{args.package} types package"
219+
settings = _generation.TypesSettings(
220+
package=args.package,
221+
package_version=args.package_version,
222+
package_description=description,
223+
selector=args.selector,
224+
generate_inputs_and_outputs=args.generate_inputs_and_outputs,
225+
)
226+
selection = _selection.select_type_shapes(
227+
model,
228+
service_id=args.service,
229+
selector=settings.selector,
230+
generate_inputs_and_outputs=settings.generate_inputs_and_outputs,
231+
)
232+
try:
233+
symbols = _symbols.SymbolProvider(
234+
model,
235+
selection,
236+
package=settings.package,
237+
reserved_member_names=_models._model_reserved_member_names( # pyright: ignore[reportPrivateUsage]
238+
model
239+
),
240+
)
241+
except ValueError as error:
242+
raise InvalidInvocationError(str(error)) from error
243+
244+
dependencies = _dependencies.DependencySet()
245+
context = _generation.GenerationContext(
246+
model=model,
247+
selection=selection,
248+
settings=settings,
249+
symbols=symbols,
250+
plan=_generation.GenerationPlan.from_selection(model, selection, symbols),
251+
dependencies=dependencies,
252+
files=_files.FileManifest(dependencies),
253+
)
254+
_types.TypesGenerator().generate(context)
255+
context.files.commit(output_dir)
256+
return selection
257+
258+
259+
def _parse_selector(value: str) -> _selection.Selector:
260+
try:
261+
return _selection.Selector.parse(value)
262+
except SelectorSyntaxError as error:
263+
raise argparse.ArgumentTypeError(str(error)) from error
264+
265+
266+
def _non_empty(value: str) -> str:
267+
if not value:
268+
raise argparse.ArgumentTypeError("expected a non-empty value")
269+
return value

‎packages/smithy-python/src/smithy_python/symbols.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ def __init__(
5454
package: str,
5555
reserved_member_names: Callable[[Shape], Collection[str]] | None = None,
5656
) -> None:
57-
if any(
57+
if not _DISTRIBUTION_NAME.fullmatch(package) or any(
5858
not part.isidentifier() or unicodedata.normalize("NFKC", part) in _KEYWORDS
5959
for part in package.split(".")
6060
):
61-
raise ValueError(f"Invalid Python package name: {package!r}")
61+
raise ValueError(
62+
f"Invalid Python package or distribution name: {package!r}"
63+
)
6264
self._model = model
6365
self._selection = selection
6466
self._package = package
@@ -289,6 +291,7 @@ def _shape(self, shape_id: ShapeId | str) -> Shape:
289291
_ENUMS = frozenset({ShapeType.ENUM, ShapeType.INT_ENUM})
290292
_RESERVED_DECLARATIONS = frozenset({"ServiceError"})
291293
_UNIT = ShapeId("smithy.api", "Unit")
294+
_DISTRIBUTION_NAME = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?")
292295
_ACRONYM_BOUNDARY = re.compile(r"([A-Z]+)([A-Z][a-z])")
293296
_WORD_BOUNDARY = re.compile(r"([a-z0-9])([A-Z])")
294297

0 commit comments

Comments
 (0)