Skip to content

Fix Python overload naming and interface-parameter dispatch - #193

Open
leileizhang (lei9444) wants to merge 7 commits into
mainfrom
lei9444-fix-python-overload-naming-and-dispatch
Open

leileizhang (lei9444) wants to merge 7 commits into
mainfrom
lei9444-fix-python-overload-naming-and-dispatch

Conversation

@lei9444

@lei9444 leileizhang (lei9444) commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Overloaded WinRT methods were grouped by their [Overload] ABI names plus suffix guesses, so the documented CLR names were often missing or split. In Windows.winmd, 566 of 623 overload groups were not fully under the documented name, and 39 lacked it entirely (e.g. StorageFile.copy_async, ToastNotifier.update, DecimalFormatter.format).
  • Overload dispatch checked interface-typed parameters with isinstance. Runtime-class objects never pass that check, so calls like DataWriter(InMemoryRandomAccessStream()) failed with "No matching constructor".

Key changes

  • The generator now plans each class's or interface's members once: public names, ordered candidates, private dispatch names, aliases and collision handling. Both the runtime .py and the .pyi generators use that plan. This refactor on its own leaves the generated output byte-identical.
  • Dispatch first tries the existing exact checks, then makes a second pass that accepts any object implementing the interface (via QueryInterface). A call that worked before still reaches the same overload.
  • Overloads are grouped by their documented CLR name, and all 623 groups now use it.
    • Previous names remain as compatibility aliases. Former standalone names still call their exact original overload.
    • 5 groups keep their old names because the documented name collides with another member.
  • Implementation-handler (*Handlers) names are unchanged.

Notes

  • This adds 455 public names and removes none.
  • In the stubs, overloads that map to an identical Python signature (e.g. Int64 and UInt64 both becoming int, with the same return type) are emitted once. If the parameters match but the return types differ, both overloads are kept, and the later one gets a targeted # type: ignore[overload-cannot-match].

Overload groups, public method names, private dispatch names, and
compatibility aliases were recomputed ad hoc at every call site:
`method_group_key` ran with different name sets for statics, instance
methods, constructors, required-interface wrappers, interface modules, and
stubs; `generate_*_method_group` re-derived the public name from the group
alone; constructors regrouped all statics to recover private names; and the
runtime `.py` and `.pyi` generators regrouped independently. Every rule change
had to be repeated in all of those places.

Add `member_plan`, which computes a `ScopePlan` (ordered candidates with
their private attribute names, group names, and aliases) once per generated
Python class, and render both the runtime and stub output from it.
Generators keep their established member order and emit a group at its first
method, so the generated output does not change.

Overload dispatch is emitted by one `emit_dispatch` for instance, static, and
constructor (`__new__`/`__init__`) dispatchers. Each parameter now has a
structured `ParamGuard` with a strict guard and an optional permissive guard
for a second pass that runs only after every strict guard failed. No guard is
permissive yet.

Verified: `.py`/`.pyi` generated for all 142 Windows.winmd namespaces are
byte-identical to the previous generator.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Generated runtime-class wrappers do not inherit interface wrapper classes, so
the `isinstance(x, IFoo)` guard of a known interface parameter rejected every
runtime-class object in overload dispatch. For example,
`DataWriter(InMemoryRandomAccessStream())` and `DataReader(stream)` raised
`TypeError: No matching constructor` even though the stubs accept them.

Give known interface parameters a permissive guard that also accepts any
projected object or raw DynWinRTValue supporting the interface through
QueryInterface, while keeping the `isinstance` check for interface wrappers
and Python implementations. Permissive guards only run in the dispatcher's
second pass, after every candidate's strict guards failed, so calls that
dispatch today keep their overload and only calls that raised TypeError can
now match. This covers constructors, static and factory methods, instance
methods, and interface wrappers.

Interface IID constants (`IID_ARG_<Namespace>_<Name>`) are emitted locally,
like the runtime-class ones, so guards do not import interface modules at
call time and also work for interfaces that are not generated. IID constants
are now collected from every wrapped parameter, including FillArray buffers,
which fixes an undefined `IID_ARG_*` reference in
`IIterator<RuntimeClass>.GetMany`.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WinMD gives all overloads of a method one CLR/MethodDef name and assigns each
ABI slot a unique `[Overload]` name. Python grouped on those unique ABI names,
then used suffix heuristics only when a base ABI name happened to exist. As a
result, hundreds of documented Python methods were incomplete or missing:
`StorageFile.CopyAsync` became three `copy_overload*` methods, for example.

Use each non-accessor method's snake-cased `raw_name` as the member plan's
primary key, with the established suffix heuristics on top. Accessors and
events keep their previous names. Static and instance scopes are planned
together so CLR names that collide with a property, event helper, generated
member, the other scope, or an existing name with different behavior fall
back to the prior name.

All old method names remain. A former standalone ABI method aliases its exact
private implementation, preserving its guard-free behavior and ABI slot;
former dispatch aliases continue to alias the dispatcher. The stubs declare
all metadata overloads. When two ABI overloads collapse to the same typed
Python signature, the later declaration gets a targeted
`overload-cannot-match` ignore so strict mypy accepts the metadata-exact count.

Regression coverage checks the representative SDK classes, every old public
name in those classes, collision fallbacks, exact compatibility-alias
behavior, strict mypy, and live Python calls for StorageFile,
RandomAccessStream, DecimalFormatter, XmlDocument, and Calendar.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Emit one stub declaration when multiple ABI candidates render to the same
full Python signature. This avoids duplicate IDE hover entries and needs no
mypy suppression for common Int64/UInt64-style projection collisions.

If parameter signatures are identical but return types differ, keep both
declarations and attach the targeted overload-cannot-match ignore to the later
one. Constructor overloads continue to deduplicate identical signatures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Mixed-language test coverage

Workflow status: ✅ Passed

Layer Lines Functions Branches/regions
Rust, including native .pyd/.node 86.59% 81.87% 86.26% regions
Python aggregate 71.6% n/a 37.59% branches
Python runtime 99.12% n/a 97.37% branches
Generated Python WinRT projections 71.15% n/a 30.75% branches
Generated Python WinRT implementations 71.81% n/a 46.37% branches
JavaScript aggregate 21.91% 25.18% 57.61% branches
JavaScript runtime 44.27% 45.76% 78.99% branches
Generated WinRT projections 22.8% 18.7% 54.84% branches
Generated WinRT implementations 45.99% 59.19% 60.97% branches
Generated Classic COM projections 11.88% 23.97% 53.4% branches

View workflow run and download full HTML/LCOV/XML reports

Do not treat identically shaped methods on different interfaces as the same
native overload when checking compatibility. When a documented CLR group
occupies an old public name with an identically shaped method from another
interface, emit a compatibility dispatcher that tries the exact previously
selected interface method first. It reuses the implementation defined by the
canonical CLR-name group, so each native method body is generated once, while
the newly documented interface method remains projected under its own name.

Keep one stub declaration per distinct full Python signature. Only identical
parameter signatures with different return types retain a second declaration
and targeted mypy suppression.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Determine compatibility from the actual reachable dispatch representative, not
raw group membership, so a same-group newcomer cannot sort ahead of the exact
interface method selected before CLR-name grouping.

When an implementation is shared with a compatibility dispatcher, assign it a
stable private attribute even if its canonical group has one candidate. The
canonical public name aliases that private implementation, preventing a Python
subclass override from changing the compatibility dispatcher's native target.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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