Skip to content

feat: inheritance mode, body-field descriptions, keep soft keywords - #1

Closed
K1rL3s wants to merge 2 commits into
mainfrom
feat/inheritance-and-body-descriptions
Closed

feat: inheritance mode, body-field descriptions, keep soft keywords#1
K1rL3s wants to merge 2 commits into
mainfrom
feat/inheritance-and-body-descriptions

Conversation

@K1rL3s

@K1rL3s K1rL3s commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Adds --inheritance (render allOf: [{$ref: Base}, ...] as a real base class),
IRBodyField.description, and stops suffixing soft keywords like type.

See the commits for detail: 4cdd787 is the feature, da4b94c is the review pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PZwQzJPfNk2RLwZ8X84vbD

K2rL3s and others added 2 commits July 23, 2026 16:34
Three additions, driven by generating a hand-written client's shape:

- `--inheritance` renders `allOf: [{$ref: Base}, ...]` as a real base class
  instead of merging the parent's fields into every subtype. A discriminated
  base stays a model (its own properties survive) and its mapped subtypes
  inherit from it, re-declaring only the discriminator tag. Declarations are
  emitted parent-first so a `class Sub(Base)` statement resolves, and model
  constructors become keyword-only because a subclass may pin an inherited
  field to a default while adding required fields of its own.

- `IRBodyField.description` carries the schema description of a spread request
  body field, which was silently dropped. `IRParameter` already had it.

- `sanitize_identifier` no longer suffixes soft keywords: `type` is a legal
  attribute name and an extremely common spec field, so `type_` was noise.
  `_` stays reserved.

Verified on a real 130-schema spec: both file layouts x all three serializers
generate code that passes `ruff --isolated` and `mypy --strict`.
Review of 4cdd787 found the inheritance path silently changing decoded data
in several shapes. Fixes, most severe first:

- A `oneOf` + `discriminator` union holder is no longer force-built as a class.
  It declares no properties, so `--inheritance` produced `class Button: pass`
  and every `list[Button]` payload decoded into it, dropping each variant's
  fields. It stays a union alias; only bases with their own properties become
  classes. Verified: pydantic again decodes `CallbackButton(payload=...)` and
  `isinstance(x, ButtonBase)` still holds.

- The pinned discriminator tag is reserved against the subtype's existing field
  names. A sibling property whose wire name only differs in case (`Type` vs
  `type`) snake-cases to the same identifier, and the two class attributes
  collapsed into one -- destroying the tag, with ruff reporting nothing.

- A subtype that restates an inherited property only to attach prose, or to
  relax it to nullable, now inherits it instead of emitting an override.
  `v: str | None` over the base's `v: str` is an `[assignment]` error under
  `mypy --strict`, so `--inheritance --check` failed on ordinary specs. Genuine
  narrowings (a `Literal` tag over a `str`) are kept.

- The base class is resolved from the schema, not from the half-built
  `_declarations` registry. A base whose own body refers back to its subtype
  (a recursive hierarchy) has no entry yet, so inheritance silently degraded to
  a field merge based on nothing but graph traversal order.

- Keyword-only constructors are limited to the models in a hierarchy. One
  `allOf` subtype used to flip every model in the package, breaking positional
  construction for models with no relation to it.

- A discriminated base kept as a class emits its mapping as a comment. No
  serializer resolves a subtype from a base-class annotation on its own, and
  `IRModel.discriminator` was read by nobody, so that was dropped on the floor.

- `IRBodyField.description` (and `IRParameter.description`) now reach the
  generated code as PEP 258 attribute docstrings. Both were carried through the
  IR and rendered nowhere.

Also: `IRModel.base` -> `base_model`, so it can't be confused with
`IREnum.base` (the enum's "str"/"int" value type); soft keywords use an
allow-list over `keyword.issoftkeyword` so a future Python's new soft keyword
stays guarded; `_ordered_declarations` breaks an inheritance cycle instead of
emitting a class before its base; the two discriminator-mapping loops share one
helper; `_inherited_ref` is computed once and passed down.

Verified: both file layouts x all three serializers generate code that passes
`ruff check --isolated` and `mypy --strict` (only the pre-existing
`BaseMethod.__init_subclass__` no-untyped-call remains).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZwQzJPfNk2RLwZ8X84vbD
@K1rL3s

K1rL3s commented Jul 23, 2026

Copy link
Copy Markdown
Owner Author

Superseded: the real PR is goduni#5 (same branch). Opened this by mistake because gh pr list defaulted to the fork, where no PR existed. The review write-up now lives on the upstream PR.

@K1rL3s K1rL3s closed this Jul 23, 2026
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.

2 participants