Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
run: |
make build-py

- name: Check native Python codegen output
- name: Check Python codegen output
if: ${{ matrix.python-version == '3.12' }}
run: |
make generate-codegen-output-py
Expand Down
7 changes: 6 additions & 1 deletion codegen-output-py/types/.smithy-python-manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{
"files": {
"README.md": "b070865ebb1dc46192ea8a45f0961e1b0f812dfccd069537cfc04311b3327660",
"pyproject.toml": "c419d5bcb7e49329ae50cab3b1a766446d3d14d40a422328788f93e8b2bbeb2b",
"src/example_types/__init__.py": "6d153749d98df8adfe6264fb74d642917cbf2d6922b9211719dee35483dab943",
"src/example_types/_private/__init__.py": "5384bfdb2df380b6557cc7a71d16891415bccaa87699406e236f752c6415389f",
"src/example_types/_private/schemas.py": "d0f35e8979c4aa9d49a1d9c0c8efeb860420bc0ceab3ba3edcc9488c053e9110",
"src/example_types/models.py": "6042e2e6454b40fef929d96cb7608ae8c591ac7205577f14337435718bb7c077",
"src/example_types/models.pyi": "bb3d59e91999a62438fce52fe3be551c63f998565e4f9640e4fb1e275b972b14"
"src/example_types/models.pyi": "bb3d59e91999a62438fce52fe3be551c63f998565e4f9640e4fb1e275b972b14",
"src/example_types/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"version": 1
}
5 changes: 5 additions & 0 deletions codegen-output-py/types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- Code generated by smithy-python-codegen DO NOT EDIT. -->

## example_types Types Package

Example Python codegen output
43 changes: 43 additions & 0 deletions codegen-output-py/types/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Code generated by smithy-python-codegen DO NOT EDIT.

[project]
name = "example_types"
version = "1.0.0"
description = "Example Python codegen output"
readme = "README.md"
requires-python = ">=3.12"
keywords = ["smithy", "example_types"]
license = {text = "Apache-2.0"}
dependencies = [
"smithy-core~=0.10.0",
]

[build-system]
requires = [
"hatchling",
]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = [
"src/example_types",
]

[tool.pyright]
typeCheckingMode = "strict"
reportPrivateUsage = false
reportUnusedFunction = false
reportUnusedVariable = false
reportUnnecessaryComparison = false
reportUnusedClass = false
enableExperimentalFeatures = true
exclude = ["**/models.py"]

[tool.ruff]
target-version = "py312"

[tool.ruff.lint]
ignore = ["F841"]

[tool.ruff.format]
skip-magic-trailing-comma = true
3 changes: 3 additions & 0 deletions codegen-output-py/types/src/example_types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from __future__ import annotations

__version__: str = "1.0.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from __future__ import annotations
Empty file.
20 changes: 9 additions & 11 deletions packages/smithy-python/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
# smithy-python

> [!WARNING]
> This package is an experimental scaffold. It does not generate code yet. The
> Java generator in the repository's `codegen` directory remains authoritative.

`smithy-python` will provide Python-native code generation for Smithy models.
The initial command-line interface exposes the planned client and types generation
commands so that their top-level shape can be developed independently from the
generator implementation.
`smithy-python` provides Python-native code generation for Smithy models.
Standalone types generation is available; client generation remains under
development.

```console
smithy-python generate client [OPTIONS]
smithy-python generate types [OPTIONS]
```

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

`generate client` currently exits with an error explaining that client generation
has not been implemented.
4 changes: 3 additions & 1 deletion packages/smithy-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ classifiers = [
"Programming Language :: Python :: Free Threading :: 2 - Beta",
"Topic :: Software Development :: Code Generators",
]
dependencies = []
dependencies = [
"packaging",
]

[project.scripts]
smithy-python = "smithy_python.cli:main"
Expand Down
137 changes: 114 additions & 23 deletions packages/smithy-python/src/smithy_python/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@
from pathlib import Path
from typing import BinaryIO, Final

from packaging.version import InvalidVersion, Version

from . import __version__
from . import dependencies as _dependencies
from . import exceptions as _exceptions
from . import files as _files
from . import generation as _generation
from . import model as _model
from . import models as _models
from . import selection as _selection
from . import symbols as _symbols
from . import types as _types
from .environment import PluginEnvironment
from .exceptions import CodegenError, InvalidInvocationError
from .model import load_model
Expand All @@ -26,10 +37,8 @@

@dataclass(frozen=True, slots=True)
class _Invocation:
artifact: str
model_source: bytes
output_dir: Path
environment: PluginEnvironment


def main(
Expand All @@ -48,24 +57,19 @@ def main(

try:
invocation = _resolve_invocation(args, environ=environ, stdin=stdin)
except InvalidInvocationError as error:
sys.stderr.write(f"smithy-python: error: {error}\n")
return 2
except (CodegenError, OSError) as error:
sys.stderr.write(f"smithy-python: error: {error}\n")
return 1

try:
model = load_model(invocation.model_source)
selection = select_shapes(
model,
service_id=args.service,
require_service=invocation.artifact == "client",
)
if args.artifact == "types":
selection = _generate_types(args, invocation.output_dir, model)
else:
selection = select_shapes(
model,
service_id=args.service,
require_service=True,
)
except InvalidInvocationError as error:
sys.stderr.write(f"smithy-python: error: {error}\n")
return 2
except CodegenError as error:
except (CodegenError, OSError) as error:
sys.stderr.write(f"smithy-python: error: {error}\n")
return 1

Expand All @@ -74,6 +78,8 @@ def main(
f"smithy-python: excluded {selection.excluded_count} eligible data shape(s) "
f"outside the closure of service {selection.service.id}\n"
)
if args.artifact == "types":
return 0
sys.stderr.write(_GENERATION_NOT_IMPLEMENTED.format(artifact=args.artifact))
return 1

Expand All @@ -93,11 +99,36 @@ def _create_parser() -> argparse.ArgumentParser:
generate = commands.add_parser("generate", help="Generate Python source")
artifacts = generate.add_subparsers(dest="artifact", required=True)
common = _common_artifact_options()
for name, help_text in (
("client", "Generate a client package"),
("types", "Generate a standalone types package"),
):
artifacts.add_parser(name, help=help_text, parents=[common])
artifacts.add_parser("client", help="Generate a client package", parents=[common])
types = artifacts.add_parser(
"types", help="Generate a standalone types package", parents=[common]
)
types.add_argument(
"--package",
required=True,
help="Import package and distribution name for the generated artifact.",
)
types.add_argument(
"--package-version",
required=True,
type=_package_version,
help="Version for the generated package.",
)
types.add_argument(
"--package-description",
help='Package description; defaults to "<package> types package".',
)
types.add_argument(
"--selector",
type=_parse_selector,
default=_selection.Selector.identity(),
help="Smithy selector used to narrow generated declaration roots.",
)
types.add_argument(
"--generate-inputs-and-outputs",
action="store_true",
help="Include operation input and output structures as generation roots.",
)

return parser

Expand Down Expand Up @@ -175,8 +206,68 @@ def _resolve_invocation(
raise InvalidInvocationError("Expected a Smithy JSON AST model")

return _Invocation(
artifact=args.artifact,
model_source=model_source,
output_dir=output_dir,
environment=environment,
)


def _generate_types(
args: argparse.Namespace,
output_dir: Path,
model: _model.Model,
) -> _selection.Selection:
description = args.package_description or f"{args.package} types package"
settings = _generation.TypesSettings(
package=args.package,
package_version=args.package_version,
package_description=description,
selector=args.selector,
generate_inputs_and_outputs=args.generate_inputs_and_outputs,
)
selection = _selection.select_type_shapes(
model,
service_id=args.service,
selector=settings.selector,
generate_inputs_and_outputs=settings.generate_inputs_and_outputs,
)
try:
symbols = _symbols.SymbolProvider(
model,
selection,
package=settings.package,
reserved_member_names=_models._model_reserved_member_names( # pyright: ignore[reportPrivateUsage]
model
),
)
except ValueError as error:
raise InvalidInvocationError(str(error)) from error

dependencies = _dependencies.DependencySet()
context = _generation.GenerationContext(
model=model,
selection=selection,
settings=settings,
symbols=symbols,
plan=_generation.GenerationPlan.from_selection(model, selection, symbols),
dependencies=dependencies,
files=_files.FileManifest(dependencies),
)
_types.TypesGenerator().generate(context)
context.files.commit(output_dir)
return selection


def _parse_selector(value: str) -> _selection.Selector:
try:
return _selection.Selector.parse(value)
except _exceptions.SelectorSyntaxError as error:
raise argparse.ArgumentTypeError(str(error)) from error


def _package_version(value: str) -> str:
if not value:
raise argparse.ArgumentTypeError("expected a non-empty value")
try:
return str(Version(value))
except InvalidVersion as error:
raise argparse.ArgumentTypeError(str(error)) from error
7 changes: 5 additions & 2 deletions packages/smithy-python/src/smithy_python/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ def __init__(
package: str,
reserved_member_names: Callable[[Shape], Collection[str]] | None = None,
) -> None:
if any(
if not _DISTRIBUTION_NAME.fullmatch(package) or any(
not part.isidentifier() or unicodedata.normalize("NFKC", part) in _KEYWORDS
for part in package.split(".")
):
raise ValueError(f"Invalid Python package name: {package!r}")
raise ValueError(
f"Invalid Python package or distribution name: {package!r}"
)
self._model = model
self._selection = selection
self._package = package
Expand Down Expand Up @@ -289,6 +291,7 @@ def _shape(self, shape_id: ShapeId | str) -> Shape:
_ENUMS = frozenset({ShapeType.ENUM, ShapeType.INT_ENUM})
_RESERVED_DECLARATIONS = frozenset({"ServiceError"})
_UNIT = ShapeId("smithy.api", "Unit")
_DISTRIBUTION_NAME = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?")
_ACRONYM_BOUNDARY = re.compile(r"([A-Z]+)([A-Z][a-z])")
_WORD_BOUNDARY = re.compile(r"([a-z0-9])([A-Z])")

Expand Down
Loading
Loading