Skip to content

fix(codegen/python): disambiguate namespace/root leaf-name collisions (#161)#162

Merged
hardbyte merged 3 commits into
mainfrom
fix/python-namespace-leaf-collision
Jun 2, 2026
Merged

fix(codegen/python): disambiguate namespace/root leaf-name collisions (#161)#162
hardbyte merged 3 commits into
mainfrom
fix/python-namespace-leaf-collision

Conversation

@hardbyte
Copy link
Copy Markdown
Contributor

@hardbyte hardbyte commented Jun 2, 2026

Fixes #161.

Problem

In the multi-file (package) Python client, a root type and a same-leaf type under a sub-namespace (e.g. a root IfConflictOnUpdate and a nomatches::IfConflictOnUpdate) collide on a bare public name. Each namespace module emitted both:

from .._types import IfConflictOnUpdate          # the root type
...
class NomatchesIfConflictOnUpdate(BaseModel, Generic[C]): ...
class NomatchesUpdateRequest(BaseModel):
    conflict: IfConflictOnUpdate[SomeMatch]       # bare → resolves to the rebind below
IfConflictOnUpdate = NomatchesIfConflictOnUpdate  # rebind shadows the import

So _types.IfConflictOnUpdate is not nomatches.IfConflictOnUpdate — two distinct classes sharing a name — and a field annotated against one rejects an instance of the other at runtime with a confusingly self-identical pydantic ValidationError. Reproduces identically on 0.17.4 and current main (the recent namespace-import work did not touch the duplicate-definition logic).

Fix

Collision-aware naming in reflectapi/src/codegen/python.rs. A leaf "collides" when a sub-namespace defines a type whose short (prefix-stripped) name equals a root type imported into every module. For collisions only, the generator now:

  • keeps the short name (IfConflictOnUpdate) bound to the imported root type — suppresses the colliding bare alias;
  • exposes the namespace-local type under its disambiguated flat name (NomatchesIfConflictOnUpdate), and rewrites references to it — in its own field annotations and in cross-namespace references from other modules — to that name.

Result: one Python class per logical type, so model.<X> resolves consistently. The collision key is the io-qualified type name, so genuinely-distinct input::/output:: projections are never collapsed. It is a strict no-op for schemas without such collisions — the committed demo client is byte-identical.

Tests

  • New regression test test_python_namespace_leaf_collision_no_shadowing (multi-file package path): asserts no colliding bare rebind, the namespace field uses the disambiguated name, and a root cross-namespace ref is qualified with the disambiguated name. Fails before the fix, passes after.
  • Verified at runtime against reflectapi-runtime + pydantic 2.13: _types.IfConflictOnUpdate is nomatches.IfConflictOnUpdate is now True, the namespace-local type stays distinct, and a request/response round-trip validates without the ValidationError.
  • cargo fmt --all, clippy -D warnings, build --workspace, demo + CLI suites pass (Python snapshots require ruff installed locally).

Docs

  • CHANGELOG entry under Unreleased.
  • Python client naming note in docs/src/clients/README.md.

…#161)

A root type and a same-leaf type under a sub-namespace each emitted a class
plus a bare `<Leaf> = <NsLeaf>` alias that shadowed the `from .._types import
<Leaf>` the module also imported, so `_types.<Leaf>` and `<ns>.<Leaf>` resolved
to two distinct classes and field annotations bound to the wrong one. Resolve
each logical type to a single class: keep the short name bound to the imported
root type and reference the namespace-local type by its disambiguated flat name.
No-op for schemas without such collisions.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

📖 Documentation Preview: https://reflectapi-docs-preview-pr-162.partly.workers.dev

Updated automatically from commit b261956

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98d0de222a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread reflectapi/src/codegen/python.rs Outdated
hardbyte added 2 commits June 2, 2026 12:07
A formatter (ruff/prettier/rustfmt) that rejects its input can exit before
draining stdin, making `write_all` fail with `BrokenPipe` and masking the real
exit status. This made `python_format_reports_ruff_failures` flaky under CI
load. Swallow only `BrokenPipe` on the stdin write so `wait_with_output`
reports the actual failure.
module_aliases() emits both a prefix-stripped alias and a Rust-leaf alias, and
either can shadow a same-named imported root type. The collision registry only
inspected the stripped form, so a namespaced `OrderInsertData` whose flat name
de-stutters to `MyapiOrderInsertData` slipped through: its stripped alias is the
harmless `InsertData`, but the Rust-leaf alias `OrderInsertData =
MyapiOrderInsertData` still shadowed the imported root. Build the registry from
the module's actual aliases so both forms are covered, and add a regression test.

Reported by Codex review on #162.
@hardbyte hardbyte merged commit 97d9b27 into main Jun 2, 2026
6 checks passed
@hardbyte hardbyte deleted the fix/python-namespace-leaf-collision branch June 2, 2026 01:01
@hardbyte hardbyte mentioned this pull request Jun 2, 2026
hardbyte added a commit that referenced this pull request Jun 2, 2026
Bump all workspace crates and the Python runtime to 0.17.5 and promote the Unreleased CHANGELOG section. Includes the #161 namespace/root leaf-name collision fix (#162).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant