Skip to content

Type Python projection outputs as non-null by default - #190

Open
leileizhang (lei9444) wants to merge 12 commits into
mainfrom
lei9444-non-null-python-return-stubs
Open

leileizhang (lei9444) wants to merge 12 commits into
mainfrom
lei9444-non-null-python-return-stubs

Conversation

@lei9444

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

Copy link
Copy Markdown
Contributor

Problem

  • WinRT metadata carries no nullability, yet generated .pyi stubs typed almost every reference output as optional, forcing unnecessary guards.
  • Some APIs, collection values, and returned array elements really can be null, so the policy must preserve those cases and match runtime behavior.

Key changes

  • Adds one position-aware Python output-nullability policy and one recursive annotation renderer. The preparatory refactor produces identical output.
  • Defaults non-collection reference outputs to non-null, while keeping | None for IReference<T>, Try*, Object, delegates, and Windows SDK members documented as nullable.
  • Generates the documented-null table from MicrosoftDocs/winrt-api at a pinned commit, including required null-check wording such as sensor GetCurrentReading docs.
  • Types reference collection elements, map keys/values, key-value-pair members, and returned array elements as nullable; String, GUID, scalar, enum, struct, and unsupported async keys/elements remain non-null.
  • Safely converts supported None collection inputs to null WinRT values, and rejects invalid keys/elements with TypeError.
  • Models the two IMapView.Split out values as optional without widening other map-view results.
  • Adds generated-stub, strict mypy/pyright, matching-wheel native, E2E, snapshot, and sample coverage.

Notes

  • Runtime .py return annotations remain pessimistic; collection input signatures now reflect safe None writes.
  • General inputs, struct fields, implementation protocols, and callback parameters are unchanged.
  • The documented-null table covers Windows SDK APIs only, not Microsoft.*; pyright strict may flag now-redundant is None checks.

Output annotations (method and property results, out tuples, async
results, returned collection elements and collection-class items) were
rendered by four overlapping recursive helpers that each decided `| None`
from the type alone, with no notion of position, member facts or target
file.

Add python/nullability.rs with the AnnotationSurface (runtime .py vs .pyi
stub), OutputPosition and OutputSite model and output_admits_none, the only
place that decides whether an output admits None. Replace
py_return_type_safe, py_output_type, py_return_type, py_async_return_type,
py_array_return_type and py_collection_return_type with one recursive
py_output_annotation that separates spelling from nullability, and thread
the surface through the .py and .pyi call sites. Callback parameters keep
the legacy py_return_type_safe wrapper.

No generated output changes: a 40-namespace Windows.winmd corpus (26,940
.py and 27,022 .pyi files) is byte-identical before and after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WinRT metadata has no nullability and most APIs raise instead of returning
null, so the .pyi stubs now type received values as non-null by default,
like the JavaScript declarations: method and property results, async
results, out values, and returned collections with their elements, keys and
values.

The stub arm of output_admits_none keeps `| None` for IReference<T> values
everywhere, for the Return/OutParam/AsyncResult/activation results of
members whose CLR name is Try followed by an uppercase letter, for Object
values, and for delegate-typed values outside callback parameters. Runtime
.py annotations stay pessimistic, and inputs, struct fields, implementation
protocols and callback parameters are unchanged.

On a 40-namespace Windows.winmd corpus every .py file is byte-identical and
40,947 stub annotations lose `| None`, all at output positions. A natural
consumer script goes from 30 pyright / 29 mypy errors to 0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the `if x is None: raise` guards in five stock-Windows samples where
the values are now typed non-null, and keep the `Try*` guard in the OCR
sample. With the previous stubs the unguarded samples produce 27 mypy
--strict and 25 pyright errors; with the new stubs both report none, and
every sample still runs.

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.67% 81.96% 86.32% regions
Python aggregate 70.16% n/a 37.12% branches
Python runtime 99.12% n/a 97.37% branches
Generated Python WinRT projections 68.87% n/a 27.57% branches
Generated Python WinRT implementations 72.23% n/a 46.82% 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

Review of the stub policy found two gaps.

Some Windows SDK members are documented to return null, such as
Accelerometer.GetDefault and DispatcherQueue.GetForCurrentThread.
scripts/extract-null-results.py derives their doc comment IDs from
MicrosoftDocs/winrt-api at a pinned commit. It collects returns and
property-value sections that say the result can be null, and remarks that
say the member itself returns null, then applies reviewed overrides.
api-docs/windows-null-results.txt holds only the api-ids (933 members).
Metadata parsing records the fact on each method, using the interface
definition and the class chain that the documentation lists members under.
The stub policy keeps `| None` on those members' results.

Anyone can store null in a mutable IVector, IMap or observable collection,
so its elements, item positions and element-reading members (GetAt, Lookup,
GetMany) keep `| None`. Read-only views, iterators and arrays stay
non-null. The element rule is a separate branch of the policy.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Inherited MutableSequence and MutableMapping mutators (append, extend,
update, setdefault) take the element type of the collection base, so they
accept None only while mutable collection bases keep `| None`. Lock that in
with a strict mypy consumer of an observable vector and a JSON object.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pinned Windows API docs require callers to check several sensor
GetCurrentReading results for null, phrased as "must first check that the
value is not null". Teach the extractor to recognize that return-value
instruction without treating generic negations, input checks, or null
holders as nullable results. Regenerate the table and add focused extractor
and generated-stub regressions across all sensor variants.

A real native IVector containing a null slot exposes None through its vector,
view, and iterator wrappers, so reference collection reads are conservative
for every collection interface; map keys and value types stay non-null.

Align mutable collection writes with those annotations. A shared generated
helper converts None reference elements and map values to a null
DynWinRTValue, while None map keys and value-type elements raise TypeError.
Cover vector create/append/insert/index/slice/extend and map
update/setdefault/index writes with native read-back tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Collection item wrapping can recurse into IReference<T> inside iterable,
vector, map, or array inputs. Detect those nested shapes when deciding
whether to emit _dynwinrt_box_reference, with scalar nested collections as
a negative control. The generated-module regression checks the nested calls
and the runtime-backed collection test imports and invokes the emitted
helper.

Run that focused runtime test as a separate e2e-runtime step after installing
the matching Python wheel. Keep the existing implementation selector
unchanged so other focused probes can compose without conflicts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
IMapView.Split succeeds while returning two null interface pointers in the
runtime. Record that exact two-slot output contract in MethodMeta and keep
only those two generated Python results optional.

Map key nullability now follows the declared ABI type. Object, interface,
runtime-class, delegate, and parameterized-interface keys accept a real null
DynWinRTValue and read back None through mappings and key-value pairs. String,
Guid, scalar, enum, struct, and unsupported async key shapes remain non-null
and fail before type-specific conversion.

Cover generated Object and IStringable maps, String/Guid controls, ABC
contains/equality/hash behavior, key-value-pair iteration, view lookup, and
native Split returning (None, None).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The runtime array converters preserve null slots as None for runtime classes,
interfaces, Object, delegates, and parameterized references. Apply that same
reference-element policy to ordinary method/out/async array results while
keeping the array object itself and String/Guid/value/byte elements non-null.

Add generated source/stub and strict mypy/pyright regressions, plus matching
wheel native methods returning Resource[], IItem[], and Int32[] controls.
Route the live PropertyValue inspectable-array E2E through generated
IPropertyValue.get_inspectable_array rather than manual ABI decoding, and run
the focused native test in e2e-runtime.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve #188's released-input/runtime helpers and combined implementation
selector, alongside #190's required-wheel nullable collection and reference
array selectors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise generated IPropertyValue projection and implementation paths for value and nullable object arrays so the reference-array E2E contributes meaningful coverage to the generated module it introduced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Allocate the collection-item runtime helper through PythonSupportSymbol so metadata declarations keep their names while generated imports and every collection conversion share a collision-safe alias.

Add custom-WinMD packaged and standalone runtime regressions for vector/map None writes, factory rendering, strict typing, and an old-output TypeError negative control. Run the native selector against the matching wheel in CI.

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