Skip to content

Load Smithy models and select the shapes to generate - #795

Draft
jonathan343 wants to merge 20 commits into
developfrom
python-codegen-model
Draft

jonathan343 wants to merge 20 commits into
developfrom
python-codegen-model

Conversation

@jonathan343

@jonathan343 jonathan343 commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #744; the base is add-smithy-python, so this diff is only the model-loading layer.

The CLI currently has no way to read a model. It now loads the Smithy JSON AST into an immutable shape index, resolves the service to generate, and selects the shapes to generate from it. Smithy serializes only what a shape introduces, so shapes using mixins arrive without their inherited members, traits, errors, and operations; we resolve mixins on load, merging each shape's apply statements while resolving it so that shapes inheriting through an intermediate mixin see its applied traits. Values inherited from a mixin are shared with every shape using it, so nested objects are frozen as read-only mappings and arrays as tuples, with JSONValue narrowed to the read-only abstract types so pyright rejects mutation statically.

We currently generate every shape in the model, which diverges from every other Smithy generator and fails on a published AWS model whose leaked, unconnected shapes collide with real ones. We now generate the resolved service's closure — named with --service when a model declares several — and report how many shapes were excluded. Without a service, the types artifact still generates every data shape and reports case-insensitive name conflicts with a removeUnusedShapes suggestion, since Smithy only guarantees unique names within a closure.

The loader also rejects what it previously accepted: an apply statement must target a member, a mixin must share the shape type of its user, and a member ID resolves only when the shape defines that member. --output is now required for direct invocation instead of defaulting. Parsing a 2.5 MB model is 35% faster and retains 23% less memory.

Test Plan

1586 unit tests with 100% coverage of the new modules; ruff and pyright --strict clean.

Loaded all 431 AWS service models in api-models-aws and 311 fixtures from smithy-model's test resources, checking each parsed model against its source AST. Fault injection confirmed the check caught unmerged mixins, dropped shapes, and unmerged applies. The 1.0 set shape and shape-level apply, which only resolves across documents, are unsupported.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@jonathan343
jonathan343 added this pull request to stack #796 September 13, 2026 06:47
Mark generation command examples as schematic and clarify option validation behavior. Exercise python -m smithy_python in a subprocess.
Document that --service is optional when the model contains a single
service, that both artifacts generate every data shape in the model
rather than the service closure, and that case-insensitive name
conflicts are a hard error. Note that run plugin env settings may back
command-line options.
Generating every shape in the model diverged from every other Smithy
generator and fails on a published AWS model whose leaked, unconnected
shapes collide with real ones. Document the service closure as the
default selection, with a note when shapes are left out, and keep the
whole-model behavior for the types artifact when no service is present.
Port the JSON AST model loader from the Python-native proof of concept
and add the selection rules described in the CLI design. The CLI now
parses the model before reporting that generation is unimplemented.

- Add ordered, immutable Model, Shape, Member, and ShapeID types that
  resolve prelude shapes on demand and honor apply statements.
- Add an optional --service option. A model with one service selects it
  automatically; multiple services require an explicit choice; the
  client artifact requires a service and types does not.
- Select every data shape in the model rather than the service closure,
  skipping prelude shapes, trait definitions, and mixins. Private shapes
  are generated only when reachable from another generated shape or an
  operation. Case-insensitive name conflicts fail generation.
- Distinguish invocation errors (exit 2) from model and generation
  failures (exit 1).
Smithy serializes only what a shape introduces, so shapes that use
mixins arrive without their inherited members, traits, errors, and
service operations, and traits added to inherited members arrive as
apply statements. Resolve mixins while loading, following the
specification's precedence and ordering rules, and apply traits in two
passes so that customizations of inherited members are honored.

Mixin services are abstract, so they are no longer service candidates
and cannot be selected with --service.
Generating every shape in the model diverged from every other Smithy
generator and failed on a published AWS model whose leaked, unconnected
shapes collide with real ones. Select the service closure when a service
is resolved, matching the existing SDK surface, and report how many
unconnected shapes were left out. Without a service, the types artifact
still generates every data shape and fails on case-insensitive name
conflicts, which Smithy only guarantees within a closure.

Synthesized prelude shapes now carry their real traits: defaults on the
Primitive* shapes and unitType on Unit.
Applying traits to inherited members after all mixins were resolved
meant a shape using an intermediate mixin copied that mixin's members
before its apply statements were merged. Merge each shape's applies as
part of resolving it, before anything inherits from it, and drop the
two-pass bookkeeping.
The model's containers are read-only views, but the JSON values inside
them are not copied. Say so rather than claiming full immutability.
Trait and attribute values inherited through mixins are shared between
the mixin and every shape that uses it, so a mutable nested value let one
shape's consumer corrupt its siblings. Freeze JSON objects as read-only
mappings and arrays as tuples when parsing, and narrow JSONValue to the
read-only abstract types so pyright rejects mutation statically as well.
Share one empty mapping across the whole model instead of allocating a
proxy per shape, layer the prelude under the shape index so a lookup is
a single step rather than a fallback chain, and replace the branch ladder
in Shape.references with a table naming the attributes that hold
references. Move the trait accessors that Member and Shape each defined
to a shared base, and the member scan that Shape and Model each
open-coded to Shape.get_member. Parsing a 2.5 MB model is about 35%
faster and retains 23% less memory as a result.

Collapse the CLI's two-layer invocation handoff into one request built by
one function, and route its output through helpers that own the program
prefix so it is spelled once.

Tighten the behaviors the loader was lenient about along the way: an
apply statement must target a member, since a shape's own traits are
serialized with its definition; a mixin must have the same shape type as
the shape using it; a list or map that inherits its members no longer
needs to restate them; and a member ID resolves only when the shape
actually defines that member.

Require --output for direct invocation rather than defaulting it, and
reject it alongside SMITHY_PLUGIN_DIR instead of silently letting the
plugin's directory win.
Model.get and Model.expect returned the containing shape when given a
member ID, so a caller resolving `ns#Shape$member` received a Shape whose
type and traits belonged to the container rather than the member. Reject
member IDs in the shape lookups and add Model.get_member and
Model.expect_member, which return the Member itself and reject shape IDs
symmetrically.
Any string was accepted as the `smithy` version, so a Smithy 1.0 AST
failed later with an unrelated error such as "Unsupported shape type
'set'". Check the major version while loading and report the version
along with how to fix it. Document the 2.x requirement in the CLI design.
A missing --model path was pre-checked and reported as an invocation
error with exit 2, while a file that existed but could not be read
raised OSError and exited 1. The design classifies I/O failures as 1,
and tools that distinguish usage errors from runtime failures, including
the Smithy CLI, treat a missing input file as the latter. Drop the
pre-check so every unreadable model exits 1 with a message that names
the path and the cause.

Also state in the design that model failures return 1, which the
implementation already did but the text left implicit.
Bring in the exit-code fix for unreadable model files. The CLI merged
cleanly; the two affected tests were rewritten in this branch's run_cli
style with the new exit code and message.
Base automatically changed from add-smithy-python to develop September 16, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant