From d20430082165d6b592db4a8ebf4b4294346eafe4 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 12:33:23 +0800 Subject: [PATCH 01/12] Route Python output annotations through one nullability policy 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> --- .../src/codegen/winrt/python/method.rs | 16 +- .../src/codegen/winrt/python/mod.rs | 1 + .../src/codegen/winrt/python/nullability.rs | 164 +++++ .../src/codegen/winrt/python/stub_helpers.rs | 16 +- .../src/codegen/winrt/python/stubs.rs | 74 +-- .../src/codegen/winrt/python/type_helpers.rs | 585 +++++++++++------- 6 files changed, 590 insertions(+), 266 deletions(-) create mode 100644 tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 3cb9aea5..caf2071b 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -10,13 +10,14 @@ use crate::codegen::winrt::shared::imports::{ }; use super::naming::{PythonProjectionContext, PythonTypeIdentity, to_snake_case}; +use super::nullability::AnnotationSurface; use super::signature::{ py_convert_return, py_runtime_named_symbol, py_runtime_symbol, py_type_guard, py_wrap_arg, py_wrap_async, py_wrap_async_with_converters, }; use super::type_helpers::{ method_pydoc, py_delegate_callable_type, py_factory_return_type, py_method_abi_output_count, - py_method_outputs, py_method_return_type, py_output_type, py_param_list, + py_method_outputs, py_method_return_type, py_param_list, py_property_type, }; fn is_delegate_type(typ: &TypeMeta, context: &PythonProjectionContext) -> bool { @@ -282,7 +283,12 @@ fn generate_factory_method_invoke_named( let in_params = get_in_params(method); let py_params = py_param_list(&in_params, context); - let return_py_type = py_factory_return_type(&context.class_name(class), method, context); + let return_py_type = py_factory_return_type( + &context.class_name(class), + method, + AnnotationSurface::Runtime, + context, + ); let mut out = String::new(); let method_name = name_override @@ -360,7 +366,7 @@ fn generate_static_method_invoke_named( let in_params = get_in_params(method); let py_params = py_param_list(&in_params, context); - let py_return = py_method_return_type(method, context); + let py_return = py_method_return_type(method, AnnotationSurface::Runtime, context); let mut out = String::new(); let iface_symbol = context.reference_name(&iface.type_identity()); @@ -780,7 +786,7 @@ pub(crate) fn generate_method_body( if method.is_property_getter && in_params.is_empty() { let prop_name = to_snake_case(method.name.strip_prefix("get_").unwrap_or(&method.name)); let py_return = return_type - .map(|typ| py_output_type(typ, context)) + .map(|typ| py_property_type(typ, AnnotationSurface::Runtime, context)) .unwrap_or_else(|| "None".to_string()); out.push_str(" @_property\n"); out.push_str(&format!(" def {}(self) -> {}:\n", prop_name, py_return)); @@ -830,7 +836,7 @@ pub(crate) fn generate_method_body( )); } else { let py_params = py_param_list(&in_params, context); - let py_return = py_method_return_type(method, context); + let py_return = py_method_return_type(method, AnnotationSurface::Runtime, context); let method_name = name_override .map(|s| s.to_string()) .unwrap_or_else(|| to_snake_case(&method.name)); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs index 84a04006..d2194c4e 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs @@ -8,6 +8,7 @@ mod implementation; pub(crate) mod method; pub(crate) mod naming; mod native_types; +pub(crate) mod nullability; pub(crate) mod overloads; mod shared; pub(crate) mod signature; diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs new file mode 100644 index 00000000..7b5e1536 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The one policy deciding whether a projected output annotation admits +//! `None`. +//! +//! WinRT metadata carries no nullability, so whether a consumer-facing value +//! is annotated `T | None` depends on the value's type, the position it is +//! received at, facts about the member producing it, and the generated file +//! the annotation is rendered into. Renderers in `type_helpers` spell the +//! non-null type expression; only [`output_admits_none`] decides whether +//! `| None` is appended. + +use crate::codegen::winrt::shared::imports::ireference_inner_type; +use crate::meta::MethodMeta; +use crate::types::TypeMeta; + +use super::naming::PythonProjectionContext; + +/// The generated artifact an annotation is rendered into. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AnnotationSurface { + /// Inline annotations of runtime `.py` modules. `typing.get_type_hints()` + /// exposes them, and checkers read them when stubs are not generated. + Runtime, + /// `.pyi` stubs read by type checkers. + Stub, +} + +/// Where the consumer receives a value from the projection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum OutputPosition { + /// Return value of a method. + Return, + /// Out value in a method's result tuple. + OutParam, + /// Value read from a property getter. + Property, + /// Completed value of an async operation. + AsyncResult, + /// Progress value reported by an async operation. + AsyncProgress, + /// Element, key or value read from a collection or an array. + CollectionElement, + /// Sender or argument the runtime passes to a consumer callback. + CallbackParam, + /// Instance created by an activation factory. Activation reports failure + /// by raising, so the instance is never null unless the factory follows + /// the `Try*` pattern. + Activation, +} + +/// A position plus the facts about the member producing the value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct OutputSite { + pub(crate) position: OutputPosition, + /// The member follows the `Try*` pattern, so a null result is part of + /// its contract ("not found", "could not parse"). + pub(crate) try_method: bool, +} + +impl OutputSite { + pub(crate) fn of(position: OutputPosition) -> Self { + Self { + position, + try_method: false, + } + } + + pub(crate) fn for_method(method: &MethodMeta, position: OutputPosition) -> Self { + Self { + position, + try_method: is_try_method(method), + } + } + + /// The site of a value nested in this one, such as an async result or a + /// collection element. Member facts carry over; the policy decides where + /// they apply. + pub(crate) fn nested(self, position: OutputPosition) -> Self { + Self { position, ..self } + } +} + +/// `TryParse`, `TryGetItemAsync`, ...: the CLR name is `Try` followed by an +/// uppercase letter. +pub(crate) fn is_try_method(method: &MethodMeta) -> bool { + method + .raw_name + .strip_prefix("Try") + .and_then(|rest| rest.chars().next()) + .is_some_and(|next| next.is_ascii_uppercase()) +} + +/// Whether the runtime converts a null ABI value of `typ` to `None`: +/// interface pointers (objects, classes, interfaces, delegates and +/// parameterized interfaces, including `IReference`). Value types, +/// strings, arrays and async operations never project as `None`. +pub(crate) fn may_project_none(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Object + | TypeMeta::Delegate { .. } + | TypeMeta::RuntimeClass { .. } + | TypeMeta::Interface { .. } + | TypeMeta::Parameterized { .. } + ) +} + +/// Whether the annotation of `typ` received at `site` admits `None` on +/// `surface`. This is the only place that makes that decision for outputs. +pub(crate) fn output_admits_none( + typ: &TypeMeta, + site: OutputSite, + surface: AnnotationSurface, + _context: &PythonProjectionContext, +) -> bool { + if ireference_inner_type(typ).is_some() { + return true; + } + if !may_project_none(typ) { + return false; + } + match (surface, site.position) { + (_, OutputPosition::Activation) => false, + (AnnotationSurface::Runtime, _) => true, + (AnnotationSurface::Stub, _) => true, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn method(raw_name: &str) -> MethodMeta { + MethodMeta { + name: raw_name.into(), + raw_name: raw_name.into(), + ..Default::default() + } + } + + #[test] + fn try_methods_require_an_uppercase_continuation() { + assert!(is_try_method(&method("TryParse"))); + assert!(is_try_method(&method("TryGetItemAsync"))); + assert!(!is_try_method(&method("Try"))); + assert!(!is_try_method(&method("Trying"))); + assert!(!is_try_method(&method("GetTryCount"))); + assert!(!is_try_method(&method("get_TryCount"))); + } + + #[test] + fn nested_sites_keep_member_facts() { + let site = OutputSite::for_method(&method("TryGetItemAsync"), OutputPosition::Return); + assert_eq!( + site.nested(OutputPosition::AsyncResult), + OutputSite { + position: OutputPosition::AsyncResult, + try_method: true, + } + ); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 316b4319..b1eaee21 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -9,10 +9,12 @@ use crate::types::{FieldMeta, TypeMeta}; use super::naming::{PythonProjectionContext, PythonSymbol, STRUCT_SYMBOLS, to_snake_case}; use super::native_types::{FoundationType, foundation_type}; +use super::nullability::AnnotationSurface; use super::structs::{py_struct_field_read_type, py_struct_field_type}; use super::type_helpers::{ - method_pydoc_with_indent, py_delegate_callable_type, py_factory_return_type, - py_method_return_type, py_output_type, py_param_list, py_param_type_safe, + method_pydoc_with_indent, py_collection_item_type, py_delegate_callable_type, + py_factory_return_type, py_method_return_type, py_param_list, py_param_type_safe, + py_property_type, }; use crate::codegen::winrt::shared::imports::ireference_inner_type; @@ -274,7 +276,7 @@ pub(super) fn emit_method_stub_named( if method.is_property_getter && in_params.is_empty() { let prop_name = to_snake_case(method.name.strip_prefix("get_").unwrap_or(&method.name)); let py_return = return_type - .map(|typ| py_output_type(typ, context)) + .map(|typ| py_property_type(typ, AnnotationSurface::Stub, context)) .unwrap_or_else(|| "None".to_string()); out.push_str(&format!("{indent}@builtins.property\n")); emit_documented_stub( @@ -317,7 +319,7 @@ pub(super) fn emit_method_stub_named( } } else { let py_params = py_param_list(&in_params, context); - let py_return = py_method_return_type(method, context); + let py_return = py_method_return_type(method, AnnotationSurface::Stub, context); let method_name = name_override .map(str::to_string) .unwrap_or_else(|| to_snake_case(&method.name)); @@ -334,7 +336,7 @@ pub(super) fn emit_method_stub_named( && method_name == "append" && in_params.first().is_some_and(|param| { py_param_type_safe(¶m.typ, context) - != super::type_helpers::py_return_type_safe(Some(¶m.typ), context) + != py_collection_item_type(¶m.typ, AnnotationSurface::Stub, context) }) { " # type: ignore[override, unused-ignore]" } else { @@ -372,9 +374,9 @@ pub(super) fn emit_static_method_stub_named( let py_params = py_param_list(&in_params, context); let py_return = if is_factory { - py_factory_return_type(class_name, method, context) + py_factory_return_type(class_name, method, AnnotationSurface::Stub, context) } else { - py_method_return_type(method, context) + py_method_return_type(method, AnnotationSurface::Stub, context) }; let mut out = String::new(); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index 0568bbf9..15baf2ea 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -29,6 +29,7 @@ use super::collections::{ }; use super::naming::{PythonProjectionContext, PythonSupportSymbol, is_py_reserved, to_snake_case}; use super::native_types::foundation_type; +use super::nullability::AnnotationSurface; use super::shared::reorder_getters_before_setters; use super::signature::py_dynwinrt_type; use super::stub_helpers::{ @@ -451,23 +452,14 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf } } - let collection_base = - collection_kind - .and_then(abc_name) - .and_then(|abc| match iface.generic_args.as_slice() { - [element] => Some(format!( - "{}[{}]", - abc, - super::type_helpers::py_return_type_safe(Some(element), context) - )), - [key, value] => Some(format!( - "{}[{}, {}]", - abc, - super::type_helpers::py_return_type_safe(Some(key), context), - super::type_helpers::py_return_type_safe(Some(value), context) - )), - _ => None, - }); + let collection_base = collection_kind.and_then(abc_name).and_then(|abc| { + super::type_helpers::py_collection_base_type( + abc, + &iface.generic_args, + AnnotationSurface::Stub, + context, + ) + }); let identity_name = format!("_{}Identity", iface.name); out.push_str(&format!("\nclass {identity_name}(Protocol):\n")); out.push_str(&format!(" def {marker}(self) -> None: ...\n")); @@ -865,19 +857,13 @@ pub fn generate_class_stub( let collection_base = collection_iface .zip(collection_kind.and_then(abc_name)) - .and_then(|(iface, abc)| match iface.generic_args.as_slice() { - [element] => Some(format!( - "{}[{}]", - abc, - super::type_helpers::py_return_type_safe(Some(element), context) - )), - [key, value] => Some(format!( - "{}[{}, {}]", + .and_then(|(iface, abc)| { + super::type_helpers::py_collection_base_type( abc, - super::type_helpers::py_return_type_safe(Some(key), context), - super::type_helpers::py_return_type_safe(Some(value), context) - )), - _ => None, + &iface.generic_args, + AnnotationSurface::Stub, + context, + ) }); let mut instance_stub_body = emit_class_instance_stubs(class, context, collection_iface, false, has_closable); @@ -1061,19 +1047,13 @@ pub fn generate_class_stub( out.push('\n'); let required_base = interface_kind(req_iface) .and_then(abc_name) - .and_then(|abc| match req_iface.generic_args.as_slice() { - [element] => Some(format!( - "{}[{}]", + .and_then(|abc| { + super::type_helpers::py_collection_base_type( abc, - super::type_helpers::py_return_type_safe(Some(element), context) - )), - [key, value] => Some(format!( - "{}[{}, {}]", - abc, - super::type_helpers::py_return_type_safe(Some(key), context), - super::type_helpers::py_return_type_safe(Some(value), context) - )), - _ => None, + &req_iface.generic_args, + AnnotationSurface::Stub, + context, + ) }); if let Some(base) = required_base { out.push_str(&format!("\nclass {symbol}({base}):\n")); @@ -1298,7 +1278,9 @@ fn collection_protocol_stubs( let item_type = iface .generic_args .first() - .map(|typ| super::type_helpers::py_return_type_safe(Some(typ), context)) + .map(|typ| { + super::type_helpers::py_collection_item_type(typ, AnnotationSurface::Stub, context) + }) .unwrap_or_else(|| "object".to_string()); let item_input = iface .generic_args @@ -1341,7 +1323,13 @@ fn collection_protocol_stubs( let value_type = iface .generic_args .get(1) - .map(|typ| super::type_helpers::py_return_type_safe(Some(typ), context)) + .map(|typ| { + super::type_helpers::py_collection_item_type( + typ, + AnnotationSurface::Stub, + context, + ) + }) .unwrap_or_else(|| "object".to_string()); let mut result = format!( "\n{indent}def __len__(self) -> int: ...\n\ diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 34d75531..4d20ab69 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -10,11 +10,14 @@ use crate::codegen::winrt::shared::imports::{ use crate::meta::MethodMeta; use crate::types::TypeMeta; -use super::collections::{CollectionKind, is_mapping_input, type_kind}; +use super::collections::{CollectionKind, abc_name, is_mapping_input, type_kind}; use super::docs::format_pydoc; use super::naming::to_snake_case; use super::naming::{PythonProjectionContext, PythonSupportSymbol, PythonSymbol}; use super::native_types::{FoundationType, foundation_type}; +use super::nullability::{ + AnnotationSurface, OutputPosition, OutputSite, may_project_none, output_admits_none, +}; /// Build the Python docstring for a method body. Uses snake_case param display /// names (matching the generated signature). Returns an empty string when no @@ -58,25 +61,17 @@ pub(super) fn method_pydoc_with_indent( // ====================================================================== pub(crate) fn py_optional_type(typ: String) -> String { - let unquoted = typ - .strip_prefix('\'') - .and_then(|value| value.strip_suffix('\'')) - .unwrap_or(&typ); + let unquoted = unquoted(&typ); if unquoted.split('|').any(|part| part.trim() == "None") { return unquoted.to_string(); } format!("{} | None", unquoted) } -fn is_nullable_reference_type(typ: &TypeMeta) -> bool { - matches!( - typ, - TypeMeta::Object - | TypeMeta::Delegate { .. } - | TypeMeta::RuntimeClass { .. } - | TypeMeta::Interface { .. } - | TypeMeta::Parameterized { .. } - ) +fn unquoted(typ: &str) -> &str { + typ.strip_prefix('\'') + .and_then(|value| value.strip_suffix('\'')) + .unwrap_or(typ) } fn py_param_type(typ: &TypeMeta, context: &PythonProjectionContext) -> String { @@ -152,85 +147,324 @@ pub(super) fn py_collection_input_type( ) -> String { let input = py_param_type_safe(typ, context); // Keep the existing nullable ABC contract; only widen its projected inputs. - if is_nullable_reference_type(typ) { + if may_project_none(typ) { py_optional_type(input) } else { input } } -pub(crate) fn py_return_type_safe( - typ: Option<&TypeMeta>, +// ====================================================================== +// Output annotations +// +// Rendering and nullability are separate layers: the `spell_*` functions +// produce the non-null type expression for a position, and +// `nullability::output_admits_none` alone decides whether `| None` is added. +// ====================================================================== + +/// Spelling rules of the rendering layer. They predate the nullability policy +/// and are preserved byte-for-byte. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Spelling { + /// Method, out and property results: delegates are raw `DynWinRTValue` + /// handles, also inside async results and arrays. + Member, + /// A standalone value, such as the item type of a collection class. + Value, + /// An element of a returned collection or array: nested generics are + /// named by their projected class. + Element, +} + +impl Spelling { + fn at(position: OutputPosition) -> Self { + match position { + OutputPosition::CollectionElement | OutputPosition::CallbackParam => Self::Value, + OutputPosition::Return + | OutputPosition::OutParam + | OutputPosition::Property + | OutputPosition::AsyncResult + | OutputPosition::AsyncProgress + | OutputPosition::Activation => Self::Member, + } + } +} + +/// Renders the annotation of a value the consumer receives at `site`. +/// +/// Nullability never changes how the base type is spelled: a value that may +/// project as `None` renders the same unquoted expression with or without +/// ` | None`. +pub(crate) fn py_output_annotation( + typ: &TypeMeta, + site: OutputSite, + surface: AnnotationSurface, context: &PythonProjectionContext, ) -> String { - if let Some(inner) = typ.and_then(ireference_inner_type) { - return py_optional_type(py_return_type_safe(Some(inner), context)); - } - if let Some(async_type) = typ.and_then(|typ| py_async_return_type(typ, context)) { - return async_type; + render_output(typ, Spelling::at(site.position), site, surface, context) +} + +fn render_output( + typ: &TypeMeta, + spelling: Spelling, + site: OutputSite, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> String { + let base = match spelling { + Spelling::Member => spell_member(typ, site, surface, context), + Spelling::Value => spell_value(typ, site, surface, context), + Spelling::Element => py_native_element_type(typ, context), + }; + if !may_project_none(typ) { + return base; } - if let Some(annotation) = typ.and_then(|typ| py_collection_return_type(typ, context)) { - return py_optional_type(annotation); + if output_admits_none(typ, site, surface, context) { + py_optional_type(base) + } else { + unquoted(&base).to_string() } +} +fn spell_member( + typ: &TypeMeta, + site: OutputSite, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> String { + if context.is_delegate_type(typ) { + return "DynWinRTValue".to_string(); + } + let nested = |typ: &TypeMeta, position: OutputPosition| { + render_output( + typ, + Spelling::Member, + site.nested(position), + surface, + context, + ) + }; match typ { - Some(typ @ TypeMeta::Enum { .. }) if !context.is_known_type(typ) => "int".to_string(), - Some(typ @ (TypeMeta::RuntimeClass { .. } | TypeMeta::Interface { .. })) - if !context.is_known_type(typ) => - { - "DynWinRTValue | None".to_string() + TypeMeta::Array(inner) if context.is_delegate_type(inner) => { + format!("list[{}]", nested(inner, OutputPosition::CollectionElement)) } - Some(TypeMeta::Array(inner)) => py_array_return_type(inner, context), - Some(typ) if is_nullable_reference_type(typ) => { - py_optional_type(py_return_type(Some(typ), context)) + TypeMeta::AsyncOperation(result) => { + format!( + "WinRTCoroutine[{}]", + nested(result, OutputPosition::AsyncResult) + ) } - _ => py_return_type(typ, context), + TypeMeta::AsyncOperationWithProgress(result, progress) => format!( + "WinRTCoroutineWithProgress[{}, {}]", + nested(result, OutputPosition::AsyncResult), + nested(progress, OutputPosition::AsyncProgress) + ), + TypeMeta::AsyncActionWithProgress(progress) => format!( + "WinRTCoroutineWithProgress[None, {}]", + nested(progress, OutputPosition::AsyncProgress) + ), + _ => spell_value(typ, site, surface, context), } } -fn py_async_return_type_with_result( +fn spell_value( typ: &TypeMeta, - result_override: Option, + site: OutputSite, + surface: AnnotationSurface, context: &PythonProjectionContext, -) -> Option { +) -> String { + if let Some(inner) = ireference_inner_type(typ) { + return render_output(inner, Spelling::Value, site, surface, context); + } + if let Some(collection) = spell_collection(typ, site, surface, context) { + return collection; + } + let nested = |typ: &TypeMeta, spelling: Spelling, position: OutputPosition| { + render_output(typ, spelling, site.nested(position), surface, context) + }; match typ { - TypeMeta::AsyncAction => Some("WinRTCoroutine[None]".to_string()), - TypeMeta::AsyncOperation(result) => Some(format!( + TypeMeta::AsyncAction => "WinRTCoroutine[None]".to_string(), + TypeMeta::AsyncOperation(result) => format!( "WinRTCoroutine[{}]", - result_override.unwrap_or_else(|| py_return_type_safe(Some(result), context)) - )), - TypeMeta::AsyncActionWithProgress(progress) => Some(format!( + nested(result, Spelling::Value, OutputPosition::AsyncResult) + ), + TypeMeta::AsyncActionWithProgress(progress) => format!( "WinRTCoroutineWithProgress[None, {}]", - py_return_type_safe(Some(progress), context) - )), - TypeMeta::AsyncOperationWithProgress(result, progress) => Some(format!( + nested(progress, Spelling::Value, OutputPosition::AsyncProgress) + ), + TypeMeta::AsyncOperationWithProgress(result, progress) => format!( "WinRTCoroutineWithProgress[{}, {}]", - result_override.unwrap_or_else(|| py_return_type_safe(Some(result), context)), - py_return_type_safe(Some(progress), context) - )), - _ => None, + nested(result, Spelling::Value, OutputPosition::AsyncResult), + nested(progress, Spelling::Value, OutputPosition::AsyncProgress) + ), + TypeMeta::Enum { .. } if !context.is_known_type(typ) => "int".to_string(), + TypeMeta::RuntimeClass { .. } | TypeMeta::Interface { .. } + if !context.is_known_type(typ) => + { + "DynWinRTValue".to_string() + } + TypeMeta::Array(inner) if matches!(inner.as_ref(), TypeMeta::U8) => "bytes".to_string(), + TypeMeta::Array(inner) => format!( + "list[{}]", + nested(inner, Spelling::Element, OutputPosition::CollectionElement) + ), + TypeMeta::String | TypeMeta::Char16 => "str".to_string(), + TypeMeta::Guid => "UUID".to_string(), + TypeMeta::Bool => "bool".to_string(), + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::I64 + | TypeMeta::U64 => "int".to_string(), + TypeMeta::F32 | TypeMeta::F64 => "float".to_string(), + TypeMeta::RuntimeClass { .. } + | TypeMeta::Enum { .. } + | TypeMeta::Interface { .. } + | TypeMeta::Parameterized { .. } => { + format!("'{}'", context.reference_name_for_type(typ)) + } + TypeMeta::Object | TypeMeta::Delegate { .. } => "'DynWinRTValue'".to_string(), + TypeMeta::Struct { name, .. } if name == "HResult" => "int".to_string(), + typ if foundation_type(typ) == Some(FoundationType::DateTime) => "datetime".to_string(), + typ if foundation_type(typ) == Some(FoundationType::TimeSpan) => "timedelta".to_string(), + TypeMeta::Struct { .. } => format!("'{}'", context.reference_name_for_type(typ)), } } -pub(super) fn py_async_return_type( +fn spell_collection( + typ: &TypeMeta, + site: OutputSite, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> Option { + let TypeMeta::Parameterized { args, .. } = typ else { + return None; + }; + let abc = type_kind(typ).and_then(abc_name)?; + let elements = args + .iter() + .map(|arg| { + render_output( + arg, + Spelling::Element, + site.nested(OutputPosition::CollectionElement), + surface, + context, + ) + }) + .collect::>(); + Some(format!("{abc}[{}]", elements.join(", "))) +} + +/// Pessimistic rendering for callers outside the output policy (callback +/// parameters and the `IReference` input arm): every value that may +/// project as `None` admits it, as on the runtime surface. +pub(crate) fn py_return_type_safe( + typ: Option<&TypeMeta>, + context: &PythonProjectionContext, +) -> String { + typ.map(|typ| { + render_output( + typ, + Spelling::Value, + OutputSite::of(OutputPosition::CallbackParam), + AnnotationSurface::Runtime, + context, + ) + }) + .unwrap_or_else(|| "None".to_string()) +} + +/// Annotation of a property getter's value. +pub(super) fn py_property_type( typ: &TypeMeta, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> String { + py_output_annotation( + typ, + OutputSite::of(OutputPosition::Property), + surface, + context, + ) +} + +/// Item, key or value type of a projected collection class. +pub(super) fn py_collection_item_type( + typ: &TypeMeta, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> String { + py_output_annotation( + typ, + OutputSite::of(OutputPosition::CollectionElement), + surface, + context, + ) +} + +/// `Sequence[T]` / `Mapping[K, V]` base of a projected collection class. +pub(super) fn py_collection_base_type( + abc: &str, + args: &[TypeMeta], + surface: AnnotationSurface, context: &PythonProjectionContext, ) -> Option { - py_async_return_type_with_result(typ, None, context) + let item = |typ| py_collection_item_type(typ, surface, context); + match args { + [element] => Some(format!("{abc}[{}]", item(element))), + [key, value] => Some(format!("{abc}[{}, {}]", item(key), item(value))), + _ => None, + } } +/// Result annotation of an activation factory creating `class_name`. pub(super) fn py_factory_return_type( class_name: &str, method: &MethodMeta, + surface: AnnotationSurface, context: &PythonProjectionContext, ) -> String { - method - .return_type - .as_ref() - .and_then(|typ| { - py_async_return_type_with_result(typ, Some(format!("'{}'", class_name)), context) - }) - .unwrap_or_else(|| format!("'{}'", class_name)) + let instance = |typ: &TypeMeta| { + let instance = format!("'{class_name}'"); + let site = OutputSite::for_method(method, OutputPosition::Activation); + if output_admits_none(typ, site, surface, context) { + py_optional_type(instance) + } else { + instance + } + }; + let progress = |typ: &TypeMeta| { + render_output( + typ, + Spelling::Value, + OutputSite::for_method(method, OutputPosition::AsyncProgress), + surface, + context, + ) + }; + match method.return_type.as_ref() { + None => format!("'{class_name}'"), + Some(TypeMeta::AsyncAction) => "WinRTCoroutine[None]".to_string(), + Some(TypeMeta::AsyncOperation(result)) => { + format!("WinRTCoroutine[{}]", instance(result)) + } + Some(TypeMeta::AsyncActionWithProgress(progress_type)) => { + format!( + "WinRTCoroutineWithProgress[None, {}]", + progress(progress_type) + ) + } + Some(TypeMeta::AsyncOperationWithProgress(result, progress_type)) => format!( + "WinRTCoroutineWithProgress[{}, {}]", + instance(result), + progress(progress_type) + ), + Some(typ) => instance(typ), + } } pub(super) fn methods_have_async_output<'a>( @@ -249,20 +483,26 @@ pub(super) fn py_method_abi_output_count(method: &MethodMeta) -> usize { } pub(super) fn py_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> { - let mut result_index = 0; + py_method_output_positions(method) + .into_iter() + .enumerate() + .map(|(result_index, (typ, _))| (result_index, typ)) + .collect() +} + +/// Logical outputs in result order: out values first, then the return value. +fn py_method_output_positions(method: &MethodMeta) -> Vec<(&TypeMeta, OutputPosition)> { let mut outputs = Vec::new(); for param in &method.params { match param.direction { crate::meta::ParamDirection::Out => { - outputs.push((result_index, ¶m.typ)); - result_index += 1; + outputs.push((¶m.typ, OutputPosition::OutParam)); } crate::meta::ParamDirection::OutFill => { // The runtime allocates a distinct filled result buffer; the // caller-provided array supplies capacity and is not mutated. - outputs.push((result_index, ¶m.typ)); - result_index += 1; + outputs.push((¶m.typ, OutputPosition::OutParam)); } crate::meta::ParamDirection::In => {} } @@ -273,108 +513,32 @@ pub(super) fn py_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> .as_ref() .filter(|_| !fill_array_uses_retval_count(method)) { - outputs.push((result_index, return_type)); + outputs.push((return_type, OutputPosition::Return)); } outputs } -fn is_delegate_output(typ: &TypeMeta, context: &PythonProjectionContext) -> bool { - context.is_delegate_type(typ) -} - -pub(super) fn py_output_type(typ: &TypeMeta, context: &PythonProjectionContext) -> String { - match typ { - _ if is_delegate_output(typ, context) => "DynWinRTValue | None".to_string(), - TypeMeta::Array(inner) if is_delegate_output(inner, context) => { - "list[DynWinRTValue | None]".to_string() - } - TypeMeta::AsyncOperation(inner) => { - format!("WinRTCoroutine[{}]", py_output_type(inner, context)) - } - TypeMeta::AsyncOperationWithProgress(result, progress) => format!( - "WinRTCoroutineWithProgress[{}, {}]", - py_output_type(result, context), - py_output_type(progress, context) - ), - TypeMeta::AsyncActionWithProgress(progress) => format!( - "WinRTCoroutineWithProgress[None, {}]", - py_output_type(progress, context) - ), - _ => py_return_type_safe(Some(typ), context), - } -} - pub(super) fn py_method_return_type( method: &MethodMeta, + surface: AnnotationSurface, context: &PythonProjectionContext, ) -> String { - let outputs = py_method_outputs(method); + let outputs = py_method_output_positions(method) + .into_iter() + .map(|(typ, position)| { + py_output_annotation( + typ, + OutputSite::for_method(method, position), + surface, + context, + ) + }) + .collect::>(); match outputs.as_slice() { [] => "None".to_string(), - [(_, typ)] => py_output_type(typ, context), - _ => format!( - "tuple[{}]", - outputs - .iter() - .map(|(_, typ)| py_output_type(typ, context)) - .collect::>() - .join(", ") - ), - } -} - -fn py_return_type(typ: Option<&TypeMeta>, context: &PythonProjectionContext) -> String { - match typ { - Some(TypeMeta::String) => "str".to_string(), - Some(TypeMeta::Guid) => "UUID".to_string(), - Some(TypeMeta::Bool) => "bool".to_string(), - Some( - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::I64 - | TypeMeta::U64, - ) => "int".to_string(), - Some(TypeMeta::Char16) => "str".to_string(), - Some(TypeMeta::F32 | TypeMeta::F64) => "float".to_string(), - Some(typ @ TypeMeta::RuntimeClass { .. }) - | Some(typ @ TypeMeta::Enum { .. }) - | Some(typ @ TypeMeta::Interface { .. }) => { - format!("'{}'", context.reference_name_for_type(typ)) - } - Some(typ @ TypeMeta::Parameterized { .. }) => { - format!("'{}'", context.reference_name_for_type(typ)) - } - Some(TypeMeta::AsyncOperation(inner)) => { - format!("WinRTCoroutine[{}]", py_return_type(Some(inner), context)) - } - Some(TypeMeta::AsyncOperationWithProgress(result, progress)) => format!( - "WinRTCoroutineWithProgress[{}, {}]", - py_return_type(Some(result), context), - py_return_type(Some(progress), context) - ), - Some(TypeMeta::AsyncAction) => "WinRTCoroutine[None]".to_string(), - Some(TypeMeta::AsyncActionWithProgress(progress)) => format!( - "WinRTCoroutineWithProgress[None, {}]", - py_return_type(Some(progress), context) - ), - Some(TypeMeta::Array(inner)) => py_array_return_type(inner, context), - Some(TypeMeta::Object) | Some(TypeMeta::Delegate { .. }) => "'DynWinRTValue'".to_string(), - Some(TypeMeta::Struct { name, .. }) if name == "HResult" => "int".to_string(), - Some(typ) if foundation_type(typ) == Some(FoundationType::DateTime) => { - "datetime".to_string() - } - Some(typ) if foundation_type(typ) == Some(FoundationType::TimeSpan) => { - "timedelta".to_string() - } - Some(typ @ TypeMeta::Struct { .. }) => { - format!("'{}'", context.reference_name_for_type(typ)) - } - None => "None".to_string(), + [output] => output.clone(), + _ => format!("tuple[{}]", outputs.join(", ")), } } @@ -441,20 +605,6 @@ fn py_native_param_element_type(inner: &TypeMeta, context: &PythonProjectionCont } } -fn py_array_return_type(inner: &TypeMeta, context: &PythonProjectionContext) -> String { - if matches!(inner, TypeMeta::U8) { - "bytes".to_string() - } else { - let element = py_native_element_type(inner, context); - let element = if is_nullable_reference_type(inner) { - py_optional_type(element) - } else { - element - }; - format!("list[{element}]") - } -} - fn py_collection_param_type(typ: &TypeMeta, context: &PythonProjectionContext) -> Option { let TypeMeta::Parameterized { args, .. } = typ else { return None; @@ -500,26 +650,6 @@ fn py_collection_param_type(typ: &TypeMeta, context: &PythonProjectionContext) - } } -fn py_collection_return_type(typ: &TypeMeta, context: &PythonProjectionContext) -> Option { - let TypeMeta::Parameterized { args, .. } = typ else { - return None; - }; - let kind = type_kind(typ)?; - let abc = super::collections::abc_name(kind)?; - let types = args - .iter() - .map(|arg| { - let element = py_native_element_type(arg, context); - if is_nullable_reference_type(arg) { - py_optional_type(element) - } else { - element - } - }) - .collect::>(); - Some(format!("{abc}[{}]", types.join(", "))) -} - pub(super) fn py_param_list( in_params: &[&crate::meta::ParamMeta], context: &PythonProjectionContext, @@ -587,6 +717,19 @@ mod tests { use super::*; use crate::meta::{ParamDirection, ParamMeta}; + fn returned( + typ: &TypeMeta, + surface: AnnotationSurface, + context: &PythonProjectionContext, + ) -> String { + py_output_annotation( + typ, + OutputSite::of(OutputPosition::Return), + surface, + context, + ) + } + #[test] fn multi_out_returns_typed_tuple_in_abi_order() { let method = MethodMeta { @@ -613,7 +756,11 @@ mod tests { assert_eq!(outputs[0], (0, &TypeMeta::U32)); assert_eq!(outputs[1], (1, &TypeMeta::Bool)); assert_eq!( - py_method_return_type(&method, &PythonProjectionContext::default()), + py_method_return_type( + &method, + AnnotationSurface::Stub, + &PythonProjectionContext::default() + ), "tuple[int, bool]" ); } @@ -646,7 +793,11 @@ mod tests { (0, &TypeMeta::Array(Box::new(TypeMeta::String))) ); assert_eq!( - py_method_return_type(&method, &PythonProjectionContext::default()), + py_method_return_type( + &method, + AnnotationSurface::Stub, + &PythonProjectionContext::default() + ), "list[str]" ); assert_eq!( @@ -665,10 +816,13 @@ mod tests { #[test] fn object_arrays_return_typed_runtime_values() { - assert_eq!( - py_array_return_type(&TypeMeta::Object, &PythonProjectionContext::default()), - "list[DynWinRTValue | None]" - ); + let array = TypeMeta::Array(Box::new(TypeMeta::Object)); + for surface in [AnnotationSurface::Runtime, AnnotationSurface::Stub] { + assert_eq!( + returned(&array, surface, &PythonProjectionContext::default()), + "list[DynWinRTValue | None]" + ); + } } #[test] @@ -705,7 +859,7 @@ mod tests { assert_eq!(py_param_type_safe(&typ, &context), expected); } assert_eq!( - py_return_type_safe(Some(&TypeMeta::Object), &context), + returned(&TypeMeta::Object, AnnotationSurface::Stub, &context), "DynWinRTValue | None" ); assert_eq!( @@ -739,17 +893,21 @@ mod tests { "DynWinRTValue | _DynWinRTObject_2 | None" ); assert_eq!( - py_return_type_safe(Some(&TypeMeta::Object), &context), + returned(&TypeMeta::Object, AnnotationSurface::Stub, &context), "DynWinRTValue | None" ); assert_eq!( - py_array_return_type(&TypeMeta::Object, &context), + returned( + &TypeMeta::Array(Box::new(TypeMeta::Object)), + AnnotationSurface::Stub, + &context + ), "list[DynWinRTValue | None]" ); } #[test] - fn reference_returns_are_annotated_as_nullable() { + fn runtime_reference_outputs_stay_nullable() { let runtime_class = TypeMeta::RuntimeClass { namespace: "Contoso".into(), name: "Widget".into(), @@ -765,23 +923,26 @@ mod tests { interface.type_identity(), ]) .unwrap(); + let runtime = AnnotationSurface::Runtime; + assert_eq!(returned(&runtime_class, runtime, &context), "Widget | None"); + assert_eq!(returned(&interface, runtime, &context), "IWidget | None"); assert_eq!( - py_return_type_safe(Some(&runtime_class), &context), - "Widget | None" - ); - assert_eq!( - py_return_type_safe(Some(&interface), &context), - "IWidget | None" - ); - assert_eq!( - py_return_type_safe(Some(&TypeMeta::Object), &context), + returned(&TypeMeta::Object, runtime, &context), "DynWinRTValue | None" ); assert_eq!( - py_array_return_type(&runtime_class, &context), + returned( + &TypeMeta::Array(Box::new(runtime_class.clone())), + runtime, + &context + ), "list[Widget | None]" ); + assert_eq!( + py_return_type_safe(Some(&runtime_class), &context), + "Widget | None" + ); } #[test] @@ -815,10 +976,12 @@ mod tests { args: vec![TypeMeta::U32], }; - assert_eq!( - py_return_type_safe(Some(&reference), &PythonProjectionContext::default()), - "int | None" - ); + for surface in [AnnotationSurface::Runtime, AnnotationSurface::Stub] { + assert_eq!( + returned(&reference, surface, &PythonProjectionContext::default()), + "int | None" + ); + } assert_eq!( py_param_type_safe(&reference, &PythonProjectionContext::default()), "int | None | IReference_UInt32" From 357e5f5fa46bb9058901db0f1a5a966fa498350b Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 13:21:42 +0800 Subject: [PATCH 02/12] Type Python stub outputs as non-null by default 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 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> --- bindings/py/README.md | 25 +++ tests/e2e/typecheck/python_generated_api.py | 20 ++- .../src/codegen/winrt/python/nullability.rs | 144 +++++++++++++++- .../src/codegen/winrt/python/stub_helpers.rs | 9 +- .../src/codegen/winrt/python/type_helpers.rs | 130 ++++++++++++++ .../tests/implementation_naming_test.rs | 8 +- .../tests/python_constructor_boundary_test.rs | 2 +- .../tests/python_consumer_typing_test.rs | 93 +++++++++- .../tests/python_inheritance_typing_test.rs | 2 +- .../tests/python_stub_nullability_test.rs | 163 ++++++++++++++++++ .../tests/python_symbol_mapping_test.rs | 38 ++-- ..._iterator_i_www_form_url_decoder_entry.pyi | 10 +- .../tests/snapshots/uri_pyi/uri.pyi | 4 +- .../uri_pyi/www_form_url_decoder.pyi | 38 ++-- 14 files changed, 620 insertions(+), 66 deletions(-) create mode 100644 tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs diff --git a/bindings/py/README.md b/bindings/py/README.md index 2a1cb72c..a079c2fc 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -19,6 +19,31 @@ Generated package manifests pin `dynwinrt` to the exact version of Generated `IReference` values are projected as `T | None`; native values, `None`, and generated `IReference_*` wrappers are accepted as inputs. +### Nullability in type stubs + +WinRT metadata does not record which values can be null, and most APIs raise +an exception instead of returning null. The generated `.pyi` stubs therefore +type the values you receive as non-null by default: method and property +results, async results, out values, and the elements, keys and values of +returned collections. For example, `StorageFolder.create_file_async()` returns +`WinRTCoroutine[StorageFile]` and `get_files_async()` returns +`WinRTCoroutine[Sequence[StorageFile]]`. + +These values keep `| None`: + +- `IReference` values, projected as `T | None` everywhere; +- results of `Try*` members, such as `try_get_item_async()` or + `JsonObject.try_parse()`, where null means "not found"; +- `Object`/`IInspectable` values (`DynWinRTValue | None`) and delegate-typed + values, which are often null. + +Arguments keep accepting `None` where they did before. The stubs are +optimistic, like the generated TypeScript declarations: the runtime still +returns `None` when a WinRT API returns null, so check the API documentation +when a result can legitimately be absent. The inline annotations of the +generated `.py` modules, which `typing.get_type_hints()` and `--no-pyi` output +expose, still mark every object result `| None`. + ## Async WinRT operations Generated async methods return typed, asyncio-compatible operation objects: diff --git a/tests/e2e/typecheck/python_generated_api.py b/tests/e2e/typecheck/python_generated_api.py index 3902d0c9..6230b2e6 100644 --- a/tests/e2e/typecheck/python_generated_api.py +++ b/tests/e2e/typecheck/python_generated_api.py @@ -3,7 +3,7 @@ import asyncio from collections.abc import Coroutine, Generator, Sequence -from typing import Any, Awaitable, List, Tuple +from typing import Any, Awaitable, List, Tuple, assert_type from dynwinrt import ( DynWinRTArray, @@ -22,7 +22,9 @@ IWwwFormUrlDecoderEntry, Uri, ) +from python_bindings.windows.foundation.collections import ValueSet from python_bindings.windows.globalization import Calendar +from python_bindings.windows.storage import IStorageItem, StorageFile, StorageFolder from python_bindings.windows.storage.streams import ( Buffer as WinRTBuffer, DataWriter, @@ -69,8 +71,9 @@ def check_uri() -> None: uri: Uri = Uri("https://example.com") relative: Uri = Uri("https://example.com/root/", "child") host: str = uri.host - combined: Uri | None = uri.combine_uri("child") - _: Tuple[str, Uri, Uri | None] = (host, relative, combined) + combined: Uri = uri.combine_uri("child") + absolute: str = combined.absolute_uri + _: Tuple[str, Uri, Uri, str] = (host, relative, combined, absolute) def check_nullable_value( @@ -86,8 +89,7 @@ def check_nullable_value( def check_string_vector(calendar: Calendar) -> None: - languages: Sequence[str] | None = calendar.languages - assert languages is not None + languages: Sequence[str] = calendar.languages first: str = languages[0] located: int = languages.index(first) many: List[str] = list(languages[:4]) @@ -152,3 +154,11 @@ def check_ibuffer_bytes() -> None: interface_bytes: bytes = interface_buffer.to_bytes() runtime_bytes: bytes = runtime_buffer.to_bytes() _: Tuple[bytes, bytes] = (interface_bytes, runtime_bytes) + + +async def check_output_nullability(folder: StorageFolder, values: ValueSet) -> None: + created: StorageFile = await folder.create_file_async("notes.txt") + names: List[str] = [item.name for item in await folder.get_files_async()] + assert_type(folder.try_get_item_async("notes.txt"), WinRTCoroutine[IStorageItem | None]) + assert_type(values["key"], DynWinRTValue | None) + _: Tuple[StorageFile, List[str]] = (created, names) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index 7b5e1536..7077b9a0 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -113,7 +113,7 @@ pub(crate) fn output_admits_none( typ: &TypeMeta, site: OutputSite, surface: AnnotationSurface, - _context: &PythonProjectionContext, + context: &PythonProjectionContext, ) -> bool { if ireference_inner_type(typ).is_some() { return true; @@ -121,13 +121,36 @@ pub(crate) fn output_admits_none( if !may_project_none(typ) { return false; } - match (surface, site.position) { - (_, OutputPosition::Activation) => false, - (AnnotationSurface::Runtime, _) => true, - (AnnotationSurface::Stub, _) => true, + match surface { + // Runtime annotations stay pessimistic: `typing.get_type_hints()` and + // `--no-pyi` consumers see every reference output as optional. + AnnotationSurface::Runtime => site.position != OutputPosition::Activation, + AnnotationSurface::Stub => stub_output_admits_none(typ, site, context), } } +/// Stubs are optimistic, like the JavaScript declarations: WinRT metadata has +/// no nullability, and most APIs raise instead of returning null. +fn stub_output_admits_none( + typ: &TypeMeta, + site: OutputSite, + context: &PythonProjectionContext, +) -> bool { + use OutputPosition::{Activation, AsyncResult, CallbackParam, OutParam, Return}; + + // `Object` positions are frequently null, e.g. the arguments of a + // `TypedEventHandler`. + if matches!(typ, TypeMeta::Object) { + return true; + } + // Delegate-typed values are raw handles that are null while unset. + if context.is_delegate_type(typ) { + return site.position != CallbackParam; + } + // `Try*` members report "not found" through a null result. + site.try_method && matches!(site.position, Return | OutParam | AsyncResult | Activation) +} + #[cfg(test)] mod tests { use super::*; @@ -161,4 +184,115 @@ mod tests { } ); } + + const POSITIONS: [OutputPosition; 8] = [ + OutputPosition::Return, + OutputPosition::OutParam, + OutputPosition::Property, + OutputPosition::AsyncResult, + OutputPosition::AsyncProgress, + OutputPosition::CollectionElement, + OutputPosition::CallbackParam, + OutputPosition::Activation, + ]; + + fn widget() -> TypeMeta { + TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + } + } + + fn handler() -> TypeMeta { + TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "Handler".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + } + } + + fn nullable_u32() -> TypeMeta { + TypeMeta::Parameterized { + namespace: "Windows.Foundation".into(), + name: "IReference`1".into(), + piid: "61c17706-2d65-11e0-9ae8-d48564015472".into(), + args: vec![TypeMeta::U32], + } + } + + fn admits(typ: &TypeMeta, site: OutputSite, surface: AnnotationSurface) -> bool { + output_admits_none(typ, site, surface, &PythonProjectionContext::default()) + } + + #[test] + fn runtime_annotations_stay_pessimistic() { + for position in POSITIONS { + let site = OutputSite::of(position); + let expected = position != OutputPosition::Activation; + for typ in [widget(), handler(), TypeMeta::Object] { + assert_eq!( + admits(&typ, site, AnnotationSurface::Runtime), + expected, + "{typ:?} at {position:?}" + ); + } + assert!(admits(&nullable_u32(), site, AnnotationSurface::Runtime)); + assert!(!admits(&TypeMeta::String, site, AnnotationSurface::Runtime)); + } + } + + #[test] + fn stubs_are_non_null_by_default() { + for position in POSITIONS { + let site = OutputSite::of(position); + assert!( + !admits(&widget(), site, AnnotationSurface::Stub), + "{position:?}" + ); + assert!(!admits(&TypeMeta::I32, site, AnnotationSurface::Stub)); + } + } + + #[test] + fn stub_exceptions_keep_none() { + for position in POSITIONS { + let site = OutputSite::of(position); + assert!(admits(&nullable_u32(), site, AnnotationSurface::Stub)); + assert!(admits(&TypeMeta::Object, site, AnnotationSurface::Stub)); + assert_eq!( + admits(&handler(), site, AnnotationSurface::Stub), + position != OutputPosition::CallbackParam, + "{position:?}" + ); + } + } + + #[test] + fn try_members_keep_none_on_their_results_only() { + let try_get = method("TryGetItemAsync"); + for position in POSITIONS { + let expected = matches!( + position, + OutputPosition::Return + | OutputPosition::OutParam + | OutputPosition::AsyncResult + | OutputPosition::Activation + ); + assert_eq!( + admits( + &widget(), + OutputSite::for_method(&try_get, position), + AnnotationSurface::Stub + ), + expected, + "{position:?}" + ); + } + assert!(!admits( + &TypeMeta::Bool, + OutputSite::for_method(&try_get, OutputPosition::Return), + AnnotationSurface::Stub + )); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index b1eaee21..853b36d5 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -328,10 +328,11 @@ pub(super) fn emit_method_stub_named( } else { format!("self, {}", py_params) }; - // WinRT vectors may reject null on mutation while returning null - // interface elements. That asymmetric native contract cannot satisfy - // MutableSequence[T | None]'s append signature exactly. Empty - // structural protocols can make mypy consider the override compatible. + // `append` takes the projected input annotation, which can differ from + // the element annotation of the MutableSequence base: WinRT vectors + // may reject null on mutation, while `Object` elements are read back + // as `DynWinRTValue | None`. Empty structural protocols can make mypy + // consider the override compatible. let override_ignore = if overrides_mutable_sequence && method_name == "append" && in_params.first().is_some_and(|param| { diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 4d20ab69..1b75501a 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -945,6 +945,136 @@ mod tests { ); } + #[test] + fn stub_outputs_are_non_null_except_policy_exceptions() { + let widget = TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + }; + let interface = TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IWidget".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + }; + let unknown = TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "NotGenerated".into(), + default_interface: None, + }; + let context = PythonProjectionContext::standalone([ + widget.type_identity(), + interface.type_identity(), + ]) + .unwrap(); + let widgets = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: crate::codegen::winrt::python::collections::IVECTOR_VIEW_PIID.into(), + args: vec![widget.clone()], + }; + let stub = AnnotationSurface::Stub; + let async_of = |typ: &TypeMeta| TypeMeta::AsyncOperation(Box::new(typ.clone())); + let method = |raw_name: &str, params: Vec, return_type: TypeMeta| MethodMeta { + name: raw_name.into(), + raw_name: raw_name.into(), + params, + return_type: Some(return_type), + ..Default::default() + }; + + assert_eq!(returned(&widget, stub, &context), "Widget"); + assert_eq!(returned(&interface, stub, &context), "IWidget"); + assert_eq!(returned(&unknown, stub, &context), "DynWinRTValue"); + assert_eq!( + returned(&TypeMeta::Array(Box::new(widget.clone())), stub, &context), + "list[Widget]" + ); + assert_eq!( + returned(&async_of(&widget), stub, &context), + "WinRTCoroutine[Widget]" + ); + assert_eq!( + returned(&async_of(&widgets), stub, &context), + "WinRTCoroutine[Sequence[Widget]]" + ); + assert_eq!( + returned(&TypeMeta::Object, stub, &context), + "DynWinRTValue | None" + ); + assert_eq!(py_property_type(&widget, stub, &context), "Widget"); + assert_eq!(py_collection_item_type(&widget, stub, &context), "Widget"); + assert_eq!( + py_collection_base_type("Sequence", std::slice::from_ref(&widget), stub, &context), + Some("Sequence[Widget]".to_string()) + ); + + let get_item = method("GetItemAsync", vec![], async_of(&widget)); + let try_get_item = method("TryGetItemAsync", vec![], async_of(&widget)); + let try_get_items = method("TryGetItemsAsync", vec![], async_of(&widgets)); + let try_parse = method( + "TryParse", + vec![ + ParamMeta { + name: "input".into(), + typ: TypeMeta::String, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: widget.clone(), + direction: ParamDirection::Out, + }, + ], + TypeMeta::Bool, + ); + for (method, stub_type, runtime_type) in [ + ( + &get_item, + "WinRTCoroutine[Widget]", + "WinRTCoroutine[Widget | None]", + ), + ( + &try_get_item, + "WinRTCoroutine[Widget | None]", + "WinRTCoroutine[Widget | None]", + ), + ( + &try_get_items, + "WinRTCoroutine[Sequence[Widget] | None]", + "WinRTCoroutine[Sequence[Widget | None] | None]", + ), + ( + &try_parse, + "tuple[Widget | None, bool]", + "tuple[Widget | None, bool]", + ), + ] { + assert_eq!(py_method_return_type(method, stub, &context), stub_type); + assert_eq!( + py_method_return_type(method, AnnotationSurface::Runtime, &context), + runtime_type + ); + } + + let create = method("CreateWidget", vec![], widget.clone()); + let try_create = method("TryCreateWidget", vec![], widget.clone()); + for surface in [AnnotationSurface::Runtime, stub] { + assert_eq!( + py_factory_return_type("Widget", &create, surface, &context), + "'Widget'" + ); + } + assert_eq!( + py_factory_return_type("Widget", &try_create, stub, &context), + "Widget | None" + ); + assert_eq!( + py_factory_return_type("Widget", &try_create, AnnotationSurface::Runtime, &context), + "'Widget'" + ); + } + #[test] fn delegate_inputs_accept_callables_and_runtime_values() { let param = ParamMeta { diff --git a/tools/dynwinrt-codegen/tests/implementation_naming_test.rs b/tools/dynwinrt-codegen/tests/implementation_naming_test.rs index 16bd74e5..02a888a9 100644 --- a/tools/dynwinrt-codegen/tests/implementation_naming_test.rs +++ b/tools/dynwinrt-codegen/tests/implementation_naming_test.rs @@ -1116,10 +1116,10 @@ fn python_full_identity_collision_imports_named_peer_not_generic_self() { from pyviews.{peer_module} import {peer_name} as Peer\n\ from pyviews.{foreign_module} import {foreign_name} as Foreign\n\ def check(box: Box, peer: Peer, foreign: Foreign) -> None:\n\ - \x20 assert_type(box.echo_self(box), Box | None)\n\ - \x20 assert_type(box.echo_peer(peer), Peer | None)\n\ - \x20 assert_type(box.echo_foreign(foreign), Foreign | None)\n\ - \x20 assert_type(peer.echo_self(peer), Peer | None)\n" + \x20 assert_type(box.echo_self(box), Box)\n\ + \x20 assert_type(box.echo_peer(peer), Peer)\n\ + \x20 assert_type(box.echo_foreign(foreign), Foreign)\n\ + \x20 assert_type(peer.echo_self(peer), Peer)\n" ), ); typecheck_py_package(&fixture.0); diff --git a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs index 9b73889b..65ee2ac7 100644 --- a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs +++ b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs @@ -72,7 +72,7 @@ fn system_returned_class_keeps_only_internal_native_wrapping() { assert!(!py.contains("self._set_native(type(self).create(")); assert!(!py.contains("_IActivationFactory =")); assert!(pyi.contains("def __init__(self, _not_constructible: NoReturn) -> None: ...")); - assert!(pyi.contains("def get_current() -> SystemResult | None: ...")); + assert!(pyi.contains("def get_current() -> SystemResult: ...")); assert!(!pyi.contains("def from_value("), "{pyi}"); assert!(!pyi.contains("def __init__(self, obj: DynWinRTValue)")); assert!(!pyi.contains("def __init__(self)")); diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 622a464b..04191b2c 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -421,7 +421,7 @@ def collections(resource: Resource, derived: OtherDerived, raw: DynWinRTValue, mapping[resource] = derived assert_type(vector[0], DynWinRTValue | None) assert_type(vector[:], list[DynWinRTValue | None]) - assert_type(resources[0], Resource | None) + assert_type(resources[0], Resource) assert_type(mapping[resource], DynWinRTValue | None) del mapping[resource] "#, @@ -813,6 +813,97 @@ print("collection-subscript-native-ok", flush=True) } } +#[test] +fn natural_sdk_consumers_need_no_none_guards() { + let winmd = Path::new( + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd", + ); + if !winmd.is_file() || !has_mypy() { + eprintln!("Skipping natural SDK consumers: Windows.winmd or mypy unavailable."); + return; + } + let fixture = Fixture::new(); + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(winmd) + .args([ + "--class-name", + "Windows.Foundation.Uri,Windows.Foundation.Collections.PropertySet,\ + Windows.Data.Json.JsonObject,Windows.Globalization.Calendar,\ + Windows.Security.Cryptography.CryptographicBuffer,\ + Windows.Security.Cryptography.Core.HashAlgorithmProvider,\ + Windows.Storage.StorageFolder,Windows.Storage.FileIO,\ + Windows.Storage.Streams.DataReader,Windows.Storage.Streams.DataWriter,\ + Windows.Storage.Streams.InMemoryRandomAccessStream", + "--lang", + "py", + "--output", + ]) + .arg(fixture.0.join("sdk")) + .output() + .unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + let imports = r#"from collections.abc import Sequence +from typing import assert_type +from dynwinrt import DynWinRTValue, WinRTCoroutine +from sdk.windows.data.json import JsonObject +from sdk.windows.foundation import Uri +from sdk.windows.foundation.collections import PropertySet +from sdk.windows.globalization import Calendar +from sdk.windows.security.cryptography import CryptographicBuffer +from sdk.windows.security.cryptography.core import HashAlgorithmProvider +from sdk.windows.storage import CreationCollisionOption, FileIO, IStorageItem, StorageFile, StorageFolder +from sdk.windows.storage.streams import DataReader, DataWriter, InMemoryRandomAccessStream +"#; + typecheck( + &fixture, + &["sdk"], + &format!( + r#"{imports} +def uri_demo() -> str: + uri = Uri("https://example.com/a/b?x=1&y=two") + query = {{entry.name: entry.value for entry in uri.query_parsed}} + return uri.combine_uri("c/d").absolute_uri + str(query) + +def json_demo() -> list[str]: + parsed = JsonObject.parse('{{"tags": ["a", "b"]}}') + assert_type(JsonObject.try_parse("{{}}"), tuple[JsonObject | None, bool]) + return [value.get_string() for value in parsed.get_named_array("tags")] + +def calendar_demo(calendar: Calendar) -> str: + languages: Sequence[str] = calendar.languages + return languages[0] + +def crypto_demo(data: bytes) -> str: + buffer = CryptographicBuffer.create_from_byte_array(data) + digest = HashAlgorithmProvider.open_algorithm("SHA256").hash_data(buffer) + return CryptographicBuffer.encode_to_hex_string(digest) + +def object_values(properties: PropertySet) -> DynWinRTValue | None: + assert_type(properties["count"], DynWinRTValue | None) + return properties.lookup("count") + +async def streams_demo() -> str: + stream = InMemoryRandomAccessStream() + writer = DataWriter(stream.get_output_stream_at(0)) + writer.write_string("streamed text") + written = await writer.store_async() + reader = DataReader(stream.get_input_stream_at(0)) + return reader.read_string(await reader.load_async(written)) + +async def storage_demo(path: str) -> list[str]: + folder = await StorageFolder.get_folder_from_path_async(path) + file = await folder.create_file_async("notes.txt", CreationCollisionOption.ReplaceExisting) + await FileIO.write_text_async(file, "first line") + assert_type(folder.create_file_async("a.txt"), WinRTCoroutine[StorageFile]) + assert_type(folder.try_get_item_async("notes.txt"), WinRTCoroutine[IStorageItem | None]) + return [item.name for item in await folder.get_files_async()] +"# + ), + &[], + ); +} + #[test] fn native_object_inputs_keep_projection_factories_and_context_lifetimes() { if !has_implementation_runtime() { diff --git a/tools/dynwinrt-codegen/tests/python_inheritance_typing_test.rs b/tools/dynwinrt-codegen/tests/python_inheritance_typing_test.rs index bdf317d0..2bd3505d 100644 --- a/tools/dynwinrt-codegen/tests/python_inheritance_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_inheritance_typing_test.rs @@ -135,7 +135,7 @@ fn stubs_model_runtime_class_and_interface_bases_without_runtime_inheritance() { "{class_stub}" ); assert!( - class_stub.contains("def use_base(self, value: 'BaseLike') -> Base | None:"), + class_stub.contains("def use_base(self, value: 'BaseLike') -> Base:"), "{class_stub}" ); assert!( diff --git a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs new file mode 100644 index 00000000..552988fe --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The stub nullability policy on real Windows SDK metadata. Values received +//! from the projection are non-null in `.pyi` stubs by default, while +//! `IReference`, `Try*` results, `Object` and delegate values keep +//! `| None`. Inputs, implementation protocols and the runtime `.py` +//! annotations are unchanged. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const WINDOWS_WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + +struct Generated(PathBuf); + +impl Drop for Generated { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +impl Generated { + fn new(classes: &str) -> Option { + if !Path::new(WINDOWS_WINMD).is_file() { + eprintln!("Skipping: Windows.winmd not found"); + return None; + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("target") + .join(format!("pn{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + WINDOWS_WINMD, + "--class-name", + classes, + ]) + .args(["--lang", "py", "--output"]) + .arg(&root) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Some(Self(root)) + } + + fn module(&self, name: &str) -> String { + fs::read_to_string(self.0.join(name)).unwrap_or_else(|error| panic!("{name}: {error}")) + } +} + +fn assert_contains(text: &str, expected: &str) { + assert!(text.contains(expected), "missing `{expected}`"); +} + +#[test] +fn stub_outputs_follow_the_nullability_policy() { + let Some(generated) = Generated::new( + "Windows.Storage.StorageFolder,Windows.Web.Http.Headers.HttpContentHeaderCollection,\ + Windows.Foundation.Collections.PropertySet,Windows.Data.Json.JsonObject", + ) else { + return; + }; + let folder = generated.module("windows__storage__storage_folder.pyi"); + let folder_py = generated.module("windows__storage__storage_folder.py"); + let folder_view = generated.module("windows__storage__i_storage_folder.pyi"); + let headers = + generated.module("windows__web__http__headers__http_content_header_collection.pyi"); + let properties = generated.module("windows__foundation__collections__property_set.pyi"); + let json = generated.module("windows__data__json__json_object.pyi"); + + // Method, async, collection and property outputs are non-null by default. + assert_contains( + &folder, + "def create_file_async(self, desired_name: str) -> WinRTCoroutine[StorageFile]: ...", + ); + assert_contains( + &folder, + "def get_files_async(self) -> WinRTCoroutine[Sequence[StorageFile]]: ...", + ); + assert_contains( + &folder, + "def get_folder_from_path_async(path: str) -> WinRTCoroutine[StorageFolder]: ...", + ); + assert_contains( + &folder, + "def properties(self) -> StorageItemContentProperties: ...", + ); + assert_contains( + &json, + "def get_named_object(self, name: str) -> JsonObject: ...", + ); + assert_contains(&json, "def parse(input: str) -> JsonObject: ..."); + + // Try* results keep None on their result, async result and out values. + assert_contains( + &folder, + "def try_get_item_async(self, name: str) -> WinRTCoroutine[IStorageItem | None]: ...", + ); + assert_contains( + &json, + "def try_parse(input: str) -> tuple[JsonObject | None, bool]: ...", + ); + + // IReference values and Object values keep None. + assert_contains(&headers, "def content_length(self) -> int | None: ..."); + assert_contains( + &headers, + "def content_type(self) -> HttpMediaTypeHeaderValue: ...", + ); + assert_contains( + &properties, + "def lookup(self, key: str) -> DynWinRTValue | None: ...", + ); + assert_contains( + &properties, + "def __getitem__(self, key: str) -> DynWinRTValue | None: ...", + ); + + // Inputs are unchanged. + assert_contains( + &headers, + "def content_length(self, value: int | None | IReference_UInt64) -> None: ...", + ); + assert_contains( + &folder, + "def create_folder_query(self, query_options: 'QueryOptionsLike') -> StorageFolderQueryResult: ...", + ); + assert_contains( + &properties, + "def __setitem__(self, key: str, value: DynWinRTValue | _DynWinRTObject | None) -> None: ...", + ); + + // Implementation protocols keep their obligations. + assert_contains(&folder_view, "class IStorageFolderHandlers(Protocol):"); + assert_contains( + &folder_view, + "def create_file_async_overload_default_options(self, desired_name: str) -> DynWinRTValue | None: ...", + ); + + // Runtime annotations stay pessimistic. + assert_contains( + &folder_py, + "def get_folder_from_path_async(path: str) -> WinRTCoroutine[StorageFolder | None]:", + ); + assert_contains( + &folder_py, + "def properties(self) -> StorageItemContentProperties | None:", + ); +} diff --git a/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs b/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs index 739bda9b..01fbc7d6 100644 --- a/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs +++ b/tools/dynwinrt-codegen/tests/python_symbol_mapping_test.rs @@ -331,7 +331,7 @@ fn python_class_self_and_base_markers_use_declarations_in_both_layouts() { "fixture must require a local self binding" ); assert!( - stub.contains("def echo_self(self, value: 'WidgetLike') -> Widget | None:"), + stub.contains("def echo_self(self, value: 'WidgetLike') -> Widget:"), "{stub}" ); assert!( @@ -375,13 +375,13 @@ fn python_class_self_and_base_markers_use_declarations_in_both_layouts() { def alpha(value: AlphaWidget, peer: BetaWidget, child: AlphaDerived) -> None: own: AlphaBaseLike = value foreign: BetaBaseLike = child - assert_type(value.echo_self(value), AlphaWidget | None) - assert_type(value.echo_foreign(peer), BetaWidget | None) + assert_type(value.echo_self(value), AlphaWidget) + assert_type(value.echo_foreign(peer), BetaWidget) def beta(value: BetaWidget, peer: AlphaWidget, child: BetaDerived) -> None: own: BetaBaseLike = value foreign: AlphaBaseLike = child - assert_type(value.echo_self(value), BetaWidget | None) - assert_type(value.echo_foreign(peer), AlphaWidget | None) + assert_type(value.echo_self(value), BetaWidget) + assert_type(value.echo_foreign(peer), AlphaWidget) "#, ); support(&package, &[]); @@ -587,13 +587,13 @@ fn python_named_class_closed_generic_collision_uses_allocated_declaration() { from typing import assert_type assert_type(Named(), Named) assert_type(Named(7), Named) -assert_type(Named.get_current(), Named | None) +assert_type(Named.get_current(), Named) def check(named: Named, generic: Bucket, derived: Derived) -> None: base: NamedLike = derived - assert_type(named.echo_self(named), Named | None) - assert_type(named.echo_generic(generic), Bucket | None) - assert_type(generic.echo_self(generic), Bucket | None) - assert_type(generic.echo_named(named), Named | None) + assert_type(named.echo_self(named), Named) + assert_type(named.echo_generic(generic), Bucket) + assert_type(generic.echo_self(generic), Bucket) + assert_type(generic.echo_named(named), Named) "#, ), ); @@ -2172,8 +2172,8 @@ fn python_cross_role_class_owners_keep_self_bindings_and_qualified_helpers() { r#"{imports} from typing import assert_type def check(value: Owner) -> None: - assert_type(value.echo_self(value), Owner | None) - assert_type(value.get_self(), Owner | None) + assert_type(value.echo_self(value), Owner) + assert_type(value.get_self(), Owner) assert_type(value.echo_payload(URLValue(17)), URLValue) "# ), @@ -2705,9 +2705,9 @@ fn python_enum_closed_generic_collision_preserves_projection_and_native_identity from typing import assert_type assert_type(RootKind(0), Kind) def check(local: IUse, foreign: IForeign) -> None: - assert_type(local.get_bucket(), Bucket | None) - assert_type(local.get_bucket(), RootBucket | None) - assert_type(foreign.get_bucket(), Bucket | None) + assert_type(local.get_bucket(), Bucket) + assert_type(local.get_bucket(), RootBucket) + assert_type(foreign.get_bucket(), Bucket) assert_type(local.echo_kind(Kind.Unknown), Kind) assert_type(foreign.echo_kind(Kind.Unknown), Kind) assert_type(local.echo_kinds([Kind.Unknown]), list[Kind]) @@ -2911,10 +2911,10 @@ fn python_class_companion_aliases_preserve_cli_and_standalone_contracts() { r#"{imports} from typing import assert_type def check(owner: Widget, peer: Peer, use: IUse) -> None: - assert_type(owner.echo(peer), Peer | None) - assert_type(peer.echo(owner), Widget | None) - assert_type(use.echo_owner(owner), Widget | None) - assert_type(use.echo_peer(peer), Peer | None) + assert_type(owner.echo(peer), Peer) + assert_type(peer.echo(owner), Widget) + assert_type(use.echo_owner(owner), Widget) + assert_type(use.echo_peer(peer), Peer) "#, ), ); diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi index ae2e8fc3..4194cea1 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi @@ -19,25 +19,25 @@ IID_IIterator_IWwwFormUrlDecoderEntry: WinGUID class _IIterator_IWwwFormUrlDecoderEntryIdentity(Protocol): def _dynwinrt_iid_g2bb0b33cef11eb455ace3197fce2b56f216f6802e23b208c88ebad8f950f8628(self) -> None: ... -class IIterator_IWwwFormUrlDecoderEntry(_IIterator_IWwwFormUrlDecoderEntryIdentity, Iterator[IWwwFormUrlDecoderEntry | None]): +class IIterator_IWwwFormUrlDecoderEntry(_IIterator_IWwwFormUrlDecoderEntryIdentity, Iterator[IWwwFormUrlDecoderEntry]): @builtins.property def _obj(self) -> DynWinRTValue: ... # Windows.Foundation.Collections.IIterator_IWwwFormUrlDecoderEntry cannot be implemented: generic interface implementations are not supported def __init__(self, obj: DynWinRTValue) -> None: ... - def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... - def __next__(self) -> IWwwFormUrlDecoderEntry | None: ... + def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... + def __next__(self) -> IWwwFormUrlDecoderEntry: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... @builtins.property - def current(self) -> IWwwFormUrlDecoderEntry | None: ... + def current(self) -> IWwwFormUrlDecoderEntry: ... @builtins.property def has_current(self) -> bool: ... def move_next(self) -> bool: ... - def get_many(self, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... + def get_many(self, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi index 815ffece..728dceb6 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/uri.pyi @@ -60,7 +60,7 @@ class UriLike(_UriIdentity, Protocol): def query(self) -> str: ... @builtins.property - def query_parsed(self) -> WwwFormUrlDecoder | None: ... + def query_parsed(self) -> WwwFormUrlDecoder: ... @builtins.property def raw_uri(self) -> str: ... @@ -79,7 +79,7 @@ class UriLike(_UriIdentity, Protocol): def equals(self, p_uri: 'UriLike') -> bool: ... - def combine_uri(self, relative_uri: str) -> Uri | None: ... + def combine_uri(self, relative_uri: str) -> Uri: ... @builtins.property def absolute_canonical_uri(self) -> str: ... diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi index 46688389..c223db0a 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi @@ -34,48 +34,48 @@ class WwwFormUrlDecoderLike(_WwwFormUrlDecoderIdentity, Protocol): def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... def get_first_value_by_name(self, name: str) -> str: ... @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None] | None: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... -class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderEntry | None], _DynWinRTRuntimeClass): +class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderEntry], _DynWinRTRuntimeClass): def __init__(self, query: str) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... def get_first_value_by_name(self, name: str) -> str: ... @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None] | None: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... @@ -83,16 +83,16 @@ class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderE def create_www_form_url_decoder(query: str) -> 'WwwFormUrlDecoder': ... -class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry | None]): +class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry]): def __init__(self, obj: DynWinRTValue) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... @@ -101,22 +101,22 @@ class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry | Non @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... -class IIterable_IWwwFormUrlDecoderEntry(Iterable[IWwwFormUrlDecoderEntry | None]): +class IIterable_IWwwFormUrlDecoderEntry(Iterable[IWwwFormUrlDecoderEntry]): def __init__(self, obj: DynWinRTValue) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... - def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... + def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None] | None: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... From 1125db1a90dac47af65c833e932c46528605abd9 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 13:28:11 +0800 Subject: [PATCH 03/12] Drop None guards the new stub policy makes unnecessary in Python samples 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> --- samples/python/async-file-io/app.py | 5 ----- samples/python/cryptography/app.py | 4 ---- samples/python/device-watcher/app.py | 2 -- samples/python/ocr-image/app.py | 11 +---------- samples/python/text-to-speech/app.py | 2 -- 5 files changed, 1 insertion(+), 23 deletions(-) diff --git a/samples/python/async-file-io/app.py b/samples/python/async-file-io/app.py index 6cd7111d..83f015a7 100644 --- a/samples/python/async-file-io/app.py +++ b/samples/python/async-file-io/app.py @@ -15,12 +15,7 @@ async def run() -> None: with tempfile.TemporaryDirectory(prefix="dynwinrt-python-") as directory: with RoApartment(1), projected_lifetime_scope(): folder = await StorageFolder.get_folder_from_path_async(directory) - if folder is None: - raise RuntimeError("StorageFolder returned no temporary folder") - file = await folder.create_file_async("sample.txt") - if file is None: - raise RuntimeError("StorageFolder returned no file") await FileIO.write_text_async(file, "Hello from dynwinrt.") await FileIO.append_text_async( file, diff --git a/samples/python/cryptography/app.py b/samples/python/cryptography/app.py index e0a7c5c3..fbae2298 100644 --- a/samples/python/cryptography/app.py +++ b/samples/python/cryptography/app.py @@ -9,12 +9,8 @@ def sha256(text: str) -> str: with RoApartment(1), projected_lifetime_scope(): provider = HashAlgorithmProvider.open_algorithm("SHA256") - if provider is None: - raise RuntimeError("SHA256 provider is unavailable") data = IBuffer.from_bytes(text.encode("utf-8")) digest = provider.hash_data(data) - if digest is None: - raise RuntimeError("HashAlgorithmProvider returned no digest") copied_digest = digest.to_bytes() expected_length = provider.hash_length if len(copied_digest) != expected_length: diff --git a/samples/python/device-watcher/app.py b/samples/python/device-watcher/app.py index 83f5578e..f329c513 100644 --- a/samples/python/device-watcher/app.py +++ b/samples/python/device-watcher/app.py @@ -15,8 +15,6 @@ async def enumerate_devices(timeout: int, show_names: bool) -> None: with RoApartment(1), projected_lifetime_scope(): watcher = DeviceInformation.create_watcher() - if watcher is None: - raise RuntimeError("DeviceInformation returned no watcher") loop = asyncio.get_running_loop() enumeration_completed = asyncio.Event() diff --git a/samples/python/ocr-image/app.py b/samples/python/ocr-image/app.py index 6cd4c9cc..b5beb506 100644 --- a/samples/python/ocr-image/app.py +++ b/samples/python/ocr-image/app.py @@ -17,30 +17,21 @@ def normalized_words(value: str) -> set[str]: async def recognize(path: Path) -> str: with RoApartment(1), projected_lifetime_scope(): file = await StorageFile.get_file_from_path_async(str(path.resolve())) - if file is None: - raise RuntimeError("StorageFile returned no image file") stream = await file.open_read_async() - if stream is None: - raise RuntimeError("StorageFile returned no image stream") decoder = await BitmapDecoder.create_async( stream.as_interface(IRandomAccessStream) ) - if decoder is None: - raise RuntimeError("BitmapDecoder returned no decoder") bitmap = await decoder.get_software_bitmap_async() - if bitmap is None: - raise RuntimeError("BitmapDecoder returned no SoftwareBitmap") with bitmap: + # Try* members return None instead of raising when nothing matches. engine = OcrEngine.try_create_from_user_profile_languages() if engine is None: raise RuntimeError( "No OCR engine is available for the user profile languages" ) result = await engine.recognize_async(bitmap) - if result is None: - raise RuntimeError("OcrEngine returned no result") return result.text diff --git a/samples/python/text-to-speech/app.py b/samples/python/text-to-speech/app.py index c3267524..3321e080 100644 --- a/samples/python/text-to-speech/app.py +++ b/samples/python/text-to-speech/app.py @@ -13,8 +13,6 @@ async def speak(text: str, smoke: bool) -> None: with RoApartment(1), projected_lifetime_scope(): with SpeechSynthesizer() as synthesizer: stream = await synthesizer.synthesize_text_to_stream_async(text) - if stream is None: - raise RuntimeError("SpeechSynthesizer returned no stream") with stream: if smoke: From 44ab63a7b7a18dc283238e370c45b46a6b188177 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 16:59:35 +0800 Subject: [PATCH 04/12] Keep | None for documented nulls and mutable collection elements 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> --- bindings/py/README.md | 22 +- .../windows-null-results.overrides.txt | 17 + .../api-docs/windows-null-results.txt | 939 ++++++++++++++++++ .../scripts/extract-null-results.py | 250 +++++ .../src/codegen/winrt/python/method.rs | 2 +- .../src/codegen/winrt/python/nullability.rs | 196 +++- .../src/codegen/winrt/python/stub_helpers.rs | 21 +- .../src/codegen/winrt/python/stubs.rs | 42 +- .../src/codegen/winrt/python/type_helpers.rs | 131 ++- .../dynwinrt-codegen/src/documented_nulls.rs | 324 ++++++ tools/dynwinrt-codegen/src/lib.rs | 1 + tools/dynwinrt-codegen/src/meta.rs | 99 +- .../tests/python_consumer_typing_test.rs | 21 +- .../tests/python_stub_nullability_test.rs | 128 ++- 14 files changed, 2102 insertions(+), 91 deletions(-) create mode 100644 tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt create mode 100644 tools/dynwinrt-codegen/api-docs/windows-null-results.txt create mode 100644 tools/dynwinrt-codegen/scripts/extract-null-results.py create mode 100644 tools/dynwinrt-codegen/src/documented_nulls.rs diff --git a/bindings/py/README.md b/bindings/py/README.md index a079c2fc..77835d7a 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -24,19 +24,33 @@ Generated `IReference` values are projected as `T | None`; native values, WinRT metadata does not record which values can be null, and most APIs raise an exception instead of returning null. The generated `.pyi` stubs therefore type the values you receive as non-null by default: method and property -results, async results, out values, and the elements, keys and values of -returned collections. For example, `StorageFolder.create_file_async()` returns -`WinRTCoroutine[StorageFile]` and `get_files_async()` returns -`WinRTCoroutine[Sequence[StorageFile]]`. +results, async results and out values. For example, +`StorageFolder.create_file_async()` returns `WinRTCoroutine[StorageFile]`. These values keep `| None`: - `IReference` values, projected as `T | None` everywhere; - results of `Try*` members, such as `try_get_item_async()` or `JsonObject.try_parse()`, where null means "not found"; +- results of Windows SDK members whose documentation says they can return + null, such as `Accelerometer.get_default()`, + `DispatcherQueue.get_for_current_thread()` or + `StorageFolder.get_parent_async()`. The codegen embeds this list, derived + from the Windows SDK API reference; it does not cover Windows App SDK + (`Microsoft.*`) APIs; - `Object`/`IInspectable` values (`DynWinRTValue | None`) and delegate-typed values, which are often null. +Collection elements follow the collection holding them. Anyone can store null +in a mutable `IVector`, `IMap` or observable collection, so their elements, +item positions (`[index]`, iteration, `get_at()`, `lookup()`) and +`items()`/`values()` are typed `T | None`: a `JsonArray` holds +`IJsonValue | None`. Read-only views, iterators and arrays keep non-null +elements: `get_files_async()` returns `WinRTCoroutine[Sequence[StorageFile]]`. +A view, iterator or key-value pair obtained from a mutable collection, such as +the result of `get_view()` or `first()`, can still contain nulls although its +elements are typed non-null. + Arguments keep accepting `None` where they did before. The stubs are optimistic, like the generated TypeScript declarations: the runtime still returns `None` when a WinRT API returns null, so check the API documentation diff --git a/tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt b/tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt new file mode 100644 index 00000000..912ee303 --- /dev/null +++ b/tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt @@ -0,0 +1,17 @@ +# Reviewed corrections for windows-null-results.txt, applied last by +# scripts/extract-null-results.py. "+pattern" adds and "-pattern" removes +# members. A pattern is an api-id that may use fnmatch wildcards; it must match +# a member documented at the pinned winrt-api commit. + +# Sensors report a missing device through null across the whole family, as +# most of their pages state ("or null if no integrated ... are found"). ++M:Windows.Devices.Sensors.*.GetDefault* + +# Device lookups complete with null when the device is missing or access is +# denied, as many of their pages state. ++M:Windows.Devices.*.FromIdAsync(*) ++M:Windows.Devices.*.GetDefaultAsync* + +# Overridable implementation callbacks, not results that consumers receive. +-M:Windows.UI.Xaml.Automation.Peers.AutomationPeer.GetPatternCore(*) +-M:Windows.UI.Xaml.Controls.StyleSelector.SelectStyleCore(*) diff --git a/tools/dynwinrt-codegen/api-docs/windows-null-results.txt b/tools/dynwinrt-codegen/api-docs/windows-null-results.txt new file mode 100644 index 00000000..186c0eb5 --- /dev/null +++ b/tools/dynwinrt-codegen/api-docs/windows-null-results.txt @@ -0,0 +1,939 @@ +# Windows SDK members whose documented result can be null: a method's +# return value (for asynchronous methods, the completed result) or a +# property's value. dynwinrt-codegen keeps `| None` on these outputs. +# Source: https://github.com/MicrosoftDocs/winrt-api at commit 8448d5eecfbc2ed903f659f350841dcb4888bc8b +# Generated by scripts/extract-null-results.py; do not edit. Reviewed +# corrections belong in windows-null-results.overrides.txt. +M:Windows.AI.MachineLearning.TensorString.CreateReference +M:Windows.ApplicationModel.AppInstance.GetActivatedEventArgs +M:Windows.ApplicationModel.Calls.PhoneCall.GetFromId(System.String) +M:Windows.ApplicationModel.Calls.PhoneLineTransportDevice.FromId(System.String) +M:Windows.ApplicationModel.Contacts.ContactStore.GetContactListAsync(System.String) +M:Windows.ApplicationModel.ConversationalAgent.ActivationSignalDetectionConfiguration.GetModelData +M:Windows.ApplicationModel.ConversationalAgent.ActivationSignalDetectionConfiguration.GetModelDataAsync +M:Windows.ApplicationModel.ConversationalAgent.ActivationSignalDetectionConfiguration.GetModelDataType +M:Windows.ApplicationModel.ConversationalAgent.ActivationSignalDetectionConfiguration.GetModelDataTypeAsync +M:Windows.ApplicationModel.Package.GetContentGroupAsync(System.String) +M:Windows.ApplicationModel.UserDataAccounts.UserDataAccountManager.RequestStoreAsync(Windows.ApplicationModel.UserDataAccounts.UserDataAccountStoreAccessType) +M:Windows.ApplicationModel.UserDataTasks.UserDataTaskStore.GetListAsync(System.String) +M:Windows.ApplicationModel.Wallet.WalletItemStore.GetWalletItemAsync(System.String) +M:Windows.Data.Xml.Dom.DtdEntity.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdEntity.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdEntity.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdEntity.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdEntity.SelectNodesNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.DtdEntity.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.DtdEntity.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.DtdNotation.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdNotation.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdNotation.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdNotation.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.DtdNotation.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.DtdNotation.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.IXmlNode.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.IXmlNode.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.IXmlNode.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.IXmlNode.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.IXmlNodeSelector.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.IXmlNodeSelector.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlAttribute.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlAttribute.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlAttribute.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlAttribute.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlAttribute.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlAttribute.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlCDataSection.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlCDataSection.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlCDataSection.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlCDataSection.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlCDataSection.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlCDataSection.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlComment.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlComment.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlComment.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlComment.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlComment.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlComment.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlDocument.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocument.GetElementById(System.String) +M:Windows.Data.Xml.Dom.XmlDocument.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocument.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocument.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocument.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlDocument.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlDocumentFragment.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlDocumentType.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentType.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentType.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentType.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlDocumentType.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlDocumentType.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlElement.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlElement.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlElement.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlElement.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlElement.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlElement.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlElement.SetAttributeNodeNS(Windows.Data.Xml.Dom.XmlAttribute) +M:Windows.Data.Xml.Dom.XmlEntityReference.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlEntityReference.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlEntityReference.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlEntityReference.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlEntityReference.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlEntityReference.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.GetNamedItem(System.String) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.GetNamedItemNS(System.Object,System.String) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.Item(System.UInt32) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.RemoveNamedItem(System.String) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.RemoveNamedItemNS(System.Object,System.String) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.SetNamedItem(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlNamedNodeMap.SetNamedItemNS(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlNodeList.Item(System.UInt32) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlProcessingInstruction.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Data.Xml.Dom.XmlText.AppendChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlText.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlText.RemoveChild(Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlText.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode) +M:Windows.Data.Xml.Dom.XmlText.SelectSingleNode(System.String) +M:Windows.Data.Xml.Dom.XmlText.SelectSingleNodeNS(System.String,System.Object) +M:Windows.Devices.Adc.AdcController.GetDefaultAsync +M:Windows.Devices.AllJoyn.AllJoynServiceInfo.FromIdAsync(System.String) +M:Windows.Devices.Bluetooth.BluetoothAdapter.FromIdAsync(System.String) +M:Windows.Devices.Bluetooth.BluetoothAdapter.GetDefaultAsync +M:Windows.Devices.Bluetooth.BluetoothDevice.FromBluetoothAddressAsync(System.UInt64) +M:Windows.Devices.Bluetooth.BluetoothDevice.FromIdAsync(System.String) +M:Windows.Devices.Bluetooth.BluetoothLEDevice.FromBluetoothAddressAsync(System.UInt64) +M:Windows.Devices.Bluetooth.BluetoothLEDevice.FromBluetoothAddressAsync(System.UInt64,Windows.Devices.Bluetooth.BluetoothAddressType) +M:Windows.Devices.Bluetooth.BluetoothLEDevice.FromIdAsync(System.String) +M:Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService.FromIdAsync(System.String) +M:Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService.FromIdAsync(System.String,Windows.Devices.Bluetooth.GenericAttributeProfile.GattSharingMode) +M:Windows.Devices.Bluetooth.Rfcomm.RfcommDeviceService.FromIdAsync(System.String) +M:Windows.Devices.Custom.CustomDevice.FromIdAsync(System.String,Windows.Devices.Custom.DeviceAccessMode,Windows.Devices.Custom.DeviceSharingMode) +M:Windows.Devices.Display.Core.DisplayTarget.TryGetMonitor +M:Windows.Devices.Display.DisplayMonitor.FromIdAsync(System.String) +M:Windows.Devices.Gpio.GpioController.GetDefault +M:Windows.Devices.Gpio.GpioController.GetDefaultAsync +M:Windows.Devices.Haptics.InputHapticsManager.TryGetForThread(System.UInt32) +M:Windows.Devices.Haptics.VibrationDevice.FromIdAsync(System.String) +M:Windows.Devices.Haptics.VibrationDevice.GetDefaultAsync +M:Windows.Devices.HumanInterfaceDevice.HidDevice.FromIdAsync(System.String,Windows.Storage.FileAccessMode) +M:Windows.Devices.I2c.I2cController.GetDefaultAsync +M:Windows.Devices.I2c.I2cDevice.FromIdAsync(System.String,Windows.Devices.I2c.I2cConnectionSettings) +M:Windows.Devices.I2c.II2cDeviceStatics.FromIdAsync(System.String,Windows.Devices.I2c.I2cConnectionSettings) +M:Windows.Devices.Input.PenDevice.GetFromPointerId(System.UInt32) +M:Windows.Devices.Lights.Lamp.FromIdAsync(System.String) +M:Windows.Devices.Lights.Lamp.GetDefaultAsync +M:Windows.Devices.Lights.LampArray.FromIdAsync(System.String) +M:Windows.Devices.Midi.MidiInPort.FromIdAsync(System.String) +M:Windows.Devices.Midi.MidiOutPort.FromIdAsync(System.String) +M:Windows.Devices.Perception.PerceptionColorFrameReader.TryReadLatestFrame +M:Windows.Devices.Perception.PerceptionColorFrameSource.AcquireControlSession +M:Windows.Devices.Perception.PerceptionColorFrameSource.FromIdAsync(System.String) +M:Windows.Devices.Perception.PerceptionColorFrameSource.TryGetDepthCorrelatedCameraIntrinsicsAsync(Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.PerceptionColorFrameSource.TryGetDepthCorrelatedCoordinateMapperAsync(System.String,Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.PerceptionDepthFrameReader.TryReadLatestFrame +M:Windows.Devices.Perception.PerceptionDepthFrameSource.AcquireControlSession +M:Windows.Devices.Perception.PerceptionDepthFrameSource.FromIdAsync(System.String) +M:Windows.Devices.Perception.PerceptionDepthFrameSource.TryGetDepthCorrelatedCameraIntrinsicsAsync(Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.PerceptionDepthFrameSource.TryGetDepthCorrelatedCoordinateMapperAsync(System.String,Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.PerceptionInfraredFrameReader.TryReadLatestFrame +M:Windows.Devices.Perception.PerceptionInfraredFrameSource.AcquireControlSession +M:Windows.Devices.Perception.PerceptionInfraredFrameSource.FromIdAsync(System.String) +M:Windows.Devices.Perception.PerceptionInfraredFrameSource.TryGetDepthCorrelatedCameraIntrinsicsAsync(Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.PerceptionInfraredFrameSource.TryGetDepthCorrelatedCoordinateMapperAsync(System.String,Windows.Devices.Perception.PerceptionDepthFrameSource) +M:Windows.Devices.Perception.Provider.IPerceptionFrameProviderManager.GetFrameProvider(Windows.Devices.Perception.Provider.PerceptionFrameProviderInfo) +M:Windows.Devices.PointOfService.BarcodeScanner.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.BarcodeScanner.GetDefaultAsync +M:Windows.Devices.PointOfService.CashDrawer.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.CashDrawer.GetDefaultAsync +M:Windows.Devices.PointOfService.ClaimedLineDisplay.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.LineDisplay.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.LineDisplay.GetDefaultAsync +M:Windows.Devices.PointOfService.MagneticStripeReader.ClaimReaderAsync +M:Windows.Devices.PointOfService.MagneticStripeReader.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.MagneticStripeReader.GetDefaultAsync +M:Windows.Devices.PointOfService.PosPrinter.FromIdAsync(System.String) +M:Windows.Devices.PointOfService.PosPrinter.GetDefaultAsync +M:Windows.Devices.Power.Battery.FromIdAsync(System.String) +M:Windows.Devices.Printers.Print3DDevice.FromIdAsync(System.String) +M:Windows.Devices.Pwm.PwmController.FromIdAsync(System.String) +M:Windows.Devices.Pwm.PwmController.GetDefaultAsync +M:Windows.Devices.Radios.Radio.FromIdAsync(System.String) +M:Windows.Devices.Scanners.ImageScanner.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Accelerometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Accelerometer.GetDefault +M:Windows.Devices.Sensors.Accelerometer.GetDefault(Windows.Devices.Sensors.AccelerometerReadingType) +M:Windows.Devices.Sensors.Accelerometer.GetDeviceSelector(Windows.Devices.Sensors.AccelerometerReadingType) +M:Windows.Devices.Sensors.ActivitySensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.ActivitySensor.GetDefaultAsync +M:Windows.Devices.Sensors.ActivitySensor.GetDeviceSelector +M:Windows.Devices.Sensors.Altimeter.GetDefault +M:Windows.Devices.Sensors.Barometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Barometer.GetDefault +M:Windows.Devices.Sensors.Barometer.GetDeviceSelector +M:Windows.Devices.Sensors.Compass.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Compass.GetDefault +M:Windows.Devices.Sensors.Compass.GetDeviceSelector +M:Windows.Devices.Sensors.Custom.CustomSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Custom.CustomSensor.GetDeviceSelector(System.Guid) +M:Windows.Devices.Sensors.Gyrometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Gyrometer.GetDefault +M:Windows.Devices.Sensors.Gyrometer.GetDeviceSelector +M:Windows.Devices.Sensors.HingeAngleSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.HingeAngleSensor.GetDefaultAsync +M:Windows.Devices.Sensors.HingeAngleSensor.GetDeviceSelector +M:Windows.Devices.Sensors.HumanPresenceSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.HumanPresenceSensor.GetDefault +M:Windows.Devices.Sensors.HumanPresenceSensor.GetDefaultAsync +M:Windows.Devices.Sensors.Inclinometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Inclinometer.GetDefault +M:Windows.Devices.Sensors.Inclinometer.GetDefault(Windows.Devices.Sensors.SensorReadingType) +M:Windows.Devices.Sensors.Inclinometer.GetDefaultForRelativeReadings +M:Windows.Devices.Sensors.Inclinometer.GetDeviceSelector(Windows.Devices.Sensors.SensorReadingType) +M:Windows.Devices.Sensors.LightSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.LightSensor.GetDefault +M:Windows.Devices.Sensors.LightSensor.GetDeviceSelector +M:Windows.Devices.Sensors.Magnetometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Magnetometer.GetDefault +M:Windows.Devices.Sensors.Magnetometer.GetDeviceSelector +M:Windows.Devices.Sensors.OrientationSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.OrientationSensor.GetDefault +M:Windows.Devices.Sensors.OrientationSensor.GetDefault(Windows.Devices.Sensors.SensorReadingType) +M:Windows.Devices.Sensors.OrientationSensor.GetDefault(Windows.Devices.Sensors.SensorReadingType,Windows.Devices.Sensors.SensorOptimizationGoal) +M:Windows.Devices.Sensors.OrientationSensor.GetDefaultForRelativeReadings +M:Windows.Devices.Sensors.OrientationSensor.GetDeviceSelector(Windows.Devices.Sensors.SensorReadingType) +M:Windows.Devices.Sensors.OrientationSensor.GetDeviceSelector(Windows.Devices.Sensors.SensorReadingType,Windows.Devices.Sensors.SensorOptimizationGoal) +M:Windows.Devices.Sensors.Pedometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Pedometer.GetDefaultAsync +M:Windows.Devices.Sensors.Pedometer.GetDeviceSelector +M:Windows.Devices.Sensors.ProximitySensor.GetDeviceSelector +M:Windows.Devices.Sensors.SimpleOrientationSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.SimpleOrientationSensor.GetDefault +M:Windows.Devices.Sensors.SimpleOrientationSensor.GetDeviceSelector +M:Windows.Devices.SerialCommunication.SerialDevice.FromIdAsync(System.String) +M:Windows.Devices.SmartCards.SmartCardEmulator.GetDefaultAsync +M:Windows.Devices.SmartCards.SmartCardReader.FromIdAsync(System.String) +M:Windows.Devices.Sms.SmsDevice.FromIdAsync(System.String) +M:Windows.Devices.Sms.SmsDevice.GetDefaultAsync +M:Windows.Devices.Spi.ISpiDeviceStatics.FromIdAsync(System.String,Windows.Devices.Spi.SpiConnectionSettings) +M:Windows.Devices.Spi.SpiController.GetDefaultAsync +M:Windows.Devices.Spi.SpiDevice.FromIdAsync(System.String,Windows.Devices.Spi.SpiConnectionSettings) +M:Windows.Devices.Usb.UsbDevice.FromIdAsync(System.String) +M:Windows.Devices.WiFi.WiFiAdapter.FromIdAsync(System.String) +M:Windows.Devices.WiFiDirect.Services.WiFiDirectService.FromIdAsync(System.String) +M:Windows.Devices.WiFiDirect.WiFiDirectDevice.FromIdAsync(System.String) +M:Windows.Devices.WiFiDirect.WiFiDirectDevice.FromIdAsync(System.String,Windows.Devices.WiFiDirect.WiFiDirectConnectionParameters) +M:Windows.Foundation.Diagnostics.FileLoggingSession.CloseAndSaveToFileAsync +M:Windows.Gaming.Input.Custom.GameControllerFactoryManager.TryGetFactoryControllerFromGameController(Windows.Gaming.Input.Custom.ICustomGameControllerFactory,Windows.Gaming.Input.IGameController) +M:Windows.Gaming.Input.Preview.LegacyGipGameControllerProvider.FromGameController(Windows.Gaming.Input.IGameController) +M:Windows.Gaming.Input.Preview.LegacyGipGameControllerProvider.FromGameControllerProvider(Windows.Gaming.Input.Custom.IGameControllerProvider) +M:Windows.Gaming.Input.Preview.LegacyGipGameControllerProvider.IsCopilot(Windows.System.User,System.String) +M:Windows.Gaming.Input.Preview.LegacyGipGameControllerProvider.IsPilot(Windows.System.User,System.String) +M:Windows.Gaming.UI.GameChatOverlay.GetDefault +M:Windows.Gaming.UI.GameMonitor.GetDefault +M:Windows.Globalization.NumberFormatting.CurrencyFormatter.ParseDouble(System.String) +M:Windows.Globalization.NumberFormatting.CurrencyFormatter.ParseInt(System.String) +M:Windows.Globalization.NumberFormatting.CurrencyFormatter.ParseUInt(System.String) +M:Windows.Globalization.NumberFormatting.DecimalFormatter.ParseDouble(System.String) +M:Windows.Globalization.NumberFormatting.DecimalFormatter.ParseInt(System.String) +M:Windows.Globalization.NumberFormatting.DecimalFormatter.ParseUInt(System.String) +M:Windows.Globalization.NumberFormatting.INumberParser.ParseDouble(System.String) +M:Windows.Globalization.NumberFormatting.INumberParser.ParseInt(System.String) +M:Windows.Globalization.NumberFormatting.INumberParser.ParseUInt(System.String) +M:Windows.Globalization.NumberFormatting.PercentFormatter.ParseDouble(System.String) +M:Windows.Globalization.NumberFormatting.PercentFormatter.ParseInt(System.String) +M:Windows.Globalization.NumberFormatting.PercentFormatter.ParseUInt(System.String) +M:Windows.Globalization.NumberFormatting.PermilleFormatter.ParseDouble(System.String) +M:Windows.Globalization.NumberFormatting.PermilleFormatter.ParseInt(System.String) +M:Windows.Globalization.NumberFormatting.PermilleFormatter.ParseUInt(System.String) +M:Windows.Graphics.Capture.Direct3D11CaptureFramePool.TryGetNextFrame +M:Windows.Graphics.Holographic.HolographicCameraPose.TryGetCullingFrustum(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Graphics.Holographic.HolographicCameraPose.TryGetViewTransform(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Graphics.Holographic.HolographicCameraPose.TryGetVisibleFrustum(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Graphics.Holographic.HolographicDisplay.TryGetViewConfiguration(Windows.Graphics.Holographic.HolographicViewConfigurationKind) +M:Windows.Graphics.Printing.Workflow.PrintWorkflowVirtualPrinterDataAvailableEventArgs.GetTargetFileAsync +M:Windows.Management.Deployment.PackageManager.FindPackage(System.String) +M:Windows.Management.Update.PreviewBuildsManager.GetDefault +M:Windows.Management.Update.WindowsUpdate.GetPropertyValue(System.String) +M:Windows.Media.Audio.AudioPlaybackConnection.TryCreateFromId(System.String) +M:Windows.Media.Capture.AppBroadcastStreamReader.TryGetNextAudioFrame +M:Windows.Media.Capture.AppBroadcastStreamReader.TryGetNextVideoFrame +M:Windows.Media.Capture.Frames.DepthMediaFrame.TryCreateCoordinateMapper(Windows.Media.Devices.Core.CameraIntrinsics,Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Media.Capture.Frames.MediaFrameSource.TryGetCameraIntrinsics(Windows.Media.Capture.Frames.MediaFrameFormat) +M:Windows.Media.Devices.CallControl.FromId(System.String) +M:Windows.Media.Devices.CallControl.GetDefault +M:Windows.Media.Ocr.OcrEngine.TryCreateFromLanguage(Windows.Globalization.Language) +M:Windows.Media.Ocr.OcrEngine.TryCreateFromUserProfileLanguages +M:Windows.Networking.Connectivity.NetworkInformation.GetInternetConnectionProfile +M:Windows.Networking.NetworkOperators.ESimManager.TryCreateESimWatcher +M:Windows.Networking.Proximity.ProximityDevice.GetDefault +M:Windows.Networking.XboxLive.XboxLiveEndpointPair.FindEndpointPairByHostNamesAndPorts(Windows.Networking.HostName,System.String,Windows.Networking.HostName,System.String) +M:Windows.Networking.XboxLive.XboxLiveEndpointPair.FindEndpointPairBySocketAddressBytes(System.Byte[],System.Byte[]) +M:Windows.Perception.Spatial.SpatialAnchor.TryCreateRelativeTo(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Perception.Spatial.SpatialAnchor.TryCreateRelativeTo(Windows.Perception.Spatial.SpatialCoordinateSystem,Windows.Foundation.Numerics.Vector3) +M:Windows.Perception.Spatial.SpatialAnchor.TryCreateRelativeTo(Windows.Perception.Spatial.SpatialCoordinateSystem,Windows.Foundation.Numerics.Vector3,Windows.Foundation.Numerics.Quaternion) +M:Windows.Perception.Spatial.SpatialCoordinateSystem.TryGetTransformTo(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Perception.Spatial.SpatialLocatorAttachedFrameOfReference.TryGetRelativeHeadingAtTimestamp(Windows.Perception.PerceptionTimestamp) +M:Windows.Perception.Spatial.SpatialStageFrameOfReference.TryGetMovementBounds(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Perception.Spatial.Surfaces.SpatialSurfaceInfo.TryComputeLatestMeshAsync(System.Double) +M:Windows.Perception.Spatial.Surfaces.SpatialSurfaceInfo.TryGetBounds(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.Services.Maps.Guidance.GuidanceRoute.TryCreateFromMapRoute(Windows.Services.Maps.MapRoute) +M:Windows.Storage.FileProperties.StorageItemContentProperties.RetrievePropertiesAsync(Windows.Foundation.Collections.IIterable{System.String}) +M:Windows.Storage.IStorageItem2.GetParentAsync +M:Windows.Storage.IStorageItemProperties.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.IStorageItemProperties.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.IStorageItemProperties.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.IStorageItemProperties2.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.IStorageItemProperties2.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.IStorageItemProperties2.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.StorageFile.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.StorageFile.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.StorageFile.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.StorageFile.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.StorageFile.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.StorageFile.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.StorageFolder.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.StorageFolder.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.StorageFolder.GetScaledImageAsThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.StorageFolder.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode) +M:Windows.Storage.StorageFolder.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32) +M:Windows.Storage.StorageFolder.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode,System.UInt32,Windows.Storage.FileProperties.ThumbnailOptions) +M:Windows.Storage.StorageFolder.TryGetItemAsync(System.String) +M:Windows.Storage.StorageLibrary.RequestAddFolderAsync +M:Windows.System.AppDiagnosticInfo.LaunchAsync +M:Windows.System.AppUriHandlerRegistrationManager.TryGetRegistration(System.String) +M:Windows.System.DispatcherQueue.GetForCurrentThread +M:Windows.System.RemoteSystems.RemoteSystem.FindByHostNameAsync(Windows.Networking.HostName) +M:Windows.System.User.GetFromId(System.String) +M:Windows.System.User.GetPictureAsync(Windows.System.UserPictureSize) +M:Windows.System.User.GetUserAgeRangeAsync +M:Windows.System.UserPicker.PickSingleUserAsync +M:Windows.System.UserProfile.UserInformation.GetAccountPicture(Windows.System.UserProfile.AccountPictureKind) +M:Windows.System.UserProfile.UserInformation.GetSessionInitiationProtocolUriAsync +M:Windows.UI.Composition.CompositionObject.TryGetAnimationController(System.String) +M:Windows.UI.Composition.Diagnostics.CompositionDebugSettings.TryGetSettings(Windows.UI.Composition.Compositor) +M:Windows.UI.Core.CoreWindow.GetForCurrentThread +M:Windows.UI.Core.CoreWindow.GetKeyState(Windows.System.VirtualKey) +M:Windows.UI.Input.Preview.Injection.InputInjector.TryCreate +M:Windows.UI.Input.Preview.Injection.InputInjector.TryCreateForAppBroadcastOnly +M:Windows.UI.Input.RadialControllerMenu.GetSelectedMenuItem +M:Windows.UI.Input.Spatial.SpatialHoldStartedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialInteractionController.TryGetBatteryReport +M:Windows.UI.Input.Spatial.SpatialInteractionController.TryGetRenderableModelAsync +M:Windows.UI.Input.Spatial.SpatialInteractionDetectedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialInteractionSource.TryCreateHandMeshObserver +M:Windows.UI.Input.Spatial.SpatialInteractionSource.TryCreateHandMeshObserverAsync +M:Windows.UI.Input.Spatial.SpatialInteractionSourceProperties.TryGetLocation(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialInteractionSourceProperties.TryGetSourceLossMitigationDirection(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialInteractionSourceState.TryGetHandPose +M:Windows.UI.Input.Spatial.SpatialInteractionSourceState.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialManipulationCompletedEventArgs.TryGetCumulativeDelta(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialManipulationStartedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialManipulationUpdatedEventArgs.TryGetCumulativeDelta(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialNavigationStartedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialPointerPose.TryGetAtTimestamp(Windows.Perception.Spatial.SpatialCoordinateSystem,Windows.Perception.PerceptionTimestamp) +M:Windows.UI.Input.Spatial.SpatialPointerPose.TryGetInteractionSourcePose(Windows.UI.Input.Spatial.SpatialInteractionSource) +M:Windows.UI.Input.Spatial.SpatialRecognitionStartedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Input.Spatial.SpatialTappedEventArgs.TryGetPointerPose(Windows.Perception.Spatial.SpatialCoordinateSystem) +M:Windows.UI.Notifications.Management.UserNotificationListener.GetNotification(System.UInt32) +M:Windows.UI.Notifications.NotificationVisual.GetBinding(System.String) +M:Windows.UI.Popups.PopupMenu.ShowAsync(Windows.Foundation.Point) +M:Windows.UI.Popups.PopupMenu.ShowForSelectionAsync(Windows.Foundation.Rect) +M:Windows.UI.Popups.PopupMenu.ShowForSelectionAsync(Windows.Foundation.Rect,Windows.UI.Popups.Placement) +M:Windows.UI.Shell.FocusSessionManager.TryStartFocusSession +M:Windows.UI.Shell.FocusSessionManager.TryStartFocusSession(Windows.Foundation.DateTime) +M:Windows.UI.Text.ITextRange.InRange(Windows.UI.Text.ITextRange) +M:Windows.UI.Text.ITextRange.InStory(Windows.UI.Text.ITextRange) +M:Windows.UI.Text.ITextRange.MoveStart(Windows.UI.Text.TextRangeUnit,System.Int32) +M:Windows.UI.Text.RichEditTextRange.InRange(Windows.UI.Text.ITextRange) +M:Windows.UI.Text.RichEditTextRange.InStory(Windows.UI.Text.ITextRange) +M:Windows.UI.Text.RichEditTextRange.MoveStart(Windows.UI.Text.TextRangeUnit,System.Int32) +M:Windows.UI.UIAutomation.Core.AutomationRemoteOperationResult.GetOperand(Windows.UI.UIAutomation.Core.AutomationRemoteOperationOperandId) +M:Windows.UI.Xaml.Automation.Peers.AutomationPeer.GetPattern(Windows.UI.Xaml.Automation.Peers.PatternInterface) +M:Windows.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.CreatePeerForElement(Windows.UI.Xaml.UIElement) +M:Windows.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer.FromElement(Windows.UI.Xaml.UIElement) +M:Windows.UI.Xaml.Automation.Peers.ItemsControlAutomationPeer.FindItemByProperty(Windows.UI.Xaml.Automation.Provider.IRawElementProviderSimple,Windows.UI.Xaml.Automation.AutomationProperty,System.Object) +M:Windows.UI.Xaml.Automation.Peers.LoopingSelectorAutomationPeer.FindItemByProperty(Windows.UI.Xaml.Automation.Provider.IRawElementProviderSimple,Windows.UI.Xaml.Automation.AutomationProperty,System.Object) +M:Windows.UI.Xaml.Automation.Provider.IDragProvider.GetGrabbedItems +M:Windows.UI.Xaml.Automation.Provider.IItemContainerProvider.FindItemByProperty(Windows.UI.Xaml.Automation.Provider.IRawElementProviderSimple,Windows.UI.Xaml.Automation.AutomationProperty,System.Object) +M:Windows.UI.Xaml.Automation.Provider.ITextRangeProvider.FindAttribute(System.Int32,System.Object,System.Boolean) +M:Windows.UI.Xaml.Automation.Provider.ITextRangeProvider.FindText(System.String,System.Boolean,System.Boolean) +M:Windows.UI.Xaml.Controls.Control.GetTemplateChild(System.String) +M:Windows.UI.Xaml.Controls.DataTemplateSelector.GetElement(Windows.UI.Xaml.ElementFactoryGetArgs) +M:Windows.UI.Xaml.Controls.DatePickerFlyoutItem.GetIndexedProperty(System.String,Windows.UI.Xaml.Interop.TypeName) +M:Windows.UI.Xaml.Controls.IItemContainerMapping.ContainerFromIndex(System.Int32) +M:Windows.UI.Xaml.Controls.IItemContainerMapping.ContainerFromItem(System.Object) +M:Windows.UI.Xaml.Controls.InkToolbar.GetToolButton(Windows.UI.Xaml.Controls.InkToolbarTool) +M:Windows.UI.Xaml.Controls.ItemContainerGenerator.ContainerFromIndex(System.Int32) +M:Windows.UI.Xaml.Controls.ItemContainerGenerator.ContainerFromItem(System.Object) +M:Windows.UI.Xaml.Controls.ItemsControl.ContainerFromIndex(System.Int32) +M:Windows.UI.Xaml.Controls.ItemsControl.ContainerFromItem(System.Object) +M:Windows.UI.Xaml.Controls.ItemsControl.GetItemsOwner(Windows.UI.Xaml.DependencyObject) +M:Windows.UI.Xaml.Controls.ItemsControl.ItemsControlFromItemContainer(Windows.UI.Xaml.DependencyObject) +M:Windows.UI.Xaml.Controls.Maps.MapControl.GetVisibleRegion(Windows.UI.Xaml.Controls.Maps.MapVisibleRegionKind) +M:Windows.UI.Xaml.Controls.Maps.StreetsidePanorama.FindNearbyAsync(Windows.Devices.Geolocation.Geopoint) +M:Windows.UI.Xaml.Controls.Maps.StreetsidePanorama.FindNearbyAsync(Windows.Devices.Geolocation.Geopoint,System.Double) +M:Windows.UI.Xaml.Controls.NavigationView.ContainerFromMenuItem(System.Object) +M:Windows.UI.Xaml.Controls.StyleSelector.SelectStyle(System.Object,Windows.UI.Xaml.DependencyObject) +M:Windows.UI.Xaml.Controls.SwapChainPanel.CreateCoreIndependentInputSource(Windows.UI.Core.CoreInputDeviceTypes) +M:Windows.UI.Xaml.Controls.TreeView.ContainerFromItem(System.Object) +M:Windows.UI.Xaml.Controls.TreeView.ContainerFromNode(Windows.UI.Xaml.Controls.TreeViewNode) +M:Windows.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(System.String) +M:Windows.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(System.String,Windows.UI.Xaml.Interop.TypeName) +M:Windows.UI.Xaml.DataTemplate.GetElement(Windows.UI.Xaml.ElementFactoryGetArgs) +M:Windows.UI.Xaml.Documents.TextElement.FindName(System.String) +M:Windows.UI.Xaml.Documents.TextPointer.GetPositionAtOffset(System.Int32,Windows.UI.Xaml.Documents.LogicalDirection) +M:Windows.UI.Xaml.FrameworkElement.FindName(System.String) +M:Windows.UI.Xaml.FrameworkElement.GetBindingExpression(Windows.UI.Xaml.DependencyProperty) +M:Windows.UI.Xaml.Input.FocusManager.FindNextFocusableElement(Windows.UI.Xaml.Input.FocusNavigationDirection) +M:Windows.UI.Xaml.Input.FocusManager.FindNextFocusableElement(Windows.UI.Xaml.Input.FocusNavigationDirection,Windows.Foundation.Rect) +M:Windows.UI.Xaml.Input.FocusManager.GetFocusedElement +M:Windows.UI.Xaml.Input.PointerRoutedEventArgs.GetIntermediatePoints(Windows.UI.Xaml.UIElement) +M:Windows.UI.Xaml.Markup.IXamlType.GetMember(System.String) +M:Windows.UI.Xaml.Media.Animation.ConnectedAnimationService.GetAnimation(System.String) +M:Windows.UI.Xaml.Media.Animation.Storyboard.GetCurrentTime +M:Windows.Web.IUriToStreamResolver.UriToStreamAsync(Windows.Foundation.Uri) +P:Windows.ApplicationModel.Activation.AppointmentsProviderAddAppointmentActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.AppointmentsProviderRemoveAppointmentActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.AppointmentsProviderReplaceAppointmentActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.AppointmentsProviderShowAppointmentDetailsActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.AppointmentsProviderShowTimeFrameActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.CachedFileUpdaterActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.DeviceActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.DeviceActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.DialReceiverActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.FileActivatedEventArgs.NeighboringFilesQuery +P:Windows.ApplicationModel.Activation.FileActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.FileActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.FileOpenPickerActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.FileOpenPickerContinuationEventArgs.User +P:Windows.ApplicationModel.Activation.FileSavePickerActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.FileSavePickerContinuationEventArgs.User +P:Windows.ApplicationModel.Activation.FolderPickerContinuationEventArgs.User +P:Windows.ApplicationModel.Activation.IViewSwitcherProvider.ViewSwitcher +P:Windows.ApplicationModel.Activation.LaunchActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.LockScreenCallActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs.Data +P:Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.ProtocolForResultsActivatedEventArgs.Data +P:Windows.ApplicationModel.Activation.ProtocolForResultsActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.RestrictedLaunchActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.SearchActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.SearchActivatedEventArgs.ViewSwitcher +P:Windows.ApplicationModel.Activation.ShareTargetActivatedEventArgs.User +P:Windows.ApplicationModel.Activation.VoiceCommandActivatedEventArgs.User +P:Windows.ApplicationModel.AppInstallerInfo.PausedUntil +P:Windows.ApplicationModel.AppInstance.RecommendedInstance +P:Windows.ApplicationModel.Appointments.Appointment.Reminder +P:Windows.ApplicationModel.Background.BackgroundTaskRegistration.Trigger +P:Windows.ApplicationModel.Background.BluetoothLEAdvertisementPublisherTrigger.PreferredTransmitPowerLevelInDBm +P:Windows.ApplicationModel.Background.IBackgroundTaskRegistration2.Trigger +P:Windows.ApplicationModel.Background.RfcommConnectionTrigger.InboundConnection +P:Windows.ApplicationModel.Background.RfcommConnectionTrigger.OutboundConnection +P:Windows.ApplicationModel.Calls.PhoneLineDialResult.DialedCall +P:Windows.ApplicationModel.Package.MachineExternalLocation +P:Windows.ApplicationModel.Package.UserExternalLocation +P:Windows.ApplicationModel.PackageCatalogAddOptionalPackageResult.ExtendedError +P:Windows.ApplicationModel.PackageCatalogAddOptionalPackageResult.Package +P:Windows.ApplicationModel.PackageId.ResourceId +P:Windows.ApplicationModel.Search.Core.SearchSuggestion.DetailText +P:Windows.ApplicationModel.Search.Core.SearchSuggestion.Image +P:Windows.ApplicationModel.Search.Core.SearchSuggestion.ImageAlternateText +P:Windows.ApplicationModel.Search.Core.SearchSuggestion.Tag +P:Windows.ApplicationModel.UserDataAccounts.UserDataAccount.EnterpriseId +P:Windows.ApplicationModel.UserDataAccounts.UserDataAccount.Icon +P:Windows.ApplicationModel.UserDataTasks.UserDataTask.Reminder +P:Windows.ApplicationModel.Wallet.WalletItem.ExpirationDate +P:Windows.ApplicationModel.Wallet.WalletItem.LastUpdated +P:Windows.ApplicationModel.Wallet.WalletItem.RelevantDate +P:Windows.ApplicationModel.Wallet.WalletTransaction.TransactionDate +P:Windows.Data.Xml.Dom.DtdEntity.FirstChild +P:Windows.Data.Xml.Dom.DtdEntity.LastChild +P:Windows.Data.Xml.Dom.DtdEntity.NextSibling +P:Windows.Data.Xml.Dom.DtdEntity.NodeValue +P:Windows.Data.Xml.Dom.DtdNotation.FirstChild +P:Windows.Data.Xml.Dom.DtdNotation.LastChild +P:Windows.Data.Xml.Dom.DtdNotation.NextSibling +P:Windows.Data.Xml.Dom.DtdNotation.NodeValue +P:Windows.Data.Xml.Dom.IXmlNode.Attributes +P:Windows.Data.Xml.Dom.IXmlNode.FirstChild +P:Windows.Data.Xml.Dom.IXmlNode.LastChild +P:Windows.Data.Xml.Dom.XmlAttribute.FirstChild +P:Windows.Data.Xml.Dom.XmlAttribute.LastChild +P:Windows.Data.Xml.Dom.XmlAttribute.NextSibling +P:Windows.Data.Xml.Dom.XmlCDataSection.ChildNodes +P:Windows.Data.Xml.Dom.XmlCDataSection.FirstChild +P:Windows.Data.Xml.Dom.XmlCDataSection.LastChild +P:Windows.Data.Xml.Dom.XmlComment.ChildNodes +P:Windows.Data.Xml.Dom.XmlComment.FirstChild +P:Windows.Data.Xml.Dom.XmlComment.LastChild +P:Windows.Data.Xml.Dom.XmlDocument.FirstChild +P:Windows.Data.Xml.Dom.XmlDocument.LastChild +P:Windows.Data.Xml.Dom.XmlDocument.ParentNode +P:Windows.Data.Xml.Dom.XmlDocumentFragment.Attributes +P:Windows.Data.Xml.Dom.XmlDocumentFragment.FirstChild +P:Windows.Data.Xml.Dom.XmlDocumentFragment.LastChild +P:Windows.Data.Xml.Dom.XmlDocumentType.Attributes +P:Windows.Data.Xml.Dom.XmlDocumentType.FirstChild +P:Windows.Data.Xml.Dom.XmlDocumentType.LastChild +P:Windows.Data.Xml.Dom.XmlElement.Attributes +P:Windows.Data.Xml.Dom.XmlElement.FirstChild +P:Windows.Data.Xml.Dom.XmlElement.LastChild +P:Windows.Data.Xml.Dom.XmlEntityReference.Attributes +P:Windows.Data.Xml.Dom.XmlEntityReference.FirstChild +P:Windows.Data.Xml.Dom.XmlEntityReference.LastChild +P:Windows.Data.Xml.Dom.XmlProcessingInstruction.Attributes +P:Windows.Data.Xml.Dom.XmlProcessingInstruction.FirstChild +P:Windows.Data.Xml.Dom.XmlProcessingInstruction.LastChild +P:Windows.Data.Xml.Dom.XmlText.Attributes +P:Windows.Data.Xml.Dom.XmlText.FirstChild +P:Windows.Data.Xml.Dom.XmlText.LastChild +P:Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementPublisher.PreferredTransmitPowerLevelInDBm +P:Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService.ParentServices +P:Windows.Devices.Display.Core.DisplayTarget.DeviceInterfacePath +P:Windows.Devices.Display.DisplayMonitor.BluePrimary +P:Windows.Devices.Display.DisplayMonitor.DeviceId +P:Windows.Devices.Display.DisplayMonitor.DisplayAdapterDeviceId +P:Windows.Devices.Display.DisplayMonitor.DisplayAdapterId +P:Windows.Devices.Display.DisplayMonitor.DisplayName +P:Windows.Devices.Display.DisplayMonitor.GreenPrimary +P:Windows.Devices.Display.DisplayMonitor.PhysicalSizeInInches +P:Windows.Devices.Display.DisplayMonitor.RedPrimary +P:Windows.Devices.Display.DisplayMonitor.WhitePoint +P:Windows.Devices.Enumeration.DeviceInformation.EnclosureLocation +P:Windows.Devices.Geolocation.GeocoordinateSatelliteData.GeometricDilutionOfPrecision +P:Windows.Devices.Geolocation.GeocoordinateSatelliteData.TimeDilutionOfPrecision +P:Windows.Devices.Geolocation.Geoposition.CivicAddress +P:Windows.Devices.Geolocation.Geoposition.VenueData +P:Windows.Devices.Haptics.InputHapticsManager.CurrentHapticsController +P:Windows.Devices.PointOfService.BarcodeScannerReport.ScanDataLabel +P:Windows.Devices.PointOfService.CashDrawer.DrawerEventSource +P:Windows.Devices.PointOfService.ClaimedLineDisplay.CustomGlyphs +P:Windows.Devices.PointOfService.ClaimedPosPrinter.Journal +P:Windows.Devices.PointOfService.ClaimedPosPrinter.Receipt +P:Windows.Devices.PointOfService.ClaimedPosPrinter.Slip +P:Windows.Devices.Scanners.ImageScanner.AutoConfiguration +P:Windows.Devices.Scanners.ImageScanner.FeederConfiguration +P:Windows.Devices.Scanners.ImageScanner.FlatbedConfiguration +P:Windows.Devices.Sensors.AccelerometerReading.PerformanceCount +P:Windows.Devices.Sensors.AltimeterReading.PerformanceCount +P:Windows.Devices.Sensors.BarometerReading.PerformanceCount +P:Windows.Devices.Sensors.CompassReading.PerformanceCount +P:Windows.Devices.Sensors.Custom.CustomSensorReading.PerformanceCount +P:Windows.Devices.Sensors.GyrometerReading.PerformanceCount +P:Windows.Devices.Sensors.InclinometerReading.PerformanceCount +P:Windows.Devices.Sensors.LightSensorReading.PerformanceCount +P:Windows.Devices.Sensors.MagnetometerReading.PerformanceCount +P:Windows.Devices.Sensors.OrientationSensorReading.PerformanceCount +P:Windows.Devices.SmartCards.SmartCardCryptogramPlacementStep.Algorithm +P:Windows.Devices.Usb.UsbBulkInEndpointDescriptor.Pipe +P:Windows.Devices.WiFi.WiFiOnDemandHotspotNetworkProperties.RemainingBatteryPercent +P:Windows.Devices.WiFiDirect.Services.WiFiDirectServiceAutoAcceptSessionConnectedEventArgs.SessionInfo +P:Windows.Devices.WiFiDirect.WiFiDirectAdvertisement.InformationElements +P:Windows.Gaming.Input.RacingWheel.WheelMotor +P:Windows.Globalization.Fonts.LanguageFontGroup.DocumentAlternate1Font +P:Windows.Globalization.Fonts.LanguageFontGroup.DocumentAlternate2Font +P:Windows.Globalization.Fonts.LanguageFontGroup.FixedWidthTextFont +P:Windows.Graphics.Display.DisplayEnhancementOverride.BrightnessOverrideSettings +P:Windows.Graphics.Display.DisplayEnhancementOverride.ColorOverrideSettings +P:Windows.Graphics.Display.DisplayInformation.DiagonalSizeInInches +P:Windows.Media.Audio.AudioFileInputNode.EndTime +P:Windows.Media.Audio.AudioFileInputNode.StartTime +P:Windows.Media.Audio.AudioGraphSettings.PrimaryRenderDevice +P:Windows.Media.Audio.MediaSourceAudioInputNode.EndTime +P:Windows.Media.Audio.MediaSourceAudioInputNode.StartTime +P:Windows.Media.Capture.AdvancedCapturedPhoto.FrameBoundsRelativeToReferencePhoto +P:Windows.Media.Capture.Frames.MediaFrameFormat.VideoFormat +P:Windows.Media.Capture.Frames.MediaFrameReference.AudioMediaFrame +P:Windows.Media.Capture.Frames.MediaFrameReference.BufferMediaFrame +P:Windows.Media.Capture.Frames.MediaFrameReference.VideoMediaFrame +P:Windows.Media.Capture.Frames.MediaFrameSourceGetPropertyResult.Value +P:Windows.Media.Capture.Frames.VideoMediaFrame.SoftwareBitmap +P:Windows.Media.Capture.Frames.VideoMediaFrameFormat.DepthFormat +P:Windows.Media.Capture.MediaCaptureInitializationSettings.AudioDeviceId +P:Windows.Media.Capture.MediaCaptureInitializationSettings.VideoDeviceId +P:Windows.Media.Capture.MediaCaptureSettings.AudioDeviceId +P:Windows.Media.Capture.MediaCaptureSettings.VideoDeviceId +P:Windows.Media.Core.MediaSource.AdaptiveMediaSource +P:Windows.Media.Core.MediaSource.MediaStreamSource +P:Windows.Media.Core.MediaSource.MseStreamSource +P:Windows.Media.Core.MediaStreamSourceSampleRequest.Sample +P:Windows.Media.Core.MediaStreamSourceStartingRequest.StartPosition +P:Windows.Media.Devices.VideoDeviceControllerGetDevicePropertyResult.Value +P:Windows.Media.Effects.AudioEffect.AcousticEchoCancellationConfiguration +P:Windows.Media.MediaProperties.MediaEncodingProfile.Audio +P:Windows.Media.MediaProperties.MediaEncodingProfile.Video +P:Windows.Media.Ocr.OcrResult.TextAngle +P:Windows.Media.Protection.PlayReady.PlayReadyContentHeader.HeaderWithEmbeddedUpdates +P:Windows.Media.Protection.PlayReady.PlayReadyIndividualizationServiceRequest.ChallengeCustomData +P:Windows.Media.Protection.PlayReady.PlayReadyIndividualizationServiceRequest.ResponseCustomData +P:Windows.Media.Protection.PlayReady.PlayReadyIndividualizationServiceRequest.Uri +P:Windows.Media.Protection.PlayReady.PlayReadyRevocationServiceRequest.ChallengeCustomData +P:Windows.Media.Protection.PlayReady.PlayReadyRevocationServiceRequest.ResponseCustomData +P:Windows.Media.Protection.PlayReady.PlayReadyRevocationServiceRequest.Uri +P:Windows.Media.Protection.PlayReady.PlayReadyStatics.HardwareDRMDisabledAtTime +P:Windows.Media.Protection.PlayReady.PlayReadyStatics.HardwareDRMDisabledUntilTime +P:Windows.Media.SpeechRecognition.SpeechRecognitionResult.Constraint +P:Windows.Media.SpeechRecognition.SpeechRecognizer.SystemSpeechLanguage +P:Windows.Media.Streaming.Adaptive.AdaptiveMediaSourceCorrelatedTimes.PresentationTimeStamp +P:Windows.Media.VideoFrame.Direct3DSurface +P:Windows.Media.VideoFrame.SoftwareBitmap +P:Windows.Networking.BackgroundTransfer.BackgroundDownloader.CompletionGroup +P:Windows.Networking.BackgroundTransfer.BackgroundUploader.CompletionGroup +P:Windows.Networking.EndpointPair.LocalHostName +P:Windows.Networking.HostName.IPInformation +P:Windows.Networking.NetworkOperators.ESim.SlotIndex +P:Windows.Networking.NetworkOperators.ESimDownloadProfileMetadataResult.ProfileMetadata +P:Windows.Networking.NetworkOperators.NetworkOperatorNotificationEventDetails.SmsMessage +P:Windows.Networking.NetworkOperators.UssdReply.Message +P:Windows.Networking.Proximity.TriggeredConnectionStateChangedEventArgs.Socket +P:Windows.Networking.PushNotifications.PushNotificationReceivedEventArgs.BadgeNotification +P:Windows.Networking.PushNotifications.PushNotificationReceivedEventArgs.RawNotification +P:Windows.Networking.PushNotifications.PushNotificationReceivedEventArgs.TileNotification +P:Windows.Networking.PushNotifications.PushNotificationReceivedEventArgs.ToastNotification +P:Windows.Networking.Sockets.IWebSocketInformation.Protocol +P:Windows.Networking.Sockets.MessageWebSocketInformation.Protocol +P:Windows.Networking.Sockets.StreamSocketInformation.RemoteAddress +P:Windows.Networking.Sockets.StreamSocketInformation.SessionKey +P:Windows.Networking.Sockets.StreamWebSocketInformation.Protocol +P:Windows.Networking.Vpn.VpnChannel.CurrentRequestTransportContext +P:Windows.Perception.People.EyesPose.Gaze +P:Windows.Perception.Spatial.SpatialStageFrameOfReference.Current +P:Windows.Perception.Spatial.Surfaces.SpatialSurfaceMesh.VertexNormals +P:Windows.Security.EnterpriseData.ProtectionPolicyManager.PrimaryManagedIdentity +P:Windows.Services.Cortana.CortanaActionableInsights.User +P:Windows.Services.Cortana.CortanaActionableInsightsOptions.ContentSourceWebLink +P:Windows.Services.Cortana.CortanaActionableInsightsOptions.SurroundingText +P:Windows.Services.Maps.Guidance.GuidanceManeuver.RoadSignpost +P:Windows.Services.Maps.Guidance.GuidanceRoadSignpost.Exit +P:Windows.Services.Maps.Guidance.GuidanceRoadSignpost.ExitNumber +P:Windows.Services.Maps.MapRouteFinderResult.AlternateRoutes +P:Windows.Services.Store.StoreContext.User +P:Windows.Services.Store.StoreSku.SubscriptionInfo +P:Windows.System.RemoteSystems.RemoteSystemSessionCreationResult.Session +P:Windows.System.RemoteSystems.RemoteSystemSessionJoinResult.Session +P:Windows.UI.Composition.CompositionGeometricClip.Geometry +P:Windows.UI.Composition.RedirectVisual.Source +P:Windows.UI.Composition.ScalarNaturalMotionAnimation.FinalValue +P:Windows.UI.Composition.ScalarNaturalMotionAnimation.InitialValue +P:Windows.UI.Composition.Vector2NaturalMotionAnimation.FinalValue +P:Windows.UI.Composition.Vector2NaturalMotionAnimation.InitialValue +P:Windows.UI.Composition.Vector3NaturalMotionAnimation.FinalValue +P:Windows.UI.Composition.Vector3NaturalMotionAnimation.InitialValue +P:Windows.UI.Input.Inking.InkPresenter.StrokeContainer +P:Windows.UI.Input.PointerPointProperties.ZDistance +P:Windows.UI.Input.RadialControllerButtonClickedEventArgs.Contact +P:Windows.UI.Input.RadialControllerButtonHoldingEventArgs.Contact +P:Windows.UI.Input.RadialControllerButtonPressedEventArgs.Contact +P:Windows.UI.Input.RadialControllerButtonReleasedEventArgs.Contact +P:Windows.UI.Input.RadialControllerControlAcquiredEventArgs.Contact +P:Windows.UI.Input.RadialControllerRotationChangedEventArgs.Contact +P:Windows.UI.Input.RadialControllerScreenContactContinuedEventArgs.Contact +P:Windows.UI.Input.RadialControllerScreenContactStartedEventArgs.Contact +P:Windows.UI.Input.Spatial.SpatialInteractionController.SimpleHapticsController +P:Windows.UI.Input.Spatial.SpatialInteractionSource.Controller +P:Windows.UI.Input.Spatial.SpatialInteractionSourceLocation.SourcePointerPose +P:Windows.UI.Input.Spatial.SpatialInteractionSourceState.ControllerProperties +P:Windows.UI.Input.Spatial.SpatialPointerPose.Eyes +P:Windows.UI.Popups.UICommand.Invoked +P:Windows.UI.Shell.Tasks.AppTaskInfo.EndTime +P:Windows.UI.StartScreen.TileMixedRealityModel.BoundingBox +P:Windows.UI.Text.ContentLinkInfo.Uri +P:Windows.UI.Text.Core.CoreTextSelectionRequest.Selection +P:Windows.UI.Text.Core.CoreTextTextRequest.Text +P:Windows.UI.WebUI.WebUIAppointmentsProviderAddAppointmentActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIAppointmentsProviderRemoveAppointmentActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIAppointmentsProviderReplaceAppointmentActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIAppointmentsProviderShowAppointmentDetailsActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIAppointmentsProviderShowTimeFrameActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIBackgroundTaskInstance.Current +P:Windows.UI.WebUI.WebUICachedFileUpdaterActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIContactPanelActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIDeviceActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIDevicePairingActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIDialReceiverActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIFileActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIFileOpenPickerActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIFileOpenPickerContinuationEventArgs.User +P:Windows.UI.WebUI.WebUIFileSavePickerActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIFileSavePickerContinuationEventArgs.User +P:Windows.UI.WebUI.WebUIFolderPickerContinuationEventArgs.User +P:Windows.UI.WebUI.WebUILaunchActivatedEventArgs.User +P:Windows.UI.WebUI.WebUILockScreenActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIProtocolActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIProtocolForResultsActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIRestrictedLaunchActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIShareTargetActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIToastNotificationActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIVoiceCommandActivatedEventArgs.User +P:Windows.UI.WebUI.WebUIWebAccountProviderActivatedEventArgs.User +P:Windows.UI.Xaml.Automation.Provider.IDragProvider.DropEffect +P:Windows.UI.Xaml.Automation.Provider.IRangeValueProvider.LargeChange +P:Windows.UI.Xaml.Automation.Provider.IRangeValueProvider.Maximum +P:Windows.UI.Xaml.Automation.Provider.IRangeValueProvider.Minimum +P:Windows.UI.Xaml.Automation.Provider.IRangeValueProvider.SmallChange +P:Windows.UI.Xaml.Automation.Provider.IRangeValueProvider.Value +P:Windows.UI.Xaml.BringIntoViewOptions.TargetRect +P:Windows.UI.Xaml.Controls.AnchorRequestedEventArgs.Anchor +P:Windows.UI.Xaml.Controls.AppBarButton.KeyboardAcceleratorTextOverride +P:Windows.UI.Xaml.Controls.AppBarToggleButton.KeyboardAcceleratorTextOverride +P:Windows.UI.Xaml.Controls.AutoSuggestBox.Description +P:Windows.UI.Xaml.Controls.AutoSuggestBox.QueryIcon +P:Windows.UI.Xaml.Controls.BitmapIcon.UriSource +P:Windows.UI.Xaml.Controls.BitmapIconSource.UriSource +P:Windows.UI.Xaml.Controls.Border.Background +P:Windows.UI.Xaml.Controls.Border.BackgroundTransition +P:Windows.UI.Xaml.Controls.Border.BorderBrush +P:Windows.UI.Xaml.Controls.Button.Flyout +P:Windows.UI.Xaml.Controls.CalendarDatePicker.Description +P:Windows.UI.Xaml.Controls.CalendarDatePicker.Header +P:Windows.UI.Xaml.Controls.CalendarDatePicker.HeaderTemplate +P:Windows.UI.Xaml.Controls.CalendarViewDayItemChangingEventArgs.Item +P:Windows.UI.Xaml.Controls.ColorPicker.PreviousColor +P:Windows.UI.Xaml.Controls.ComboBox.Description +P:Windows.UI.Xaml.Controls.ComboBox.Header +P:Windows.UI.Xaml.Controls.ComboBox.HeaderTemplate +P:Windows.UI.Xaml.Controls.CommandBar.CommandBarOverflowPresenterStyle +P:Windows.UI.Xaml.Controls.ContainerContentChangingEventArgs.Item +P:Windows.UI.Xaml.Controls.ContentControl.Content +P:Windows.UI.Xaml.Controls.ContentControl.ContentTemplateRoot +P:Windows.UI.Xaml.Controls.ContentDialog.CloseButtonCommandParameter +P:Windows.UI.Xaml.Controls.ContentDialog.CloseButtonStyle +P:Windows.UI.Xaml.Controls.ContentDialog.PrimaryButtonCommandParameter +P:Windows.UI.Xaml.Controls.ContentDialog.PrimaryButtonStyle +P:Windows.UI.Xaml.Controls.ContentDialog.PrimaryButtonText +P:Windows.UI.Xaml.Controls.ContentDialog.SecondaryButtonCommandParameter +P:Windows.UI.Xaml.Controls.ContentDialog.SecondaryButtonStyle +P:Windows.UI.Xaml.Controls.ContentDialog.SecondaryButtonText +P:Windows.UI.Xaml.Controls.ContentPresenter.Background +P:Windows.UI.Xaml.Controls.ContentPresenter.BackgroundTransition +P:Windows.UI.Xaml.Controls.ContentPresenter.BorderBrush +P:Windows.UI.Xaml.Controls.ContentPresenter.Content +P:Windows.UI.Xaml.Controls.ContentPresenter.ContentTemplate +P:Windows.UI.Xaml.Controls.ContentPresenter.Foreground +P:Windows.UI.Xaml.Controls.Control.Background +P:Windows.UI.Xaml.Controls.Control.BorderBrush +P:Windows.UI.Xaml.Controls.DatePicker.Header +P:Windows.UI.Xaml.Controls.DatePicker.HeaderTemplate +P:Windows.UI.Xaml.Controls.DatePicker.SelectedDate +P:Windows.UI.Xaml.Controls.Flyout.FlyoutPresenterStyle +P:Windows.UI.Xaml.Controls.Grid.BorderBrush +P:Windows.UI.Xaml.Controls.GroupStyle.ContainerStyle +P:Windows.UI.Xaml.Controls.GroupStyle.ContainerStyleSelector +P:Windows.UI.Xaml.Controls.GroupStyle.HeaderContainerStyle +P:Windows.UI.Xaml.Controls.GroupStyle.HeaderTemplate +P:Windows.UI.Xaml.Controls.GroupStyle.HeaderTemplateSelector +P:Windows.UI.Xaml.Controls.HandwritingView.PlacementTarget +P:Windows.UI.Xaml.Controls.Hub.Header +P:Windows.UI.Xaml.Controls.Hub.HeaderTemplate +P:Windows.UI.Xaml.Controls.Hub.SemanticZoomOwner +P:Windows.UI.Xaml.Controls.HubSection.Header +P:Windows.UI.Xaml.Controls.HubSection.HeaderTemplate +P:Windows.UI.Xaml.Controls.IScrollAnchorProvider.CurrentAnchor +P:Windows.UI.Xaml.Controls.IconElement.Foreground +P:Windows.UI.Xaml.Controls.IconSource.Foreground +P:Windows.UI.Xaml.Controls.IconSourceElement.IconSource +P:Windows.UI.Xaml.Controls.InkToolbarCustomPenButton.ConfigurationContent +P:Windows.UI.Xaml.Controls.InkToolbarCustomToolButton.ConfigurationContent +P:Windows.UI.Xaml.Controls.ItemsControl.ItemContainerStyle +P:Windows.UI.Xaml.Controls.ItemsControl.ItemTemplate +P:Windows.UI.Xaml.Controls.ItemsControl.Items +P:Windows.UI.Xaml.Controls.ItemsControl.ItemsPanelRoot +P:Windows.UI.Xaml.Controls.ItemsControl.ItemsSource +P:Windows.UI.Xaml.Controls.ItemsPresenter.Footer +P:Windows.UI.Xaml.Controls.ItemsPresenter.FooterTemplate +P:Windows.UI.Xaml.Controls.ItemsPresenter.Header +P:Windows.UI.Xaml.Controls.ItemsPresenter.HeaderTemplate +P:Windows.UI.Xaml.Controls.ListPickerFlyout.ItemTemplate +P:Windows.UI.Xaml.Controls.ListPickerFlyout.ItemsSource +P:Windows.UI.Xaml.Controls.ListPickerFlyout.SelectedItem +P:Windows.UI.Xaml.Controls.ListPickerFlyout.SelectedValue +P:Windows.UI.Xaml.Controls.ListViewBase.Footer +P:Windows.UI.Xaml.Controls.ListViewBase.FooterTemplate +P:Windows.UI.Xaml.Controls.ListViewBase.Header +P:Windows.UI.Xaml.Controls.ListViewBase.HeaderTemplate +P:Windows.UI.Xaml.Controls.ListViewBase.SemanticZoomOwner +P:Windows.UI.Xaml.Controls.Maps.MapControl.Region +P:Windows.UI.Xaml.Controls.MediaElement.AudioStreamIndex +P:Windows.UI.Xaml.Controls.MediaElement.Source +P:Windows.UI.Xaml.Controls.MediaPlayerElement.Source +P:Windows.UI.Xaml.Controls.MenuFlyoutItem.Command +P:Windows.UI.Xaml.Controls.MenuFlyoutItem.CommandParameter +P:Windows.UI.Xaml.Controls.MenuFlyoutItem.KeyboardAcceleratorTextOverride +P:Windows.UI.Xaml.Controls.MenuFlyoutSubItem.Items +P:Windows.UI.Xaml.Controls.NavigationView.MenuItemContainerStyle +P:Windows.UI.Xaml.Controls.NavigationView.MenuItemTemplate +P:Windows.UI.Xaml.Controls.NavigationView.MenuItemsSource +P:Windows.UI.Xaml.Controls.NavigationView.PaneFooter +P:Windows.UI.Xaml.Controls.NavigationView.PaneHeader +P:Windows.UI.Xaml.Controls.NavigationView.PaneToggleButtonStyle +P:Windows.UI.Xaml.Controls.NavigationView.SelectedItem +P:Windows.UI.Xaml.Controls.NavigationViewItem.Icon +P:Windows.UI.Xaml.Controls.Page.BottomAppBar +P:Windows.UI.Xaml.Controls.Page.TopAppBar +P:Windows.UI.Xaml.Controls.Panel.Background +P:Windows.UI.Xaml.Controls.Panel.BackgroundTransition +P:Windows.UI.Xaml.Controls.ParallaxView.Child +P:Windows.UI.Xaml.Controls.PasswordBox.Description +P:Windows.UI.Xaml.Controls.PasswordBox.Header +P:Windows.UI.Xaml.Controls.PasswordBox.HeaderTemplate +P:Windows.UI.Xaml.Controls.PasswordBox.InputScope +P:Windows.UI.Xaml.Controls.PasswordBox.SelectionFlyout +P:Windows.UI.Xaml.Controls.PasswordBox.SelectionHighlightColor +P:Windows.UI.Xaml.Controls.Pivot.LeftHeader +P:Windows.UI.Xaml.Controls.Pivot.RightHeader +P:Windows.UI.Xaml.Controls.Primitives.ButtonBase.Command +P:Windows.UI.Xaml.Controls.Primitives.ButtonBase.CommandParameter +P:Windows.UI.Xaml.Controls.Primitives.FlyoutBase.XamlRoot +P:Windows.UI.Xaml.Controls.Primitives.Selector.SelectedItem +P:Windows.UI.Xaml.Controls.Primitives.Selector.SelectedValue +P:Windows.UI.Xaml.Controls.RadioButton.GroupName +P:Windows.UI.Xaml.Controls.RatingControl.PlaceholderValue +P:Windows.UI.Xaml.Controls.RatingControl.Value +P:Windows.UI.Xaml.Controls.RelativePanel.BorderBrush +P:Windows.UI.Xaml.Controls.RichEditBox.Description +P:Windows.UI.Xaml.Controls.RichEditBox.Header +P:Windows.UI.Xaml.Controls.RichEditBox.HeaderTemplate +P:Windows.UI.Xaml.Controls.RichEditBox.InputScope +P:Windows.UI.Xaml.Controls.RichEditBox.SelectionFlyout +P:Windows.UI.Xaml.Controls.RichEditBox.SelectionHighlightColor +P:Windows.UI.Xaml.Controls.RichEditBox.SelectionHighlightColorWhenNotFocused +P:Windows.UI.Xaml.Controls.RichTextBlock.Foreground +P:Windows.UI.Xaml.Controls.RichTextBlock.SelectionEnd +P:Windows.UI.Xaml.Controls.RichTextBlock.SelectionFlyout +P:Windows.UI.Xaml.Controls.RichTextBlock.SelectionHighlightColor +P:Windows.UI.Xaml.Controls.RichTextBlock.SelectionStart +P:Windows.UI.Xaml.Controls.ScrollViewer.CurrentAnchor +P:Windows.UI.Xaml.Controls.SettingsFlyout.HeaderBackground +P:Windows.UI.Xaml.Controls.SettingsFlyout.HeaderForeground +P:Windows.UI.Xaml.Controls.SettingsFlyout.IconSource +P:Windows.UI.Xaml.Controls.Slider.Header +P:Windows.UI.Xaml.Controls.Slider.HeaderTemplate +P:Windows.UI.Xaml.Controls.SplitButton.Command +P:Windows.UI.Xaml.Controls.SplitButton.CommandParameter +P:Windows.UI.Xaml.Controls.SplitButton.Flyout +P:Windows.UI.Xaml.Controls.SplitView.Content +P:Windows.UI.Xaml.Controls.SplitView.Pane +P:Windows.UI.Xaml.Controls.StackPanel.BorderBrush +P:Windows.UI.Xaml.Controls.SwipeItem.Command +P:Windows.UI.Xaml.Controls.SwipeItem.CommandParameter +P:Windows.UI.Xaml.Controls.SwipeItem.IconSource +P:Windows.UI.Xaml.Controls.TextBlock.Foreground +P:Windows.UI.Xaml.Controls.TextBlock.SelectionEnd +P:Windows.UI.Xaml.Controls.TextBlock.SelectionFlyout +P:Windows.UI.Xaml.Controls.TextBlock.SelectionHighlightColor +P:Windows.UI.Xaml.Controls.TextBlock.SelectionStart +P:Windows.UI.Xaml.Controls.TextBox.Description +P:Windows.UI.Xaml.Controls.TextBox.Header +P:Windows.UI.Xaml.Controls.TextBox.HeaderTemplate +P:Windows.UI.Xaml.Controls.TextBox.InputScope +P:Windows.UI.Xaml.Controls.TextBox.SelectionFlyout +P:Windows.UI.Xaml.Controls.TextBox.SelectionHighlightColorWhenNotFocused +P:Windows.UI.Xaml.Controls.TimePicker.Header +P:Windows.UI.Xaml.Controls.TimePicker.HeaderTemplate +P:Windows.UI.Xaml.Controls.TimePicker.SelectedTime +P:Windows.UI.Xaml.Controls.ToolTip.PlacementRect +P:Windows.UI.Xaml.Controls.ToolTip.PlacementTarget +P:Windows.UI.Xaml.Controls.TreeView.ItemContainerStyle +P:Windows.UI.Xaml.Controls.TreeView.ItemTemplate +P:Windows.UI.Xaml.Controls.TreeView.ItemsSource +P:Windows.UI.Xaml.Controls.TreeViewItem.ItemsSource +P:Windows.UI.Xaml.Data.Binding.ConverterParameter +P:Windows.UI.Xaml.Data.Binding.RelativeSource +P:Windows.UI.Xaml.Data.Binding.TargetNullValue +P:Windows.UI.Xaml.Data.ICollectionView.CurrentItem +P:Windows.UI.Xaml.Documents.Glyphs.Fill +P:Windows.UI.Xaml.Documents.Glyphs.FontUri +P:Windows.UI.Xaml.Documents.Hyperlink.NavigateUri +P:Windows.UI.Xaml.Documents.TextElement.XamlRoot +P:Windows.UI.Xaml.DragEventArgs.DragUIOverride +P:Windows.UI.Xaml.ElementFactoryGetArgs.Parent +P:Windows.UI.Xaml.ElementFactoryRecycleArgs.Parent +P:Windows.UI.Xaml.FrameworkElement.Parent +P:Windows.UI.Xaml.FrameworkElement.Style +P:Windows.UI.Xaml.Input.FindNextElementOptions.SearchRoot +P:Windows.UI.Xaml.Input.FocusManagerGotFocusEventArgs.CorrelationId +P:Windows.UI.Xaml.Input.FocusManagerLostFocusEventArgs.CorrelationId +P:Windows.UI.Xaml.Input.GettingFocusEventArgs.CorrelationId +P:Windows.UI.Xaml.Input.KeyboardAccelerator.ScopeOwner +P:Windows.UI.Xaml.Input.LosingFocusEventArgs.CorrelationId +P:Windows.UI.Xaml.Markup.IXamlType.ContentProperty +P:Windows.UI.Xaml.Markup.IXamlType.ItemType +P:Windows.UI.Xaml.Markup.IXamlType.KeyType +P:Windows.UI.Xaml.Media.Animation.BeginStoryboard.Storyboard +P:Windows.UI.Xaml.Media.Animation.ColorAnimation.By +P:Windows.UI.Xaml.Media.Animation.ColorAnimation.From +P:Windows.UI.Xaml.Media.Animation.ColorAnimation.To +P:Windows.UI.Xaml.Media.Animation.ColorKeyFrame.KeyTime +P:Windows.UI.Xaml.Media.Animation.DoubleAnimation.By +P:Windows.UI.Xaml.Media.Animation.DoubleAnimation.From +P:Windows.UI.Xaml.Media.Animation.DoubleAnimation.To +P:Windows.UI.Xaml.Media.Animation.DoubleKeyFrame.KeyTime +P:Windows.UI.Xaml.Media.Animation.ObjectKeyFrame.KeyTime +P:Windows.UI.Xaml.Media.Animation.ObjectKeyFrame.Value +P:Windows.UI.Xaml.Media.Animation.PointAnimation.By +P:Windows.UI.Xaml.Media.Animation.PointAnimation.EasingFunction +P:Windows.UI.Xaml.Media.Animation.PointAnimation.From +P:Windows.UI.Xaml.Media.Animation.PointAnimation.To +P:Windows.UI.Xaml.Media.Animation.PointKeyFrame.KeyTime +P:Windows.UI.Xaml.Media.Brush.RelativeTransform +P:Windows.UI.Xaml.Media.GeneralTransform.Inverse +P:Windows.UI.Xaml.Media.PlaneProjection.ProjectionMatrix +P:Windows.UI.Xaml.Media.RectangleGeometry.Rect +P:Windows.UI.Xaml.Media.TimelineMarker.Time +P:Windows.UI.Xaml.Navigation.NavigationEventArgs.Parameter +P:Windows.UI.Xaml.Setter.Property +P:Windows.UI.Xaml.Shapes.Polygon.Points +P:Windows.UI.Xaml.Shapes.Polyline.Points +P:Windows.UI.Xaml.Shapes.Shape.Fill +P:Windows.UI.Xaml.Shapes.Shape.Stroke +P:Windows.UI.Xaml.Style.BasedOn +P:Windows.UI.Xaml.UIElement.CacheMode +P:Windows.UI.Xaml.UIElement.Clip +P:Windows.UI.Xaml.UIElement.ContextFlyout +P:Windows.UI.Xaml.UIElement.RenderTransform +P:Windows.UI.Xaml.UIElement.Transform3D +P:Windows.UI.Xaml.UIElement.XamlRoot +P:Windows.UI.Xaml.VisualStateGroup.CurrentState +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentDisposition +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentLength +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentLocation +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentMD5 +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentRange +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.ContentType +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.Expires +P:Windows.Web.Http.Headers.HttpContentHeaderCollection.LastModified +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.Authorization +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.Date +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.Host +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.IfModifiedSince +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.IfUnmodifiedSince +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.MaxForwards +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.ProxyAuthorization +P:Windows.Web.Http.Headers.HttpRequestHeaderCollection.Referer +P:Windows.Web.Http.Headers.HttpResponseHeaderCollection.Age +P:Windows.Web.Http.Headers.HttpResponseHeaderCollection.Date +P:Windows.Web.Http.Headers.HttpResponseHeaderCollection.Location +P:Windows.Web.Http.Headers.HttpResponseHeaderCollection.RetryAfter +P:Windows.Web.Http.HttpCookie.Expires +P:Windows.Web.Http.HttpGetInputStreamResult.ExtendedError +P:Windows.Web.Http.HttpGetStringResult.ExtendedError +P:Windows.Web.Http.HttpRequestResult.ExtendedError +P:Windows.Web.Syndication.SyndicationFeed.FirstUri +P:Windows.Web.Syndication.SyndicationFeed.IconUri +P:Windows.Web.Syndication.SyndicationFeed.LastUri +P:Windows.Web.Syndication.SyndicationFeed.NextUri +P:Windows.Web.Syndication.SyndicationFeed.PreviousUri +P:Windows.Web.Syndication.SyndicationItem.EditMediaUri +P:Windows.Web.Syndication.SyndicationItem.EditUri diff --git a/tools/dynwinrt-codegen/scripts/extract-null-results.py b/tools/dynwinrt-codegen/scripts/extract-null-results.py new file mode 100644 index 00000000..987a305d --- /dev/null +++ b/tools/dynwinrt-codegen/scripts/extract-null-results.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Extract the Windows SDK members whose documented result can be null. + +WinRT metadata carries no nullability, so dynwinrt-codegen types most outputs +as non-null and keeps `| None` for members whose documentation says that the +result can be null. This script derives that list from MicrosoftDocs/winrt-api +at a pinned commit and writes only doc comment IDs (api-ids), never +documentation text, to api-docs/windows-null-results.txt. + +A method (`M:`) or property (`P:`) is listed when +- a sentence of its `## -returns` or `## -property-value` section says that + the result can be null (for an asynchronous method: its completed result), or +- a sentence of its `## -remarks` section says that the member itself + ("this method", "this property", "it", or the member's name) returns or is + null. +Sentences that negate null, that describe null arguments, or that describe an +object holding a null value are ignored, and so are Boolean results, attached +properties, constructors and members of generic types. Reviewed corrections +from api-docs/windows-null-results.overrides.txt are applied last. + +The documentation is read from git objects, without a working tree: + + python extract-null-results.py # fetch the pinned commit + python extract-null-results.py --repo DIR # reuse a local clone + python extract-null-results.py --commit SHA # move the pin +""" + +from __future__ import annotations + +import argparse +import fnmatch +import re +import subprocess +import sys +import tempfile +from collections import Counter +from collections.abc import Iterator +from pathlib import Path + +REPOSITORY = "https://github.com/MicrosoftDocs/winrt-api" +COMMIT = "8448d5eecfbc2ed903f659f350841dcb4888bc8b" +CODEGEN = Path(__file__).resolve().parent.parent +OUTPUT = CODEGEN / "api-docs" / "windows-null-results.txt" +OVERRIDES = CODEGEN / "api-docs" / "windows-null-results.overrides.txt" + +NULL = re.compile(r"\bnull(?:ptr)?\b(?![- ](?:terminat|character|char\b))", re.I) +NEGATION = re.compile( + r"\b(?:never|not|cannot|can't|won't|doesn't|does not|isn't|is not|will not|must not|may not)" + r"\s+(?:be\s+|is\s+|return\s+|returns\s+)?(?:a\s+)?null\b|\bnon-?null\b", + re.I, +) +ARGUMENT = re.compile( + r"\bnull\s+(?:was|is|were|are)\s+passed\b" + r"|\bpass(?:es|ed|ing)?\s+(?:in\s+)?(?:a\s+)?null\b" + r"|\bother than\s+null\b" + r"|\b(?:argument|parameter)\s+(?:is|was|are)\s+null\b" + r"|\bnull\s+(?:for|as)\s+(?:the\s+)?\w+\s+(?:argument|parameter)\b" + r"|\bexception\b[^.]*\bset\s+to\s+null\b|\bset\s+to\s+null\b[^.]*\bexception\b", + re.I, +) +HOLDER = re.compile( + r"\b(?:with|holds?|holding|contains?|containing|supports?|has|have)\s+(?:a|an)\s+" + r"(?:json\s+)?null\s+value\b", + re.I, +) +BOOLEAN = re.compile(r"\A\W*(?:true|false)\b", re.I) +REMARKS_GAP = r"(?:(?!\b(?:when|if|unless|called|until|whether|and|but)\b)[^.;,]){0,40}?" +REMARKS_VERB = ( + r"\b(?:returns?|is|will\s+be|may\s+be|can\s+be|could\s+be|might\s+be|is\s+set\s+to)" + r"\s+(?:a\s+|an\s+|the\s+)?null(?:ptr)?\b" +) + + +def git(repo: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(repo), *args], check=True, capture_output=True, text=True + ).stdout + + +def ensure_commit(repo: Path, commit: str) -> None: + if not (repo / ".git").exists(): + repo.mkdir(parents=True, exist_ok=True) + git(repo, "init", "--quiet") + present = subprocess.run( + ["git", "-C", str(repo), "cat-file", "-e", f"{commit}^{{commit}}"], + capture_output=True, + ) + if present.returncode != 0: + git(repo, "fetch", "--quiet", "--depth", "1", REPOSITORY, commit) + + +def documents(repo: Path, commit: str) -> Iterator[str]: + listing = git(repo, "ls-tree", "-r", "-z", commit) + blobs = [] + for entry in listing.split("\0"): + if not entry: + continue + info, path = entry.split("\t", 1) + _, kind, sha = info.split() + if kind == "blob" and path.endswith(".md"): + blobs.append(sha) + with subprocess.Popen( + ["git", "-C", str(repo), "cat-file", "--batch"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) as batch: + assert batch.stdin is not None and batch.stdout is not None + for sha in blobs: + batch.stdin.write(sha.encode() + b"\n") + batch.stdin.flush() + size = int(batch.stdout.readline().split()[2]) + data = batch.stdout.read(size) + batch.stdout.read(1) + yield data.decode("utf-8", "replace").replace("\r\n", "\n") + batch.stdin.close() + + +def front_matter(text: str) -> dict[str, str]: + match = re.match(r"\A---[ \t]*\n(.*?)\n---", text, re.S) + fields = {} + for line in match.group(1).splitlines() if match else []: + key, _, value = line.strip().partition(":") + if key.startswith("-"): + fields[key[1:]] = value.strip() + return fields + + +def section(text: str, name: str) -> str: + match = re.search(rf"^## -{re.escape(name)}[ \t]*\n(.*?)(?=^## -|\Z)", text, re.M | re.S) + return plain(match.group(1)) if match else "" + + +def plain(markdown: str) -> str: + text = re.sub(r"", " ", markdown, flags=re.S) + text = re.sub(r"!?\[([^\]]*)\]\([^)]*\)", r"\1", text) + text = re.sub(r"\[!(?:NOTE|IMPORTANT|TIP|WARNING|CAUTION)\]", " ", text) + text = re.sub(r"(?m)^\s*>\s?", " ", text) + text = re.sub(r"[*_`]", "", text) + return " ".join(text.split()) + + +def sentences(text: str) -> list[str]: + return [sentence for sentence in re.split(r"(?<=[.!?])\s+", text) if NULL.search(sentence)] + + +def states_null(sentence: str) -> bool: + return not (NEGATION.search(sentence) or ARGUMENT.search(sentence) or HOLDER.search(sentence)) + + +def result_is_nullable(result: str) -> bool: + if not result or BOOLEAN.match(result): + return False + return any(states_null(sentence) for sentence in sentences(result)) + + +def remarks_say_null(remarks: str, member: str) -> bool: + subject = ( + r"(?:\bthis\s+(?:method|property|function|call|operation)" + r"|\bthe\s+(?:method|property|call|operation)" + rf"|\bit|\b{re.escape(member)})\b" + ) + claim = re.compile(subject + REMARKS_GAP + REMARKS_VERB, re.I) + returns = re.compile(rf"\bif\s+(?:this|it|{re.escape(member)})\s+returns\s+null\b", re.I) + return any( + (claim.search(sentence) or returns.search(sentence)) and states_null(sentence) + for sentence in sentences(remarks) + ) + + +def member_name(api_id: str) -> str: + return api_id[2:].split("(", 1)[0].rsplit(".", 1)[-1] + + +def extract(repo: Path, commit: str) -> tuple[set[str], set[str], Counter[str]]: + documented: set[str] = set() + nullable: set[str] = set() + sources: Counter[str] = Counter() + for text in documents(repo, commit): + fields = front_matter(text) + api_id = fields.get("api-id", "") + api_type = fields.get("api-type", "") + if not api_id.startswith(("M:", "P:")) or api_type in { + "winrt attachedproperty", + "winrt constructor", + }: + continue + if "#ctor" in api_id or "`" in api_id.split("(", 1)[0]: + continue + documented.add(api_id) + result = section(text, "returns" if api_id.startswith("M:") else "property-value") + if result_is_nullable(result): + sources["returns" if api_id.startswith("M:") else "property-value"] += 1 + nullable.add(api_id) + elif remarks_say_null(section(text, "remarks"), member_name(api_id)): + sources["remarks"] += 1 + nullable.add(api_id) + return documented, nullable, sources + + +def apply_overrides(documented: set[str], nullable: set[str], sources: Counter[str]) -> set[str]: + result = set(nullable) + for number, raw in enumerate(OVERRIDES.read_text(encoding="utf-8").splitlines(), 1): + entry = raw.split("#", 1)[0].strip() + if not entry: + continue + sign, pattern = entry[0], entry[1:].strip() + if sign not in "+-" or not pattern: + sys.exit(f"{OVERRIDES.name}:{number}: expected '+api-id' or '-api-id'") + matches = {api_id for api_id in documented if fnmatch.fnmatchcase(api_id, pattern)} + if not matches: + sys.exit(f"{OVERRIDES.name}:{number}: {pattern} matches no documented member") + if sign == "+": + sources["override additions"] += len(matches - result) + result |= matches + else: + sources["override removals"] += len(matches & result) + result -= matches + return result + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0]) + parser.add_argument("--commit", default=COMMIT, help="winrt-api commit to read") + parser.add_argument("--repo", type=Path, help="local winrt-api clone to reuse") + arguments = parser.parse_args() + + with tempfile.TemporaryDirectory(prefix="winrt-api-") as scratch: + repo = arguments.repo or Path(scratch) + ensure_commit(repo, arguments.commit) + documented, nullable, sources = extract(repo, arguments.commit) + members = sorted(apply_overrides(documented, nullable, sources)) + + header = [ + "# Windows SDK members whose documented result can be null: a method's", + "# return value (for asynchronous methods, the completed result) or a", + "# property's value. dynwinrt-codegen keeps `| None` on these outputs.", + f"# Source: {REPOSITORY} at commit {arguments.commit}", + "# Generated by scripts/extract-null-results.py; do not edit. Reviewed", + "# corrections belong in windows-null-results.overrides.txt.", + ] + OUTPUT.write_text("\n".join(header + members) + "\n", encoding="utf-8", newline="\n") + print(f"{len(members)} members from {len(documented)} documented methods and properties") + for source, count in sources.most_common(): + print(f" {source}: {count}") + + +if __name__ == "__main__": + main() diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index caf2071b..52484e00 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -786,7 +786,7 @@ pub(crate) fn generate_method_body( if method.is_property_getter && in_params.is_empty() { let prop_name = to_snake_case(method.name.strip_prefix("get_").unwrap_or(&method.name)); let py_return = return_type - .map(|typ| py_property_type(typ, AnnotationSurface::Runtime, context)) + .map(|typ| py_property_type(method, typ, AnnotationSurface::Runtime, context)) .unwrap_or_else(|| "None".to_string()); out.push_str(" @_property\n"); out.push_str(&format!(" def {}(self) -> {}:\n", prop_name, py_return)); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index 7077b9a0..6a9de1fc 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -12,9 +12,10 @@ //! `| None` is appended. use crate::codegen::winrt::shared::imports::ireference_inner_type; -use crate::meta::MethodMeta; +use crate::meta::{ElementAccess, MethodMeta}; use crate::types::TypeMeta; +use super::collections::CollectionKind; use super::naming::PythonProjectionContext; /// The generated artifact an annotation is rendered into. @@ -50,6 +51,52 @@ pub(crate) enum OutputPosition { Activation, } +/// The collection holding an element. Whether a reference-type element admits +/// `None` depends only on this; see [`element_admits_none`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ElementContainer { + /// `IIterable`, `IIterator`, `IVectorView`, `IMapView` or `IKeyValuePair`. + View, + /// `IVector`, `IMap` or their observable forms. + Mutable, + /// An array returned by a member that does not read collection elements. + Array, +} + +impl ElementContainer { + pub(crate) fn of(kind: CollectionKind) -> Self { + match kind { + CollectionKind::MutableSequence | CollectionKind::MutableMapping => Self::Mutable, + CollectionKind::Iterable + | CollectionKind::Iterator + | CollectionKind::Sequence + | CollectionKind::Mapping + | CollectionKind::KeyValuePair => Self::View, + } + } +} + +impl From for ElementContainer { + fn from(access: ElementAccess) -> Self { + match access { + ElementAccess::ReadOnly => Self::View, + ElementAccess::Mutable => Self::Mutable, + } + } +} + +/// The collection element rule: anyone can store null in a mutable collection, +/// so its reference-type elements admit `None`. Views, iterators and arrays +/// are typed like other outputs. Positions that read elements inherit the +/// rule of their owning collection; a view obtained from a mutable collection +/// follows the view rule. +pub(crate) fn element_admits_none(container: ElementContainer) -> bool { + match container { + ElementContainer::Mutable => true, + ElementContainer::View | ElementContainer::Array => false, + } +} + /// A position plus the facts about the member producing the value. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct OutputSite { @@ -57,6 +104,12 @@ pub(crate) struct OutputSite { /// The member follows the `Try*` pattern, so a null result is part of /// its contract ("not found", "could not parse"). pub(crate) try_method: bool, + /// The Windows SDK documentation says the member's result can be null. + pub(crate) documented_null: bool, + /// The collection holding the value: set on collection elements, and on + /// the results of members that read elements of the collection declaring + /// them (`get_at`, `lookup`, `current`, ...). + pub(crate) container: Option, } impl OutputSite { @@ -64,6 +117,8 @@ impl OutputSite { Self { position, try_method: false, + documented_null: false, + container: None, } } @@ -71,15 +126,30 @@ impl OutputSite { Self { position, try_method: is_try_method(method), + documented_null: method.documented_null_result, + container: method.element_access.map(ElementContainer::from), } } - /// The site of a value nested in this one, such as an async result or a - /// collection element. Member facts carry over; the policy decides where - /// they apply. + /// An element read from a collection of the given kind. + pub(crate) fn element_of(container: ElementContainer) -> Self { + Self::of(OutputPosition::CollectionElement).element_in(container) + } + + /// The site of a value nested in this one, such as an async result. + /// Member facts carry over; the policy decides where they apply. pub(crate) fn nested(self, position: OutputPosition) -> Self { Self { position, ..self } } + + /// The site of an element held by `container`, nested in this value. + pub(crate) fn element_in(self, container: ElementContainer) -> Self { + Self { + position: OutputPosition::CollectionElement, + container: Some(container), + ..self + } + } } /// `TryParse`, `TryGetItemAsync`, ...: the CLR name is `Try` followed by an @@ -136,7 +206,9 @@ fn stub_output_admits_none( site: OutputSite, context: &PythonProjectionContext, ) -> bool { - use OutputPosition::{Activation, AsyncResult, CallbackParam, OutParam, Return}; + use OutputPosition::{ + Activation, AsyncResult, CallbackParam, CollectionElement, OutParam, Property, Return, + }; // `Object` positions are frequently null, e.g. the arguments of a // `TypedEventHandler`. @@ -147,8 +219,19 @@ fn stub_output_admits_none( if context.is_delegate_type(typ) { return site.position != CallbackParam; } - // `Try*` members report "not found" through a null result. - site.try_method && matches!(site.position, Return | OutParam | AsyncResult | Activation) + // `Try*` members report "not found" through a null result, and the + // Windows SDK documentation names the other members that return null. + let member_result = matches!( + site.position, + Return | OutParam | Property | AsyncResult | Activation + ); + if member_result && (site.try_method || site.documented_null) { + return true; + } + // Collection elements, including the results of `get_at`, `lookup` and + // `current`, follow the collection holding them. + matches!(site.position, CollectionElement | Return | Property) + && site.container.is_some_and(element_admits_none) } #[cfg(test)] @@ -181,6 +264,17 @@ mod tests { OutputSite { position: OutputPosition::AsyncResult, try_method: true, + documented_null: false, + container: None, + } + ); + assert_eq!( + site.element_in(ElementContainer::Array), + OutputSite { + position: OutputPosition::CollectionElement, + try_method: true, + documented_null: false, + container: Some(ElementContainer::Array), } ); } @@ -268,24 +362,25 @@ mod tests { } } + const MEMBER_RESULTS: [OutputPosition; 5] = [ + OutputPosition::Return, + OutputPosition::OutParam, + OutputPosition::Property, + OutputPosition::AsyncResult, + OutputPosition::Activation, + ]; + #[test] fn try_members_keep_none_on_their_results_only() { let try_get = method("TryGetItemAsync"); for position in POSITIONS { - let expected = matches!( - position, - OutputPosition::Return - | OutputPosition::OutParam - | OutputPosition::AsyncResult - | OutputPosition::Activation - ); assert_eq!( admits( &widget(), OutputSite::for_method(&try_get, position), AnnotationSurface::Stub ), - expected, + MEMBER_RESULTS.contains(&position), "{position:?}" ); } @@ -295,4 +390,75 @@ mod tests { AnnotationSurface::Stub )); } + + #[test] + fn documented_null_members_keep_none_on_their_results_only() { + let get_default = MethodMeta { + documented_null_result: true, + ..method("GetDefault") + }; + for position in POSITIONS { + assert_eq!( + admits( + &widget(), + OutputSite::for_method(&get_default, position), + AnnotationSurface::Stub + ), + MEMBER_RESULTS.contains(&position), + "{position:?}" + ); + } + let site = OutputSite::for_method(&get_default, OutputPosition::Return); + assert!(!admits( + &widget(), + site.element_in(ElementContainer::View), + AnnotationSurface::Stub + )); + } + + #[test] + fn collection_elements_follow_the_mutability_of_their_collection() { + let stub = AnnotationSurface::Stub; + assert!(admits( + &widget(), + OutputSite::element_of(ElementContainer::Mutable), + stub + )); + for container in [ElementContainer::View, ElementContainer::Array] { + let site = OutputSite::element_of(container); + assert!(!admits(&widget(), site, stub), "{container:?}"); + assert!(admits(&TypeMeta::Object, site, stub)); + assert!(admits(&nullable_u32(), site, stub)); + assert!(!admits(&TypeMeta::String, site, stub)); + } + for (access, expected) in [ + (ElementAccess::Mutable, true), + (ElementAccess::ReadOnly, false), + ] { + let get_at = MethodMeta { + element_access: Some(access), + ..method("GetAt") + }; + for position in [OutputPosition::Return, OutputPosition::Property] { + assert_eq!( + admits(&widget(), OutputSite::for_method(&get_at, position), stub), + expected, + "{access:?} at {position:?}" + ); + } + assert!(!admits( + &widget(), + OutputSite::for_method(&get_at, OutputPosition::AsyncProgress), + stub + )); + } + assert_eq!( + ElementContainer::of(CollectionKind::MutableSequence), + ElementContainer::Mutable + ); + assert_eq!( + ElementContainer::of(CollectionKind::KeyValuePair), + ElementContainer::View + ); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 853b36d5..37ba7e3f 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -9,7 +9,7 @@ use crate::types::{FieldMeta, TypeMeta}; use super::naming::{PythonProjectionContext, PythonSymbol, STRUCT_SYMBOLS, to_snake_case}; use super::native_types::{FoundationType, foundation_type}; -use super::nullability::AnnotationSurface; +use super::nullability::{AnnotationSurface, ElementContainer}; use super::structs::{py_struct_field_read_type, py_struct_field_type}; use super::type_helpers::{ method_pydoc_with_indent, py_collection_item_type, py_delegate_callable_type, @@ -276,7 +276,7 @@ pub(super) fn emit_method_stub_named( if method.is_property_getter && in_params.is_empty() { let prop_name = to_snake_case(method.name.strip_prefix("get_").unwrap_or(&method.name)); let py_return = return_type - .map(|typ| py_property_type(typ, AnnotationSurface::Stub, context)) + .map(|typ| py_property_type(method, typ, AnnotationSurface::Stub, context)) .unwrap_or_else(|| "None".to_string()); out.push_str(&format!("{indent}@builtins.property\n")); emit_documented_stub( @@ -328,16 +328,21 @@ pub(super) fn emit_method_stub_named( } else { format!("self, {}", py_params) }; - // `append` takes the projected input annotation, which can differ from - // the element annotation of the MutableSequence base: WinRT vectors - // may reject null on mutation, while `Object` elements are read back - // as `DynWinRTValue | None`. Empty structural protocols can make mypy - // consider the override compatible. + // `append` takes the projected input annotation, while the + // MutableSequence base reads elements back as `T | None`: WinRT + // vectors may reject null on mutation, yet anyone can store null in + // them. Empty structural protocols can make mypy consider the + // override compatible. let override_ignore = if overrides_mutable_sequence && method_name == "append" && in_params.first().is_some_and(|param| { py_param_type_safe(¶m.typ, context) - != py_collection_item_type(¶m.typ, AnnotationSurface::Stub, context) + != py_collection_item_type( + ¶m.typ, + ElementContainer::Mutable, + AnnotationSurface::Stub, + context, + ) }) { " # type: ignore[override, unused-ignore]" } else { diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index 15baf2ea..3dcd6d24 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -25,11 +25,11 @@ use crate::codegen::winrt::shared::structs::{ }; use super::collections::{ - CollectionKind, abc_name, class_interface, interface_kind, observable_vector_identity, + CollectionKind, class_interface, interface_kind, observable_vector_identity, }; use super::naming::{PythonProjectionContext, PythonSupportSymbol, is_py_reserved, to_snake_case}; use super::native_types::foundation_type; -use super::nullability::AnnotationSurface; +use super::nullability::{AnnotationSurface, ElementContainer}; use super::shared::reorder_getters_before_setters; use super::signature::py_dynwinrt_type; use super::stub_helpers::{ @@ -452,9 +452,9 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf } } - let collection_base = collection_kind.and_then(abc_name).and_then(|abc| { + let collection_base = collection_kind.and_then(|kind| { super::type_helpers::py_collection_base_type( - abc, + kind, &iface.generic_args, AnnotationSurface::Stub, context, @@ -856,10 +856,10 @@ pub fn generate_class_stub( } let collection_base = collection_iface - .zip(collection_kind.and_then(abc_name)) - .and_then(|(iface, abc)| { + .zip(collection_kind) + .and_then(|(iface, kind)| { super::type_helpers::py_collection_base_type( - abc, + kind, &iface.generic_args, AnnotationSurface::Stub, context, @@ -1045,16 +1045,14 @@ pub fn generate_class_stub( continue; } out.push('\n'); - let required_base = interface_kind(req_iface) - .and_then(abc_name) - .and_then(|abc| { - super::type_helpers::py_collection_base_type( - abc, - &req_iface.generic_args, - AnnotationSurface::Stub, - context, - ) - }); + let required_base = interface_kind(req_iface).and_then(|kind| { + super::type_helpers::py_collection_base_type( + kind, + &req_iface.generic_args, + AnnotationSurface::Stub, + context, + ) + }); if let Some(base) = required_base { out.push_str(&format!("\nclass {symbol}({base}):\n")); } else { @@ -1275,11 +1273,18 @@ fn collection_protocol_stubs( return String::new(); }; let indent = " ".repeat(indent_spaces); + // Item positions inherit the element rule of the collection that owns them. + let container = ElementContainer::of(kind); let item_type = iface .generic_args .first() .map(|typ| { - super::type_helpers::py_collection_item_type(typ, AnnotationSurface::Stub, context) + super::type_helpers::py_collection_item_type( + typ, + container, + AnnotationSurface::Stub, + context, + ) }) .unwrap_or_else(|| "object".to_string()); let item_input = iface @@ -1326,6 +1331,7 @@ fn collection_protocol_stubs( .map(|typ| { super::type_helpers::py_collection_item_type( typ, + container, AnnotationSurface::Stub, context, ) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 1b75501a..46c9ba5c 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -16,7 +16,8 @@ use super::naming::to_snake_case; use super::naming::{PythonProjectionContext, PythonSupportSymbol, PythonSymbol}; use super::native_types::{FoundationType, foundation_type}; use super::nullability::{ - AnnotationSurface, OutputPosition, OutputSite, may_project_none, output_admits_none, + AnnotationSurface, ElementContainer, OutputPosition, OutputSite, may_project_none, + output_admits_none, }; /// Build the Python docstring for a method body. Uses snake_case param display @@ -245,9 +246,16 @@ fn spell_member( ) }; match typ { - TypeMeta::Array(inner) if context.is_delegate_type(inner) => { - format!("list[{}]", nested(inner, OutputPosition::CollectionElement)) - } + TypeMeta::Array(inner) if context.is_delegate_type(inner) => format!( + "list[{}]", + render_output( + inner, + Spelling::Member, + array_element(site), + surface, + context + ) + ), TypeMeta::AsyncOperation(result) => { format!( "WinRTCoroutine[{}]", @@ -306,7 +314,13 @@ fn spell_value( TypeMeta::Array(inner) if matches!(inner.as_ref(), TypeMeta::U8) => "bytes".to_string(), TypeMeta::Array(inner) => format!( "list[{}]", - nested(inner, Spelling::Element, OutputPosition::CollectionElement) + render_output( + inner, + Spelling::Element, + array_element(site), + surface, + context + ) ), TypeMeta::String | TypeMeta::Char16 => "str".to_string(), TypeMeta::Guid => "UUID".to_string(), @@ -343,22 +357,22 @@ fn spell_collection( let TypeMeta::Parameterized { args, .. } = typ else { return None; }; - let abc = type_kind(typ).and_then(abc_name)?; + let kind = type_kind(typ)?; + let abc = abc_name(kind)?; + let element = site.element_in(ElementContainer::of(kind)); let elements = args .iter() - .map(|arg| { - render_output( - arg, - Spelling::Element, - site.nested(OutputPosition::CollectionElement), - surface, - context, - ) - }) + .map(|arg| render_output(arg, Spelling::Element, element, surface, context)) .collect::>(); Some(format!("{abc}[{}]", elements.join(", "))) } +/// The elements of an array filled by a member that reads collection +/// elements (`get_many`) follow that collection; other arrays are snapshots. +fn array_element(site: OutputSite) -> OutputSite { + site.element_in(site.container.unwrap_or(ElementContainer::Array)) +} + /// Pessimistic rendering for callers outside the output policy (callback /// parameters and the `IReference` input arm): every value that may /// project as `None` admits it, as on the runtime surface. @@ -378,42 +392,40 @@ pub(crate) fn py_return_type_safe( .unwrap_or_else(|| "None".to_string()) } -/// Annotation of a property getter's value. +/// Annotation of the value read by a property getter. pub(super) fn py_property_type( + getter: &MethodMeta, typ: &TypeMeta, surface: AnnotationSurface, context: &PythonProjectionContext, ) -> String { py_output_annotation( typ, - OutputSite::of(OutputPosition::Property), + OutputSite::for_method(getter, OutputPosition::Property), surface, context, ) } -/// Item, key or value type of a projected collection class. +/// Item, key or value type of a projected collection held by `container`. pub(super) fn py_collection_item_type( typ: &TypeMeta, + container: ElementContainer, surface: AnnotationSurface, context: &PythonProjectionContext, ) -> String { - py_output_annotation( - typ, - OutputSite::of(OutputPosition::CollectionElement), - surface, - context, - ) + py_output_annotation(typ, OutputSite::element_of(container), surface, context) } -/// `Sequence[T]` / `Mapping[K, V]` base of a projected collection class. +/// `Sequence[T]` / `Mapping[K, V]` base of a projected collection of `kind`. pub(super) fn py_collection_base_type( - abc: &str, + kind: CollectionKind, args: &[TypeMeta], surface: AnnotationSurface, context: &PythonProjectionContext, ) -> Option { - let item = |typ| py_collection_item_type(typ, surface, context); + let abc = abc_name(kind)?; + let item = |typ| py_collection_item_type(typ, ElementContainer::of(kind), surface, context); match args { [element] => Some(format!("{abc}[{}]", item(element))), [key, value] => Some(format!("{abc}[{}, {}]", item(key), item(value))), @@ -1002,12 +1014,71 @@ mod tests { returned(&TypeMeta::Object, stub, &context), "DynWinRTValue | None" ); - assert_eq!(py_property_type(&widget, stub, &context), "Widget"); - assert_eq!(py_collection_item_type(&widget, stub, &context), "Widget"); + let getter = method("get_Widget", vec![], widget.clone()); + assert_eq!(py_property_type(&getter, &widget, stub, &context), "Widget"); + let documented_getter = MethodMeta { + documented_null_result: true, + ..getter.clone() + }; assert_eq!( - py_collection_base_type("Sequence", std::slice::from_ref(&widget), stub, &context), + py_property_type(&documented_getter, &widget, stub, &context), + "Widget | None" + ); + assert_eq!( + py_collection_item_type(&widget, ElementContainer::View, stub, &context), + "Widget" + ); + assert_eq!( + py_collection_item_type(&widget, ElementContainer::Mutable, stub, &context), + "Widget | None" + ); + assert_eq!( + py_collection_base_type( + CollectionKind::Sequence, + std::slice::from_ref(&widget), + stub, + &context + ), Some("Sequence[Widget]".to_string()) ); + assert_eq!( + py_collection_base_type( + CollectionKind::MutableMapping, + &[TypeMeta::String, widget.clone()], + stub, + &context + ), + Some("MutableMapping[str, Widget | None]".to_string()) + ); + let vector = |piid: &str| TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVector`1".into(), + piid: piid.into(), + args: vec![widget.clone()], + }; + for piid in [ + crate::codegen::winrt::python::collections::IVECTOR_PIID, + crate::codegen::winrt::python::collections::IOBSERVABLE_VECTOR_PIID, + ] { + assert_eq!( + returned(&vector(piid), stub, &context), + "MutableSequence[Widget | None]" + ); + } + let get_many = MethodMeta { + params: vec![ParamMeta { + name: "items".into(), + typ: TypeMeta::Array(Box::new(widget.clone())), + direction: ParamDirection::OutFill, + }], + return_type: Some(TypeMeta::U32), + element_access: Some(crate::meta::ElementAccess::Mutable), + ..method("GetMany", vec![], TypeMeta::U32) + }; + assert_eq!( + py_method_return_type(&get_many, stub, &context), + "list[Widget | None]" + ); let get_item = method("GetItemAsync", vec![], async_of(&widget)); let try_get_item = method("TryGetItemAsync", vec![], async_of(&widget)); diff --git a/tools/dynwinrt-codegen/src/documented_nulls.rs b/tools/dynwinrt-codegen/src/documented_nulls.rs new file mode 100644 index 00000000..83117a13 --- /dev/null +++ b/tools/dynwinrt-codegen/src/documented_nulls.rs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Windows SDK members whose documented result can be null. +//! +//! WinRT metadata carries no nullability. `api-docs/windows-null-results.txt` +//! lists the doc comment IDs (`M:`/`P:` api-ids) of the Windows SDK methods +//! and properties whose documentation says the result can be null; +//! `scripts/extract-null-results.py` derives it from MicrosoftDocs/winrt-api. +//! A member is looked up by the type its documentation lists it under, its +//! CLR name, and its parameter types. + +use std::collections::HashSet; +use std::sync::LazyLock; + +use crate::meta::MethodMeta; +use crate::types::TypeMeta; + +const TABLE: &str = include_str!("../api-docs/windows-null-results.txt"); + +struct Table { + members: HashSet, + owners: HashSet, +} + +static DOCUMENTED: LazyLock = LazyLock::new(|| { + let members = entries().map(normalize_api_id).collect::>(); + let owners = members.iter().filter_map(|id| owner_of(id)).collect(); + Table { members, owners } +}); + +/// The api-ids listed in the table, verbatim. +pub(crate) fn entries() -> impl Iterator { + TABLE + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) +} + +/// Whether the documentation of `method`, listed under the type named +/// `owner` (`Namespace.Type`), says its result can be null. +pub(crate) fn documents_null_result(owner: &str, method: &MethodMeta) -> bool { + DOCUMENTED.owners.contains(owner) + && member_id(owner, method).is_some_and(|id| DOCUMENTED.members.contains(&id)) +} + +/// The normalized doc comment ID of `method` listed under `owner`. Only +/// methods and property getters produce results. +pub(crate) fn member_id(owner: &str, method: &MethodMeta) -> Option { + if method.is_property_getter { + let property = method.raw_name.strip_prefix("get_")?; + return Some(format!("P:{owner}.{property}")); + } + if method.is_property_setter || method.is_event_add || method.is_event_remove { + return None; + } + let parameters = method + .params + .iter() + .map(|parameter| doc_type_name(¶meter.typ)) + .collect::>() + .join(","); + Some(format!("M:{owner}.{}({parameters})", method.raw_name)) +} + +/// Normalizes an api-id so that metadata can reproduce it: methods always +/// carry a parameter list, and parameter types lose by-reference markers, +/// modifiers and generic arguments. +pub(crate) fn normalize_api_id(api_id: &str) -> String { + let Some(open) = api_id.find('(') else { + return if api_id.starts_with("M:") { + format!("{api_id}()") + } else { + api_id.to_string() + }; + }; + let parameters = split_parameters(api_id[open + 1..].trim_end_matches(')')) + .into_iter() + .map(normalize_doc_type) + .collect::>() + .join(","); + format!("{}({parameters})", &api_id[..open]) +} + +fn owner_of(id: &str) -> Option { + let member = id.get(2..)?.split('(').next()?; + member.rsplit_once('.').map(|(owner, _)| owner.to_string()) +} + +fn split_parameters(list: &str) -> Vec<&str> { + let mut parameters = Vec::new(); + let (mut depth, mut start) = (0usize, 0usize); + for (index, character) in list.char_indices() { + match character { + '{' => depth += 1, + '}' => depth = depth.saturating_sub(1), + ',' if depth == 0 => { + parameters.push(&list[start..index]); + start = index + 1; + } + _ => {} + } + } + if !list.is_empty() { + parameters.push(&list[start..]); + } + parameters +} + +fn normalize_doc_type(name: &str) -> String { + let name = name.split('!').next().unwrap_or(name).trim_end_matches('@'); + let mut result = String::with_capacity(name.len()); + let mut depth = 0usize; + for character in name.chars() { + match character { + '{' => depth += 1, + '}' => depth = depth.saturating_sub(1), + _ if depth == 0 => result.push(character), + _ => {} + } + } + match result.split_once('`') { + Some((definition, _)) => definition.to_string(), + // Some pages spell this struct with its .NET projection. + None if result == "System.Type" => "Windows.UI.Xaml.Interop.TypeName".to_string(), + None => result, + } +} + +/// The doc comment ID name of a parameter type, after normalization. +fn doc_type_name(typ: &TypeMeta) -> String { + let name = match typ { + TypeMeta::Bool => "System.Boolean", + TypeMeta::I8 => "System.SByte", + TypeMeta::U8 => "System.Byte", + TypeMeta::I16 => "System.Int16", + TypeMeta::U16 => "System.UInt16", + TypeMeta::I32 => "System.Int32", + TypeMeta::U32 => "System.UInt32", + TypeMeta::I64 => "System.Int64", + TypeMeta::U64 => "System.UInt64", + TypeMeta::F32 => "System.Single", + TypeMeta::F64 => "System.Double", + TypeMeta::Char16 => "System.Char", + TypeMeta::String => "System.String", + TypeMeta::Guid => "System.Guid", + TypeMeta::Object => "System.Object", + TypeMeta::AsyncAction => "Windows.Foundation.IAsyncAction", + TypeMeta::AsyncActionWithProgress(_) => "Windows.Foundation.IAsyncActionWithProgress", + TypeMeta::AsyncOperation(_) => "Windows.Foundation.IAsyncOperation", + TypeMeta::AsyncOperationWithProgress(..) => { + "Windows.Foundation.IAsyncOperationWithProgress" + } + TypeMeta::Array(inner) => return format!("{}[]", doc_type_name(inner)), + TypeMeta::Interface { + namespace, name, .. + } + | TypeMeta::RuntimeClass { + namespace, name, .. + } + | TypeMeta::Delegate { + namespace, name, .. + } + | TypeMeta::Struct { + namespace, name, .. + } + | TypeMeta::Enum { + namespace, name, .. + } + | TypeMeta::Parameterized { + namespace, name, .. + } => { + let definition = name.split('`').next().unwrap_or(name); + return format!("{namespace}.{definition}"); + } + }; + name.to_string() +} + +#[cfg(test)] +mod tests { + use std::collections::{BTreeMap, BTreeSet}; + use std::path::Path; + + use super::*; + use crate::meta::{ParamDirection, ParamMeta}; + + const WINDOWS_WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + + #[test] + fn api_ids_normalize_to_metadata_reproducible_keys() { + assert_eq!( + normalize_api_id("M:Windows.Devices.Sensors.Compass.GetDefault"), + "M:Windows.Devices.Sensors.Compass.GetDefault()" + ); + assert_eq!( + normalize_api_id( + "M:N.T.Find(Windows.Foundation.Collections.IMap{System.String,Windows.Foundation.Collections.IVector{System.String}},System.Byte[]@,System.Guid@!System.Runtime.CompilerServices.IsConst)" + ), + "M:N.T.Find(Windows.Foundation.Collections.IMap,System.Byte[],System.Guid)" + ); + assert_eq!(normalize_api_id("P:N.T.Value"), "P:N.T.Value"); + assert_eq!( + normalize_api_id("M:N.T.Get(System.Type)"), + normalize_api_id("M:N.T.Get(Windows.UI.Xaml.Interop.TypeName)") + ); + assert_eq!(owner_of("M:N.T.Find()").as_deref(), Some("N.T")); + } + + #[test] + fn member_ids_use_clr_names_and_parameter_types() { + let method = MethodMeta { + name: "GetDefaultWithAccelerometerReadingType".into(), + raw_name: "GetDefault".into(), + params: vec![ + ParamMeta { + name: "readingType".into(), + typ: TypeMeta::Enum { + namespace: "Windows.Devices.Sensors".into(), + name: "AccelerometerReadingType".into(), + underlying: Box::new(TypeMeta::I32), + members: vec![], + is_flags: false, + doc: None, + deprecated: None, + }, + direction: ParamDirection::In, + }, + ParamMeta { + name: "values".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IIterable`1".into(), + piid: String::new(), + args: vec![TypeMeta::String], + })), + direction: ParamDirection::Out, + }, + ], + ..Default::default() + }; + assert_eq!( + member_id("Windows.Devices.Sensors.Accelerometer", &method).as_deref(), + Some( + "M:Windows.Devices.Sensors.Accelerometer.GetDefault(Windows.Devices.Sensors.AccelerometerReadingType,Windows.Foundation.Collections.IIterable[])" + ) + ); + let getter = MethodMeta { + name: "get_Parent".into(), + raw_name: "get_Parent".into(), + is_property_getter: true, + ..Default::default() + }; + assert_eq!( + member_id("Windows.UI.Xaml.FrameworkElement", &getter).as_deref(), + Some("P:Windows.UI.Xaml.FrameworkElement.Parent") + ); + let setter = MethodMeta { + name: "put_Parent".into(), + raw_name: "put_Parent".into(), + is_property_setter: true, + ..Default::default() + }; + assert_eq!(member_id("N.T", &setter), None); + } + + /// Every entry names a member of the Windows SDK metadata whose parsed + /// method carries the documented-null fact. This guards the table against + /// typos and drift, and the key derivation against metadata changes. + #[test] + fn every_entry_resolves_to_a_flagged_windows_sdk_member() { + // Documented, but absent from the 10.0.26100 SDK metadata: newer APIs, + // and AllJoyn, which the SDK dropped. + const ABSENT_FROM_TEST_SDK: [&str; 3] = [ + "M:Windows.Devices.AllJoyn.AllJoynServiceInfo.FromIdAsync(System.String)", + "M:Windows.Gaming.UI.GameMonitor.GetDefault()", + "M:Windows.System.User.GetUserAgeRangeAsync()", + ]; + if !Path::new(WINDOWS_WINMD).is_file() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + let index = crate::meta::load_index(WINDOWS_WINMD).expect("Windows.winmd index"); + let mut by_owner = BTreeMap::>::new(); + for entry in entries() { + let id = normalize_api_id(entry); + let owner = owner_of(&id).expect("owner"); + by_owner.entry(owner).or_default().insert(id); + } + let mut unresolved = Vec::new(); + let mut unflagged = Vec::new(); + for (owner, ids) in &by_owner { + let (namespace, name) = owner.rsplit_once('.').expect("qualified owner"); + let methods = crate::meta::documented_owner_methods(&index, namespace, name); + for id in ids { + let matches = methods + .iter() + .filter(|method| member_id(owner, method).as_deref() == Some(id)) + .collect::>(); + if ABSENT_FROM_TEST_SDK.contains(&id.as_str()) { + let member = id[2..] + .split('(') + .next() + .unwrap() + .rsplit('.') + .next() + .unwrap(); + assert!( + !methods.iter().any(|method| method.raw_name == member), + "{id} is present in the test SDK; drop it from ABSENT_FROM_TEST_SDK" + ); + } else if matches.is_empty() { + unresolved.push(id.clone()); + } else if !matches.iter().any(|method| method.documented_null_result) { + unflagged.push(id.clone()); + } + } + } + assert!(unresolved.is_empty(), "unresolved entries: {unresolved:#?}"); + assert!(unflagged.is_empty(), "entries not applied: {unflagged:#?}"); + assert!(by_owner.values().map(BTreeSet::len).sum::() > 900); + } +} diff --git a/tools/dynwinrt-codegen/src/lib.rs b/tools/dynwinrt-codegen/src/lib.rs index ef0c3b86..09db795e 100644 --- a/tools/dynwinrt-codegen/src/lib.rs +++ b/tools/dynwinrt-codegen/src/lib.rs @@ -5,6 +5,7 @@ pub mod codegen; mod com_activation_registry; pub mod com_metadata; mod contract_registry; +mod documented_nulls; pub mod meta; pub mod types; mod win32_contracts; diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 93e51ee8..729b504c 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -57,6 +57,46 @@ pub struct MethodMeta { pub param_docs: std::collections::HashMap, /// XML `` text. pub returns_doc: Option, + /// The Windows SDK documentation says the result can be null: a method's + /// return value (for asynchronous methods, the completed result) or a + /// property's value. See `documented_nulls`. + pub documented_null_result: bool, + /// Set on the members that read elements of the + /// `Windows.Foundation.Collections` interface declaring them. + pub element_access: Option, +} + +/// How a member reads the elements of the `Windows.Foundation.Collections` +/// interface declaring it: `GetAt`, `GetMany`, `Lookup`, `Current`, `Key` and +/// `Value`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ElementAccess { + /// `IIterator`, `IVectorView`, `IMapView` and `IKeyValuePair`. + ReadOnly, + /// `IVector` and `IMap`, in which anyone can store null. + Mutable, +} + +fn collection_element_access( + namespace: &str, + definition: &str, + member: &str, +) -> Option { + if namespace != WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE { + return None; + } + let access = match definition { + "IIterator`1" | "IVectorView`1" | "IMapView`2" | "IKeyValuePair`2" => { + ElementAccess::ReadOnly + } + "IVector`1" | "IMap`2" => ElementAccess::Mutable, + _ => return None, + }; + matches!( + member, + "GetAt" | "GetMany" | "Lookup" | "get_Current" | "get_Key" | "get_Value" + ) + .then_some(access) } /// A WinRT interface with its methods. @@ -1224,6 +1264,8 @@ fn parse_class_from_index(index: &reader::Index, namespace: &str, name: &str) -> let mut ancestor_key: Option<(String, String)> = def .extends() .map(|e| (e.namespace().to_string(), e.name().to_string())); + // The documentation lists inherited members under the class declaring them. + let mut documentation_owners = vec![full_name.clone()]; while let Some((ext_ns, ext_name)) = ancestor_key.take() { if ext_ns == "System" && ext_name == "Object" { break; @@ -1232,6 +1274,7 @@ fn parse_class_from_index(index: &reader::Index, namespace: &str, name: &str) -> Some(d) => d, None => break, }; + documentation_owners.push(format!("{ext_ns}.{ext_name}")); for iface_impl in parent_def.interface_impls() { let iface_ty = iface_impl.interface(&[]); if iface_impl.has_attribute("OverridableAttribute") { @@ -1378,6 +1421,19 @@ fn parse_class_from_index(index: &reader::Index, namespace: &str, name: &str) -> } } + for interface in default_interface + .iter_mut() + .chain(required_interfaces.iter_mut()) + .chain(factory_interfaces.iter_mut()) + .chain(static_interfaces.iter_mut()) + { + for method in &mut interface.methods { + method.documented_null_result |= documentation_owners + .iter() + .any(|owner| crate::documented_nulls::documents_null_result(owner, method)); + } + } + Some(ClassMeta { name: name.to_string(), namespace: namespace.to_string(), @@ -1428,6 +1484,36 @@ fn parse_interface(index: &reader::Index, namespace: &str, name: &str) -> Option parse_interface_methods(index, &def, name, namespace, &iid, &[]) } +/// The parsed methods that the documentation lists under `namespace.name`: +/// the members of a class's interfaces, or an interface's own members. +#[cfg(test)] +pub(crate) fn documented_owner_methods( + index: &reader::Index, + namespace: &str, + name: &str, +) -> Vec { + let Some(def) = index.get(namespace, name).next() else { + return Vec::new(); + }; + if def.extends().is_none() { + return parse_interface(index, namespace, name) + .map(|interface| interface.methods) + .unwrap_or_default(); + } + parse_class_from_index(index, namespace, name) + .map(|class| { + class + .default_interface + .into_iter() + .chain(class.required_interfaces) + .chain(class.factory_interfaces) + .chain(class.static_interfaces) + .flat_map(|interface| interface.methods) + .collect() + }) + .unwrap_or_default() +} + fn parse_interface_type( index: &reader::Index, interface_type: &windows_metadata::Type, @@ -1582,6 +1668,9 @@ fn parse_interface_methods( ) -> Option { let winmd_generics: Vec = generic_args.iter().map(type_meta_to_winmd_type).collect(); + // The documentation lists interface members under the interface + // definition, e.g. `Windows.Foundation.Collections.IVector`1`. + let documentation_owner = format!("{namespace}.{}", def.name()); let mut methods = Vec::new(); let mut implementation_metadata = InterfaceImplementationMetadata { @@ -1741,7 +1830,8 @@ fn parse_interface_methods( format!("({})", clr_sig_types.join(",")) }; - methods.push(MethodMeta { + let element_access = collection_element_access(namespace, def.name(), &raw_name); + let mut method_meta = MethodMeta { name: method_name.clone(), vtable_index, params, @@ -1756,7 +1846,12 @@ fn parse_interface_methods( deprecated: None, param_docs: std::collections::HashMap::new(), returns_doc: None, - }); + documented_null_result: false, + element_access, + }; + method_meta.documented_null_result = + crate::documented_nulls::documents_null_result(&documentation_owner, &method_meta); + methods.push(method_meta); } let (generic_piid, generic_args_vec) = if !generic_args.is_empty() { diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 04191b2c..ba16d503 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -421,7 +421,7 @@ def collections(resource: Resource, derived: OtherDerived, raw: DynWinRTValue, mapping[resource] = derived assert_type(vector[0], DynWinRTValue | None) assert_type(vector[:], list[DynWinRTValue | None]) - assert_type(resources[0], Resource) + assert_type(resources[0], Resource | None) assert_type(mapping[resource], DynWinRTValue | None) del mapping[resource] "#, @@ -814,7 +814,7 @@ print("collection-subscript-native-ok", flush=True) } #[test] -fn natural_sdk_consumers_need_no_none_guards() { +fn natural_sdk_consumers_guard_only_nullable_results() { let winmd = Path::new( r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd", ); @@ -834,7 +834,8 @@ fn natural_sdk_consumers_need_no_none_guards() { Windows.Security.Cryptography.Core.HashAlgorithmProvider,\ Windows.Storage.StorageFolder,Windows.Storage.FileIO,\ Windows.Storage.Streams.DataReader,Windows.Storage.Streams.DataWriter,\ - Windows.Storage.Streams.InMemoryRandomAccessStream", + Windows.Storage.Streams.InMemoryRandomAccessStream,\ + Windows.Devices.Sensors.Accelerometer", "--lang", "py", "--output", @@ -846,7 +847,8 @@ fn natural_sdk_consumers_need_no_none_guards() { let imports = r#"from collections.abc import Sequence from typing import assert_type from dynwinrt import DynWinRTValue, WinRTCoroutine -from sdk.windows.data.json import JsonObject +from sdk.windows.data.json import IJsonValue, JsonObject +from sdk.windows.devices.sensors import Accelerometer from sdk.windows.foundation import Uri from sdk.windows.foundation.collections import PropertySet from sdk.windows.globalization import Calendar @@ -868,7 +870,15 @@ def uri_demo() -> str: def json_demo() -> list[str]: parsed = JsonObject.parse('{{"tags": ["a", "b"]}}') assert_type(JsonObject.try_parse("{{}}"), tuple[JsonObject | None, bool]) - return [value.get_string() for value in parsed.get_named_array("tags")] + tags = parsed.get_named_array("tags") + assert_type(tags[0], IJsonValue | None) + return [value.get_string() for value in tags if value is not None] + +def sensor_demo() -> float | None: + accelerometer = Accelerometer.get_default() + if accelerometer is None: + return None + return accelerometer.get_current_reading().acceleration_x def calendar_demo(calendar: Calendar) -> str: languages: Sequence[str] = calendar.languages @@ -897,6 +907,7 @@ async def storage_demo(path: str) -> list[str]: await FileIO.write_text_async(file, "first line") assert_type(folder.create_file_async("a.txt"), WinRTCoroutine[StorageFile]) assert_type(folder.try_get_item_async("notes.txt"), WinRTCoroutine[IStorageItem | None]) + assert_type(folder.get_parent_async(), WinRTCoroutine[StorageFolder | None]) return [item.name for item in await folder.get_files_async()] "# ), diff --git a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs index 552988fe..a0dba5e7 100644 --- a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs +++ b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs @@ -3,9 +3,10 @@ //! The stub nullability policy on real Windows SDK metadata. Values received //! from the projection are non-null in `.pyi` stubs by default, while -//! `IReference`, `Try*` results, `Object` and delegate values keep -//! `| None`. Inputs, implementation protocols and the runtime `.py` -//! annotations are unchanged. +//! `IReference`, `Try*` results, members documented to return null, +//! `Object` and delegate values keep `| None`. Collection elements follow the +//! mutability of the collection holding them. Inputs, implementation +//! protocols and the runtime `.py` annotations are unchanged. use std::fs; use std::path::{Path, PathBuf}; @@ -23,7 +24,7 @@ impl Drop for Generated { } impl Generated { - fn new(classes: &str) -> Option { + fn new(label: &str, classes: &str) -> Option { if !Path::new(WINDOWS_WINMD).is_file() { eprintln!("Skipping: Windows.winmd not found"); return None; @@ -34,7 +35,7 @@ impl Generated { .parent() .unwrap() .join("target") - .join(format!("pn{}", std::process::id())); + .join(format!("pn{label}{}", std::process::id())); let _ = fs::remove_dir_all(&root); let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) .args([ @@ -69,8 +70,11 @@ fn assert_contains(text: &str, expected: &str) { #[test] fn stub_outputs_follow_the_nullability_policy() { let Some(generated) = Generated::new( + "out", "Windows.Storage.StorageFolder,Windows.Web.Http.Headers.HttpContentHeaderCollection,\ - Windows.Foundation.Collections.PropertySet,Windows.Data.Json.JsonObject", + Windows.Foundation.Collections.PropertySet,Windows.Data.Json.JsonObject,\ + Windows.Devices.Sensors.Accelerometer,Windows.Devices.Sensors.Compass,\ + Windows.System.DispatcherQueue,Windows.Data.Xml.Dom.XmlDocument", ) else { return; }; @@ -81,6 +85,10 @@ fn stub_outputs_follow_the_nullability_policy() { generated.module("windows__web__http__headers__http_content_header_collection.pyi"); let properties = generated.module("windows__foundation__collections__property_set.pyi"); let json = generated.module("windows__data__json__json_object.pyi"); + let accelerometer = generated.module("windows__devices__sensors__accelerometer.pyi"); + let compass = generated.module("windows__devices__sensors__compass.pyi"); + let dispatcher = generated.module("windows__system__dispatcher_queue.pyi"); + let xml = generated.module("windows__data__xml__dom__xml_document.pyi"); // Method, async, collection and property outputs are non-null by default. assert_contains( @@ -115,11 +123,40 @@ fn stub_outputs_follow_the_nullability_policy() { "def try_parse(input: str) -> tuple[JsonObject | None, bool]: ...", ); - // IReference values and Object values keep None. + // Members the Windows SDK documentation says can return null keep None, + // including through overloads, interfaces and async results. + assert_contains( + &accelerometer, + "def get_default() -> Accelerometer | None: ...", + ); + assert_contains( + &accelerometer, + "def get_default_with_accelerometer_reading_type(reading_type: 'AccelerometerReadingType') -> Accelerometer | None: ...", + ); + assert_contains(&compass, "def get_default() -> Compass | None: ..."); + assert_contains( + &dispatcher, + "def get_for_current_thread() -> DispatcherQueue | None: ...", + ); + assert_contains( + &dispatcher, + "def create_timer(self) -> DispatcherQueueTimer: ...", + ); + assert_contains( + &folder, + "def get_parent_async(self) -> WinRTCoroutine[StorageFolder | None]: ...", + ); + assert_contains( + &xml, + "def select_single_node(self, xpath: str) -> IXmlNode | None: ...", + ); + + // IReference values and Object values keep None, and so do properties + // whose documentation says a null value means "absent". assert_contains(&headers, "def content_length(self) -> int | None: ..."); assert_contains( &headers, - "def content_type(self) -> HttpMediaTypeHeaderValue: ...", + "def content_type(self) -> HttpMediaTypeHeaderValue | None: ...", ); assert_contains( &properties, @@ -161,3 +198,78 @@ fn stub_outputs_follow_the_nullability_policy() { "def properties(self) -> StorageItemContentProperties | None:", ); } + +#[test] +fn collection_elements_follow_the_mutability_of_their_collection() { + let Some(generated) = Generated::new( + "elements", + "Windows.Storage.StorageFolder,Windows.Data.Json.JsonObject,\ + Windows.ApplicationModel.Resources.Core.ResourceMap,Windows.Media.Playback.MediaPlaybackList", + ) else { + return; + }; + let files = + generated.module("windows__foundation__collections__i_vector_view_storage_file.pyi"); + let array = generated.module("windows__data__json__json_array.pyi"); + let object = generated.module("windows__data__json__json_object.pyi"); + let resources = + generated.module("windows__application_model__resources__core__resource_map.pyi"); + let playlist = generated.module("windows__media__playback__media_playback_list.pyi"); + let observable = generated + .module("windows__foundation__collections__i_observable_vector_media_playback_item.pyi"); + + // Views keep non-null elements, including their item positions. + assert_contains(&files, "def get_at(self, index: int) -> StorageFile: ..."); + assert_contains( + &files, + "def __getitem__(self, index: int) -> StorageFile: ...", + ); + assert_contains( + &resources, + "class ResourceMap(_ResourceMapIdentity, Mapping[str, NamedResource], _DynWinRTRuntimeClass):", + ); + assert_contains( + &resources, + "def lookup(self, key: str) -> NamedResource: ...", + ); + + // Anyone can store null in a mutable collection, so its elements, item + // positions and element-reading members keep None. + assert_contains( + &array, + "class JsonArray(_JsonArrayIdentity, MutableSequence[IJsonValue | None], _DynWinRTRuntimeClass):", + ); + assert_contains( + &array, + "def get_at(self, index: int) -> IJsonValue | None: ...", + ); + assert_contains( + &array, + "def __getitem__(self, index: int) -> IJsonValue | None: ...", + ); + assert_contains( + &array, + "def get_object_at(self, index: int) -> JsonObject: ...", + ); + assert_contains( + &object, + "class JsonObject(_JsonObjectIdentity, MutableMapping[str, IJsonValue | None], _DynWinRTRuntimeClass):", + ); + assert_contains( + &object, + "def lookup(self, key: str) -> IJsonValue | None: ...", + ); + assert_contains( + &object, + "def get_named_array(self, name: str) -> JsonArray: ...", + ); + assert_contains( + &playlist, + "def items(self) -> MutableSequence[MediaPlaybackItem | None]: ...", + ); + assert_contains(&observable, "MutableSequence[MediaPlaybackItem | None]):"); + assert_contains( + &observable, + "def __getitem__(self, index: int) -> MediaPlaybackItem | None: ...", + ); +} From 557eba097b4ec5b7677ed246bf302a674afef00b Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 24 Sep 2026 17:21:04 +0800 Subject: [PATCH 05/12] Test that mutable collection mutators accept None 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> --- .../tests/python_consumer_typing_test.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index ba16d503..34c7b778 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -915,6 +915,58 @@ async def storage_demo(path: str) -> list[str]: ); } +#[test] +fn mutable_collection_mutators_accept_none() { + let winmd = Path::new( + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd", + ); + if !winmd.is_file() || !has_mypy() { + eprintln!("Skipping mutable collection mutators: Windows.winmd or mypy unavailable."); + return; + } + let fixture = Fixture::new(); + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(winmd) + .args([ + "--class-name", + "Windows.Storage.StorageLibrary,Windows.Data.Json.JsonObject", + "--lang", + "py", + "--output", + ]) + .arg(fixture.0.join("sdk")) + .output() + .unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + // Inherited MutableSequence and MutableMapping mutators take the element + // type of the collection base, which keeps `| None` for mutable + // collections, like the generated item setters. + typecheck( + &fixture, + &["sdk"], + r#"from typing import assert_type +from sdk.windows.data.json import IJsonValue, JsonObject +from sdk.windows.foundation.collections import IObservableVector_StorageFolder +from sdk.windows.storage import StorageFolder + +def vector(folders: IObservableVector_StorageFolder) -> None: + folders.append(None) + folders.extend([None]) + folders.insert(0, None) + folders[0] = None + assert_type(folders[0], StorageFolder | None) + +def mapping(values: JsonObject) -> None: + values.update({"k": None}) + values.setdefault("k", None) + values["k"] = None + assert_type(values["k"], IJsonValue | None) +"#, + &[], + ); +} + #[test] fn native_object_inputs_keep_projection_factories_and_context_lifetimes() { if !has_implementation_runtime() { From 16c94565f55c6c5e34f35e239c30b0e1ab5df290 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 10:10:24 +0800 Subject: [PATCH 06/12] Extract nullable sensor readings and support null collection values 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> --- bindings/py/README.md | 27 +++-- eng/ci/test_extract_null_results.py | 111 +++++++++++++++++ tests/e2e/typecheck/python_generated_api.py | 4 +- .../api-docs/windows-null-results.txt | 7 ++ .../scripts/extract-null-results.py | 81 +++++++++---- .../codegen/winrt/python/generator/class.rs | 6 + .../src/codegen/winrt/python/generator/mod.rs | 28 ++++- .../codegen/winrt/python/generator/types.rs | 50 ++++++-- .../src/codegen/winrt/python/method.rs | 43 ++++--- .../src/codegen/winrt/python/nullability.rs | 58 +++++---- .../src/codegen/winrt/python/signature.rs | 88 +++++++++++++- .../src/codegen/winrt/python/stub_helpers.rs | 6 +- .../src/codegen/winrt/python/stubs.rs | 29 +++-- .../src/codegen/winrt/python/type_helpers.rs | 106 +++++++++++++++-- tools/dynwinrt-codegen/src/meta.rs | 45 ++++++- .../tests/observable_vector_test.rs | 4 +- .../tests/python_consumer_typing_test.rs | 93 ++++++++++++++- .../tests/python_stub_nullability_test.rs | 112 ++++++++++++++++-- ...i_iterator_i_www_form_url_decoder_entry.py | 1 + .../snapshots/uri_py/www_form_url_decoder.py | 9 +- ..._iterator_i_www_form_url_decoder_entry.pyi | 10 +- .../uri_pyi/www_form_url_decoder.pyi | 44 +++---- 22 files changed, 808 insertions(+), 154 deletions(-) create mode 100644 eng/ci/test_extract_null_results.py diff --git a/bindings/py/README.md b/bindings/py/README.md index 77835d7a..efd13ca8 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -41,17 +41,22 @@ These values keep `| None`: - `Object`/`IInspectable` values (`DynWinRTValue | None`) and delegate-typed values, which are often null. -Collection elements follow the collection holding them. Anyone can store null -in a mutable `IVector`, `IMap` or observable collection, so their elements, -item positions (`[index]`, iteration, `get_at()`, `lookup()`) and -`items()`/`values()` are typed `T | None`: a `JsonArray` holds -`IJsonValue | None`. Read-only views, iterators and arrays keep non-null -elements: `get_files_async()` returns `WinRTCoroutine[Sequence[StorageFile]]`. -A view, iterator or key-value pair obtained from a mutable collection, such as -the result of `get_view()` or `first()`, can still contain nulls although its -elements are typed non-null. - -Arguments keep accepting `None` where they did before. The stubs are +Reference-type elements read from WinRT collection interfaces are always typed +`T | None`, including vectors, views, iterables, iterators, map values and +key-value-pair values. Map keys remain non-null. A view or iterator obtained +from a mutable collection can expose a null slot, and WinRT collection +interfaces do not retain enough provenance for the stubs to distinguish that +case. For example, a `JsonArray` holds `IJsonValue | None`, and +`get_files_async()` returns `WinRTCoroutine[Sequence[StorageFile | None]]`. +Value-type elements remain non-null. + +Mutable collections accept `None` when their element or map-value type is a +WinRT reference type and store a real null WinRT value. This includes +`append()`, `insert()`, index and slice assignment, `extend()`, `update()` and +`setdefault()`. Map keys and value-type elements reject `None` with +`TypeError`. + +Other arguments keep accepting `None` where they did before. The stubs are optimistic, like the generated TypeScript declarations: the runtime still returns `None` when a WinRT API returns null, so check the API documentation when a result can legitimately be absent. The inline annotations of the diff --git a/eng/ci/test_extract_null_results.py b/eng/ci/test_extract_null_results.py new file mode 100644 index 00000000..08900752 --- /dev/null +++ b/eng/ci/test_extract_null_results.py @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import importlib.util +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ( + ROOT + / "tools" + / "dynwinrt-codegen" + / "scripts" + / "extract-null-results.py" +) +SPEC = importlib.util.spec_from_file_location("extract_null_results", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +extractor = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(extractor) + + +def method_doc( + api_id: str, + *, + returns: str = "The current reading.", + remarks: str = "", +) -> str: + return f"""--- +-api-id: {api_id} +-api-type: winrt method +--- +## -returns +{returns} +## -remarks +{remarks} +""" + + +class NullResultExtractionTests(unittest.TestCase): + def test_required_return_null_check_is_nullable(self): + wording = ( + "Before using the return value from this method, the application " + "must first check that the value is not null. (If the value is " + "null and you attempt to retrieve it, Windows will generate an " + "exception.)" + ) + for sensor in ( + "Accelerometer", + "Compass", + "Gyrometer", + "Inclinometer", + "LightSensor", + "OrientationSensor", + ): + api_id = f"M:Windows.Devices.Sensors.{sensor}.GetCurrentReading" + with self.subTest(sensor=sensor): + self.assertEqual( + extractor.classify_document( + method_doc(api_id, remarks=wording) + ), + (api_id, "remarks"), + ) + + def test_negations_arguments_and_null_holders_are_not_nullable(self): + cases = ( + "This method always returns a value that is not null.", + ( + "Before using this method, check that the input parameter is " + "not null." + ), + "The returned object contains a JSON null value.", + ) + api_id = "M:Contoso.Sensor.GetCurrentReading" + for remarks in cases: + with self.subTest(remarks=remarks): + self.assertEqual( + extractor.classify_document( + method_doc(api_id, remarks=remarks) + ), + (api_id, None), + ) + + def test_direct_result_nullability_still_uses_result_sections(self): + method_id = "M:Contoso.Sensor.GetDefault" + self.assertEqual( + extractor.classify_document( + method_doc( + method_id, + returns="The default sensor, or null if none is installed.", + ) + ), + (method_id, "returns"), + ) + + property_id = "P:Contoso.Reading.OptionalValue" + property_doc = f"""--- +-api-id: {property_id} +-api-type: winrt property +--- +## -property-value +The current value, or null when no value is available. +""" + self.assertEqual( + extractor.classify_document(property_doc), + (property_id, "property-value"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/e2e/typecheck/python_generated_api.py b/tests/e2e/typecheck/python_generated_api.py index 6230b2e6..bdff5be2 100644 --- a/tests/e2e/typecheck/python_generated_api.py +++ b/tests/e2e/typecheck/python_generated_api.py @@ -158,7 +158,9 @@ def check_ibuffer_bytes() -> None: async def check_output_nullability(folder: StorageFolder, values: ValueSet) -> None: created: StorageFile = await folder.create_file_async("notes.txt") - names: List[str] = [item.name for item in await folder.get_files_async()] + names: List[str] = [ + item.name for item in await folder.get_files_async() if item is not None + ] assert_type(folder.try_get_item_async("notes.txt"), WinRTCoroutine[IStorageItem | None]) assert_type(values["key"], DynWinRTValue | None) _: Tuple[StorageFile, List[str]] = (created, names) diff --git a/tools/dynwinrt-codegen/api-docs/windows-null-results.txt b/tools/dynwinrt-codegen/api-docs/windows-null-results.txt index 186c0eb5..51c1b97b 100644 --- a/tools/dynwinrt-codegen/api-docs/windows-null-results.txt +++ b/tools/dynwinrt-codegen/api-docs/windows-null-results.txt @@ -171,6 +171,7 @@ M:Windows.Devices.Pwm.PwmController.GetDefaultAsync M:Windows.Devices.Radios.Radio.FromIdAsync(System.String) M:Windows.Devices.Scanners.ImageScanner.FromIdAsync(System.String) M:Windows.Devices.Sensors.Accelerometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Accelerometer.GetCurrentReading M:Windows.Devices.Sensors.Accelerometer.GetDefault M:Windows.Devices.Sensors.Accelerometer.GetDefault(Windows.Devices.Sensors.AccelerometerReadingType) M:Windows.Devices.Sensors.Accelerometer.GetDeviceSelector(Windows.Devices.Sensors.AccelerometerReadingType) @@ -182,11 +183,14 @@ M:Windows.Devices.Sensors.Barometer.FromIdAsync(System.String) M:Windows.Devices.Sensors.Barometer.GetDefault M:Windows.Devices.Sensors.Barometer.GetDeviceSelector M:Windows.Devices.Sensors.Compass.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Compass.GetCurrentReading M:Windows.Devices.Sensors.Compass.GetDefault M:Windows.Devices.Sensors.Compass.GetDeviceSelector M:Windows.Devices.Sensors.Custom.CustomSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Custom.CustomSensor.GetCurrentReading M:Windows.Devices.Sensors.Custom.CustomSensor.GetDeviceSelector(System.Guid) M:Windows.Devices.Sensors.Gyrometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Gyrometer.GetCurrentReading M:Windows.Devices.Sensors.Gyrometer.GetDefault M:Windows.Devices.Sensors.Gyrometer.GetDeviceSelector M:Windows.Devices.Sensors.HingeAngleSensor.FromIdAsync(System.String) @@ -196,17 +200,20 @@ M:Windows.Devices.Sensors.HumanPresenceSensor.FromIdAsync(System.String) M:Windows.Devices.Sensors.HumanPresenceSensor.GetDefault M:Windows.Devices.Sensors.HumanPresenceSensor.GetDefaultAsync M:Windows.Devices.Sensors.Inclinometer.FromIdAsync(System.String) +M:Windows.Devices.Sensors.Inclinometer.GetCurrentReading M:Windows.Devices.Sensors.Inclinometer.GetDefault M:Windows.Devices.Sensors.Inclinometer.GetDefault(Windows.Devices.Sensors.SensorReadingType) M:Windows.Devices.Sensors.Inclinometer.GetDefaultForRelativeReadings M:Windows.Devices.Sensors.Inclinometer.GetDeviceSelector(Windows.Devices.Sensors.SensorReadingType) M:Windows.Devices.Sensors.LightSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.LightSensor.GetCurrentReading M:Windows.Devices.Sensors.LightSensor.GetDefault M:Windows.Devices.Sensors.LightSensor.GetDeviceSelector M:Windows.Devices.Sensors.Magnetometer.FromIdAsync(System.String) M:Windows.Devices.Sensors.Magnetometer.GetDefault M:Windows.Devices.Sensors.Magnetometer.GetDeviceSelector M:Windows.Devices.Sensors.OrientationSensor.FromIdAsync(System.String) +M:Windows.Devices.Sensors.OrientationSensor.GetCurrentReading M:Windows.Devices.Sensors.OrientationSensor.GetDefault M:Windows.Devices.Sensors.OrientationSensor.GetDefault(Windows.Devices.Sensors.SensorReadingType) M:Windows.Devices.Sensors.OrientationSensor.GetDefault(Windows.Devices.Sensors.SensorReadingType,Windows.Devices.Sensors.SensorOptimizationGoal) diff --git a/tools/dynwinrt-codegen/scripts/extract-null-results.py b/tools/dynwinrt-codegen/scripts/extract-null-results.py index 987a305d..837e9546 100644 --- a/tools/dynwinrt-codegen/scripts/extract-null-results.py +++ b/tools/dynwinrt-codegen/scripts/extract-null-results.py @@ -14,11 +14,13 @@ the result can be null (for an asynchronous method: its completed result), or - a sentence of its `## -remarks` section says that the member itself ("this method", "this property", "it", or the member's name) returns or is - null. -Sentences that negate null, that describe null arguments, or that describe an -object holding a null value are ignored, and so are Boolean results, attached -properties, constructors and members of generic types. Reviewed corrections -from api-docs/windows-null-results.overrides.txt are applied last. + null, or requires the caller to check that this member's return value is not + null before using it. +Other sentences that negate null, that describe null arguments, or that +describe an object holding a null value are ignored, and so are Boolean +results, attached properties, constructors and members of generic types. +Reviewed corrections from api-docs/windows-null-results.overrides.txt are +applied last. The documentation is read from git objects, without a working tree: @@ -71,6 +73,12 @@ r"\b(?:returns?|is|will\s+be|may\s+be|can\s+be|could\s+be|might\s+be|is\s+set\s+to)" r"\s+(?:a\s+|an\s+|the\s+)?null(?:ptr)?\b" ) +REQUIRED_NULL_CHECK = re.compile( + r"\b(?:must|should|need(?:s)?\s+to)\b" + r"[^.]{0,100}\bcheck\b" + r"[^.]{0,100}\b(?:the|that|return)\s+value\s+is\s+not\s+null\b", + re.I, +) def git(repo: Path, *args: str) -> str: @@ -155,6 +163,21 @@ def result_is_nullable(result: str) -> bool: return any(states_null(sentence) for sentence in sentences(result)) +def requires_return_null_check(sentence: str, member: str) -> bool: + """A required check for a member's return value proves it can be null. + + This is intentionally narrower than a generic "not null" mention. It + requires both the return-value subject and an instruction to check the + returned value before use, so statements such as "always returns a value + that is not null" and checks on input parameters remain exclusions. + """ + source = re.compile( + rf"\breturn value from (?:this\s+(?:method|function|call|operation)|{re.escape(member)})\b", + re.I, + ) + return bool(source.search(sentence) and REQUIRED_NULL_CHECK.search(sentence)) + + def remarks_say_null(remarks: str, member: str) -> bool: subject = ( r"(?:\bthis\s+(?:method|property|function|call|operation)" @@ -164,7 +187,8 @@ def remarks_say_null(remarks: str, member: str) -> bool: claim = re.compile(subject + REMARKS_GAP + REMARKS_VERB, re.I) returns = re.compile(rf"\bif\s+(?:this|it|{re.escape(member)})\s+returns\s+null\b", re.I) return any( - (claim.search(sentence) or returns.search(sentence)) and states_null(sentence) + requires_return_null_check(sentence, member) + or ((claim.search(sentence) or returns.search(sentence)) and states_null(sentence)) for sentence in sentences(remarks) ) @@ -173,28 +197,43 @@ def member_name(api_id: str) -> str: return api_id[2:].split("(", 1)[0].rsplit(".", 1)[-1] +def classify_document(text: str) -> tuple[str, str | None] | None: + """Return `(api_id, source)` for one supported docs page. + + `source` is `returns`, `property-value`, `remarks`, or `None` when the + member is documented but its result is not documented as nullable. + """ + fields = front_matter(text) + api_id = fields.get("api-id", "") + api_type = fields.get("api-type", "") + if not api_id.startswith(("M:", "P:")) or api_type in { + "winrt attachedproperty", + "winrt constructor", + }: + return None + if "#ctor" in api_id or "`" in api_id.split("(", 1)[0]: + return None + + result_section = "returns" if api_id.startswith("M:") else "property-value" + if result_is_nullable(section(text, result_section)): + return api_id, result_section + if remarks_say_null(section(text, "remarks"), member_name(api_id)): + return api_id, "remarks" + return api_id, None + + def extract(repo: Path, commit: str) -> tuple[set[str], set[str], Counter[str]]: documented: set[str] = set() nullable: set[str] = set() sources: Counter[str] = Counter() for text in documents(repo, commit): - fields = front_matter(text) - api_id = fields.get("api-id", "") - api_type = fields.get("api-type", "") - if not api_id.startswith(("M:", "P:")) or api_type in { - "winrt attachedproperty", - "winrt constructor", - }: - continue - if "#ctor" in api_id or "`" in api_id.split("(", 1)[0]: + classification = classify_document(text) + if classification is None: continue + api_id, source = classification documented.add(api_id) - result = section(text, "returns" if api_id.startswith("M:") else "property-value") - if result_is_nullable(result): - sources["returns" if api_id.startswith("M:") else "property-value"] += 1 - nullable.add(api_id) - elif remarks_say_null(section(text, "remarks"), member_name(api_id)): - sources["remarks"] += 1 + if source is not None: + sources[source] += 1 nullable.add(api_id) return documented, nullable, sources diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs index 1a670f54..418e3c20 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -65,6 +65,12 @@ pub fn generate_class( out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); out.push_str(&import_line(context)); + out.push_str(collection_item_import( + class + .all_interfaces() + .flat_map(|interface| interface.methods.iter()), + collection_iface.is_some(), + )); if has_public_composition { out.push_str( "from dynwinrt import register_xaml_runtime_class as _dynwinrt_register_xaml_runtime_class\n", diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index 76351781..47ee1993 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -30,7 +30,7 @@ use super::naming::{PythonProjectionContext, PythonSupportSymbol, is_py_reserved use super::shared::reorder_getters_before_setters; use super::signature::{ py_collect_runtime_class_iid_consts, py_dynwinrt_type, py_generate_interface_registration, - py_interface_iid_expr, py_runtime_named_symbol, py_runtime_symbol, py_wrap_native_value, + py_interface_iid_expr, py_runtime_named_symbol, py_runtime_symbol, py_wrap_collection_item, }; use super::structs::{ py_struct_field_getter, py_struct_field_read_type, py_struct_field_setter, py_struct_field_type, @@ -61,6 +61,24 @@ from ._runtime import ( ) } +fn collection_item_import<'a>( + methods: impl IntoIterator, + collection_interface: bool, +) -> &'static str { + let method_uses_helper = methods.into_iter().any(|method| { + !method.collection_inputs.is_empty() + || method.params.iter().any(|parameter| { + parameter.direction == ParamDirection::In + && super::collections::type_kind(¶meter.typ).is_some() + }) + }); + if collection_interface || method_uses_helper { + "from ._runtime import _dynwinrt_collection_item\n" + } else { + "" + } +} + const RUNTIME_SUPPORT_BODY: &str = "\ from builtins import property as _property from functools import lru_cache @@ -96,6 +114,14 @@ def _dynwinrt_wrap_values(module, name, values): return [None if value.is_null() else wrap(value) for value in values] +def _dynwinrt_collection_item(value, wrap, allow_none, role): + if value is None: + if allow_none: + return DynWinRTValue.null_value() + raise TypeError(f'{role} cannot be None') + return wrap(value) + + def _dynwinrt_enum(module, name, value): enum_type = _dynwinrt_symbol(module, name) try: diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 4eb5dd40..9ebe8dfd 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -10,6 +10,7 @@ use crate::codegen::winrt::python::collections::{ CollectionKind, interface_kind, map_iterable_identity, observable_vector_identity, runtime_mixin, }; +use crate::meta::CollectionInputRole; use crate::types::{TypeIdentity, TypeIdentityKind}; /// Generate a Python file for a single enum. @@ -93,6 +94,10 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); out.push_str(&import_line(context)); + out.push_str(collection_item_import( + iface.methods.iter(), + interface_kind(iface).is_some(), + )); if implementation.supported { out.push_str(super::super::implementation::IMPORTS); } @@ -360,11 +365,17 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe if let Some(ref piid) = iface.generic_piid { if piid == "5917eb53-50b4-4a0d-b309-65862b3f1dbc" && iface.generic_args.len() == 1 { let elem_type = py_dynwinrt_type(&iface.generic_args[0]); - let elem_annotation = crate::codegen::winrt::python::type_helpers::py_param_type_safe( + let elem_annotation = + crate::codegen::winrt::python::type_helpers::py_collection_input_type( + &iface.generic_args[0], + context, + ); + let wrap = py_wrap_collection_item( + "item", &iface.generic_args[0], + CollectionInputRole::Element, context, ); - let wrap = py_wrap_native_value("item", &iface.generic_args[0], context); let vector_identity = observable_vector .as_ref() .expect("observable vector companion"); @@ -385,11 +396,17 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe out.push('\n'); } else if piid == "913337e9-11a1-4345-a3a2-4e7f956e222d" && iface.generic_args.len() == 1 { let elem_type = py_dynwinrt_type(&iface.generic_args[0]); - let elem_annotation = crate::codegen::winrt::python::type_helpers::py_param_type_safe( + let elem_annotation = + crate::codegen::winrt::python::type_helpers::py_collection_input_type( + &iface.generic_args[0], + context, + ); + let wrap = py_wrap_collection_item( + "item", &iface.generic_args[0], + CollectionInputRole::Element, context, ); - let wrap = py_wrap_native_value("item", &iface.generic_args[0], context); out.push_str(" @staticmethod\n"); out.push_str(&format!( " def create(items: Iterable[{}]) -> '{}':\n", @@ -407,12 +424,23 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe &iface.generic_args[0], context, ); - let val_annotation = crate::codegen::winrt::python::type_helpers::py_param_type_safe( + let val_annotation = + crate::codegen::winrt::python::type_helpers::py_collection_input_type( + &iface.generic_args[1], + context, + ); + let wrap_key = py_wrap_collection_item( + "item", + &iface.generic_args[0], + CollectionInputRole::Key, + context, + ); + let wrap_value = py_wrap_collection_item( + "item", &iface.generic_args[1], + CollectionInputRole::Value, context, ); - let wrap_key = py_wrap_native_value("item", &iface.generic_args[0], context); - let wrap_value = py_wrap_native_value("item", &iface.generic_args[1], context); out.push_str(" @staticmethod\n"); out.push_str(&format!( " def create(items: Mapping[{}, {}]) -> '{}':\n", @@ -721,7 +749,7 @@ mod tests { } #[test] - fn collection_create_inputs_remain_non_nullable() { + fn collection_create_reference_inputs_accept_none() { let iface = InterfaceMeta { name: "IVector_Widget".into(), iid: "913337e9-11a1-4345-a3a2-4e7f956e222d".into(), @@ -740,7 +768,9 @@ mod tests { .unwrap(); let code = generate_interface(&context, &iface); - assert!(code.contains("def create(items: Iterable['WidgetLike'])")); - assert!(!code.contains("def create(items: Iterable[WidgetLike | None])")); + assert!(code.contains("def create(items: Iterable[WidgetLike | None])")); + assert!(code.contains( + "_dynwinrt_collection_item(item, lambda item: getattr(item, '_obj', item), True, 'collection element')" + )); } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 52484e00..26251a34 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -13,11 +13,11 @@ use super::naming::{PythonProjectionContext, PythonTypeIdentity, to_snake_case}; use super::nullability::AnnotationSurface; use super::signature::{ py_convert_return, py_runtime_named_symbol, py_runtime_symbol, py_type_guard, py_wrap_arg, - py_wrap_async, py_wrap_async_with_converters, + py_wrap_async, py_wrap_async_with_converters, py_wrap_collection_input, }; use super::type_helpers::{ method_pydoc, py_delegate_callable_type, py_factory_return_type, py_method_abi_output_count, - py_method_outputs, py_method_return_type, py_param_list, py_property_type, + py_method_outputs, py_method_param_list, py_method_return_type, py_property_type, }; fn is_delegate_type(typ: &TypeMeta, context: &PythonProjectionContext) -> bool { @@ -136,13 +136,28 @@ pub(crate) fn py_wrap_method_arg( py_wrap_arg(name, typ, context) } -fn py_build_method_args_expr( - in_params: &[&crate::meta::ParamMeta], - context: &PythonProjectionContext, -) -> String { - in_params +fn py_build_method_args_expr(method: &MethodMeta, context: &PythonProjectionContext) -> String { + method + .params .iter() - .map(|param| py_wrap_method_arg(&to_snake_case(¶m.name), ¶m.typ, context)) + .enumerate() + .filter(|(_, param)| { + matches!( + param.direction, + crate::meta::ParamDirection::In | crate::meta::ParamDirection::OutFill + ) + }) + .map(|(index, param)| { + let name = to_snake_case(¶m.name); + match method + .collection_inputs + .iter() + .find_map(|(parameter, role)| (*parameter == index).then_some(*role)) + { + Some(role) => py_wrap_collection_input(&name, ¶m.typ, role, context), + None => py_wrap_method_arg(&name, ¶m.typ, context), + } + }) .collect::>() .join(", ") } @@ -281,7 +296,7 @@ fn generate_factory_method_invoke_named( name_override: Option<&str>, ) -> String { let in_params = get_in_params(method); - let py_params = py_param_list(&in_params, context); + let py_params = py_method_param_list(method, context); let return_py_type = py_factory_return_type( &context.class_name(class), @@ -309,7 +324,7 @@ fn generate_factory_method_invoke_named( } out.push_str(&method_pydoc(method, &in_params)); - let args_expr = py_build_method_args_expr(&in_params, context); + let args_expr = py_build_method_args_expr(method, context); let iface_symbol = context.reference_name(&iface.type_identity()); let call_expr = method_call_expr( &context.registration_symbol(iface), @@ -364,7 +379,7 @@ fn generate_static_method_invoke_named( name_override: Option<&str>, ) -> String { let in_params = get_in_params(method); - let py_params = py_param_list(&in_params, context); + let py_params = py_method_param_list(method, context); let py_return = py_method_return_type(method, AnnotationSurface::Runtime, context); @@ -409,7 +424,7 @@ fn generate_static_method_invoke_named( )); } out.push_str(&method_pydoc(method, &in_params)); - let args_expr = py_build_method_args_expr(&in_params, context); + let args_expr = py_build_method_args_expr(method, context); let call_expr = method_call_expr( &context.registration_symbol(iface), method, @@ -835,7 +850,7 @@ pub(crate) fn generate_method_body( iface_var, method.vtable_index, obj_expr, arg )); } else { - let py_params = py_param_list(&in_params, context); + let py_params = py_method_param_list(method, context); let py_return = py_method_return_type(method, AnnotationSurface::Runtime, context); let method_name = name_override .map(|s| s.to_string()) @@ -852,7 +867,7 @@ pub(crate) fn generate_method_body( )); out.push_str(&method_pydoc(method, &in_params)); - let args_expr = py_build_method_args_expr(&in_params, context); + let args_expr = py_build_method_args_expr(method, context); let call_expr = method_call_expr(iface_var, method, obj_expr, &args_expr, context); emit_method_result(&mut out, &call_expr, method, context); } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index 6a9de1fc..c820bef0 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -59,6 +59,8 @@ pub(crate) enum ElementContainer { View, /// `IVector`, `IMap` or their observable forms. Mutable, + /// A map key or `IKeyValuePair.Key`. Null keys are invalid. + MapKey, /// An array returned by a member that does not read collection elements. Array, } @@ -81,19 +83,20 @@ impl From for ElementContainer { match access { ElementAccess::ReadOnly => Self::View, ElementAccess::Mutable => Self::Mutable, + ElementAccess::MapKey => Self::MapKey, } } } -/// The collection element rule: anyone can store null in a mutable collection, -/// so its reference-type elements admit `None`. Views, iterators and arrays -/// are typed like other outputs. Positions that read elements inherit the -/// rule of their owning collection; a view obtained from a mutable collection -/// follows the view rule. +/// The collection element rule: a WinRT collection interface does not carry +/// the provenance needed to prove that a reference-type element is non-null. +/// In particular, a view or iterator obtained from a mutable collection can +/// expose a null slot. All WinRT collection element reads therefore admit +/// `None`; returned arrays remain snapshots and use the ordinary output rule. pub(crate) fn element_admits_none(container: ElementContainer) -> bool { match container { - ElementContainer::Mutable => true, - ElementContainer::View | ElementContainer::Array => false, + ElementContainer::View | ElementContainer::Mutable => true, + ElementContainer::MapKey | ElementContainer::Array => false, } } @@ -210,6 +213,9 @@ fn stub_output_admits_none( Activation, AsyncResult, CallbackParam, CollectionElement, OutParam, Property, Return, }; + if site.container == Some(ElementContainer::MapKey) { + return false; + } // `Object` positions are frequently null, e.g. the arguments of a // `TypedEventHandler`. if matches!(typ, TypeMeta::Object) { @@ -409,7 +415,7 @@ mod tests { ); } let site = OutputSite::for_method(&get_default, OutputPosition::Return); - assert!(!admits( + assert!(admits( &widget(), site.element_in(ElementContainer::View), AnnotationSurface::Stub @@ -417,32 +423,31 @@ mod tests { } #[test] - fn collection_elements_follow_the_mutability_of_their_collection() { + fn reference_collection_elements_are_nullable_regardless_of_provenance() { let stub = AnnotationSurface::Stub; - assert!(admits( - &widget(), - OutputSite::element_of(ElementContainer::Mutable), - stub - )); - for container in [ElementContainer::View, ElementContainer::Array] { + for container in [ElementContainer::View, ElementContainer::Mutable] { let site = OutputSite::element_of(container); - assert!(!admits(&widget(), site, stub), "{container:?}"); + assert!(admits(&widget(), site, stub), "{container:?}"); assert!(admits(&TypeMeta::Object, site, stub)); assert!(admits(&nullable_u32(), site, stub)); assert!(!admits(&TypeMeta::String, site, stub)); } - for (access, expected) in [ - (ElementAccess::Mutable, true), - (ElementAccess::ReadOnly, false), - ] { + let array = OutputSite::element_of(ElementContainer::Array); + assert!(!admits(&widget(), array, stub)); + assert!(admits(&TypeMeta::Object, array, stub)); + assert!(admits(&nullable_u32(), array, stub)); + let key = OutputSite::element_of(ElementContainer::MapKey); + assert!(!admits(&widget(), key, stub)); + assert!(!admits(&TypeMeta::Object, key, stub)); + + for access in [ElementAccess::Mutable, ElementAccess::ReadOnly] { let get_at = MethodMeta { element_access: Some(access), ..method("GetAt") }; for position in [OutputPosition::Return, OutputPosition::Property] { - assert_eq!( + assert!( admits(&widget(), OutputSite::for_method(&get_at, position), stub), - expected, "{access:?} at {position:?}" ); } @@ -460,5 +465,14 @@ mod tests { ElementContainer::of(CollectionKind::KeyValuePair), ElementContainer::View ); + let key = MethodMeta { + element_access: Some(ElementAccess::MapKey), + ..method("get_Key") + }; + assert!(!admits( + &widget(), + OutputSite::for_method(&key, OutputPosition::Property), + stub + )); } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 9bbb5ba6..9d0cbf4d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -3,7 +3,7 @@ //! Python method signatures, argument wrapping, and return conversion. -use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; +use crate::meta::{CollectionInputRole, InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::{TypeIdentity, TypeIdentityKind, TypeMeta}; use super::naming::{PythonProjectionContext, PythonSymbol}; @@ -477,6 +477,55 @@ pub(crate) fn py_wrap_native_value( } } +/// Wrap one Python collection item through the shared runtime validator. +/// +/// Reference elements and map values accept `None` and encode a real null +/// `DynWinRTValue`. Map keys and value-type positions reject `None` with a +/// stable `TypeError` before type-specific conversion (and before `.cast()`). +pub(crate) fn py_wrap_collection_item( + name: &str, + typ: &TypeMeta, + role: CollectionInputRole, + context: &PythonProjectionContext, +) -> String { + let allow_none = role != CollectionInputRole::Key && super::nullability::may_project_none(typ); + let label = match role { + CollectionInputRole::Element => "collection element", + CollectionInputRole::Key => "map key", + CollectionInputRole::Value => "map value", + }; + format!( + "_dynwinrt_collection_item({name}, lambda item: {}, {}, '{label}')", + py_wrap_arg("item", typ, context), + if allow_none { "True" } else { "False" } + ) +} + +/// Wrap a parameter that is itself a collection element/value contract. The +/// `ReplaceAll` array is the only array-shaped case; its individual elements +/// use the same validator as append/insert/set operations. +pub(crate) fn py_wrap_collection_input( + name: &str, + typ: &TypeMeta, + role: CollectionInputRole, + context: &PythonProjectionContext, +) -> String { + if let TypeMeta::Array(inner) = typ { + return format!( + "_dynwinrt_array({}, lambda item: {}, {}, {})", + name, + py_wrap_collection_item("item", inner, role, context), + py_dynwinrt_type(inner), + if matches!(inner.as_ref(), TypeMeta::U8) { + "True" + } else { + "False" + } + ); + } + py_wrap_collection_item(name, typ, role, context) +} + fn py_wrap_collection( name: &str, typ: &TypeMeta, @@ -503,8 +552,8 @@ fn py_wrap_collection( return Some(format!( "_dynwinrt_map({}, lambda item: {}, lambda item: {}, {}, {})", name, - py_wrap_native_value("item", key, context), - py_wrap_native_value("item", value, context), + py_wrap_collection_item("item", key, CollectionInputRole::Key, context), + py_wrap_collection_item("item", value, CollectionInputRole::Value, context), py_dynwinrt_type(key), py_dynwinrt_type(value) )); @@ -517,7 +566,7 @@ fn py_wrap_collection( return Some(format!( "_dynwinrt_vector({}, lambda item: {}, {})", name, - py_wrap_native_value("item", element, context), + py_wrap_collection_item("item", element, CollectionInputRole::Element, context), py_dynwinrt_type(element) )); } @@ -950,6 +999,37 @@ mod tests { ); } + #[test] + fn collection_items_validate_none_before_type_specific_conversion() { + let context = PythonProjectionContext::default(); + let geometry = geometry_type(); + let nullable = + py_wrap_collection_item("value", &geometry, CollectionInputRole::Element, &context); + assert!(nullable.contains("_dynwinrt_collection_item(value,")); + assert!(nullable.contains(".cast(IID_ARG_Microsoft_UI_Xaml_Media_Geometry)")); + assert!(nullable.contains("True, 'collection element'")); + + let key = py_wrap_collection_item("key", &geometry, CollectionInputRole::Key, &context); + assert!(key.contains("False, 'map key'")); + let scalar = py_wrap_collection_item( + "value", + &TypeMeta::I32, + CollectionInputRole::Value, + &context, + ); + assert!(scalar.contains("DynWinRTValue.from_i32(item)")); + assert!(scalar.contains("False, 'map value'")); + + let array = py_wrap_collection_input( + "items", + &TypeMeta::Array(Box::new(geometry)), + CollectionInputRole::Element, + &context, + ); + assert!(array.starts_with("_dynwinrt_array(items, lambda item:")); + assert!(array.contains("True, 'collection element'")); + } + #[test] fn python_numeric_overload_integer_guards_use_exact_ranges() { let context = PythonProjectionContext::default(); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 37ba7e3f..8a57fa5e 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -13,7 +13,7 @@ use super::nullability::{AnnotationSurface, ElementContainer}; use super::structs::{py_struct_field_read_type, py_struct_field_type}; use super::type_helpers::{ method_pydoc_with_indent, py_collection_item_type, py_delegate_callable_type, - py_factory_return_type, py_method_return_type, py_param_list, py_param_type_safe, + py_factory_return_type, py_method_param_list, py_method_return_type, py_param_type_safe, py_property_type, }; use crate::codegen::winrt::shared::imports::ireference_inner_type; @@ -318,7 +318,7 @@ pub(super) fn emit_method_stub_named( ); } } else { - let py_params = py_param_list(&in_params, context); + let py_params = py_method_param_list(method, context); let py_return = py_method_return_type(method, AnnotationSurface::Stub, context); let method_name = name_override .map(str::to_string) @@ -377,7 +377,7 @@ pub(super) fn emit_static_method_stub_named( name_override: Option<&str>, ) -> String { let in_params = get_in_params(method); - let py_params = py_param_list(&in_params, context); + let py_params = py_method_param_list(method, context); let py_return = if is_factory { py_factory_return_type(class_name, method, AnnotationSurface::Stub, context) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index 3dcd6d24..caecfdfe 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -513,7 +513,8 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf // IVector / IMap create() if let Some(ref piid) = iface.generic_piid { if piid == "5917eb53-50b4-4a0d-b309-65862b3f1dbc" && iface.generic_args.len() == 1 { - let element = super::type_helpers::py_param_type_safe(&iface.generic_args[0], context); + let element = + super::type_helpers::py_collection_input_type(&iface.generic_args[0], context); let vector_name = context.projected_name( observable_vector .as_ref() @@ -530,7 +531,8 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf vector_name )); } else if piid == "913337e9-11a1-4345-a3a2-4e7f956e222d" && iface.generic_args.len() == 1 { - let element = super::type_helpers::py_param_type_safe(&iface.generic_args[0], context); + let element = + super::type_helpers::py_collection_input_type(&iface.generic_args[0], context); out.push('\n'); out.push_str(" @staticmethod\n"); out.push_str(&format!( @@ -539,7 +541,8 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf )); } else if piid == "3c2925fe-8519-45c1-aa79-197b6718c1c1" && iface.generic_args.len() == 2 { let key = super::type_helpers::py_param_type_safe(&iface.generic_args[0], context); - let value = super::type_helpers::py_param_type_safe(&iface.generic_args[1], context); + let value = + super::type_helpers::py_collection_input_type(&iface.generic_args[1], context); out.push('\n'); out.push_str(" @staticmethod\n"); out.push_str(&format!( @@ -1279,12 +1282,20 @@ fn collection_protocol_stubs( .generic_args .first() .map(|typ| { - super::type_helpers::py_collection_item_type( - typ, - container, - AnnotationSurface::Stub, - context, - ) + if matches!( + kind, + super::collections::CollectionKind::Mapping + | super::collections::CollectionKind::MutableMapping + ) { + super::type_helpers::py_collection_key_type(typ, AnnotationSurface::Stub, context) + } else { + super::type_helpers::py_collection_item_type( + typ, + container, + AnnotationSurface::Stub, + context, + ) + } }) .unwrap_or_else(|| "object".to_string()); let item_input = iface diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 46c9ba5c..e35171ff 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -7,7 +7,7 @@ use crate::codegen::winrt::shared::docs::{DocText, find_param_doc}; use crate::codegen::winrt::shared::imports::{ fill_array_uses_retval_count, ireference_inner_type, method_abi_output_count, }; -use crate::meta::MethodMeta; +use crate::meta::{CollectionInputRole, MethodMeta}; use crate::types::TypeMeta; use super::collections::{CollectionKind, abc_name, is_mapping_input, type_kind}; @@ -155,6 +155,29 @@ pub(super) fn py_collection_input_type( } } +/// Input annotation for an element/value position of a mutable collection. +/// `ReplaceAll` is array-shaped, so nullable reference elements belong inside +/// its `Sequence[...]` rather than on the array parameter itself. +pub(super) fn py_collection_contract_input_type( + typ: &TypeMeta, + context: &PythonProjectionContext, +) -> String { + if let TypeMeta::Array(inner) = typ { + let element = py_native_param_element_type(inner, context); + let element = if may_project_none(inner) { + py_optional_type(element) + } else { + element + }; + return if matches!(inner.as_ref(), TypeMeta::U8) { + format!("DynWinRTArray | bytes | bytearray | Sequence[{element}]") + } else { + format!("DynWinRTArray | Sequence[{element}]") + }; + } + py_collection_input_type(typ, context) +} + // ====================================================================== // Output annotations // @@ -362,7 +385,19 @@ fn spell_collection( let element = site.element_in(ElementContainer::of(kind)); let elements = args .iter() - .map(|arg| render_output(arg, Spelling::Element, element, surface, context)) + .enumerate() + .map(|(index, arg)| { + let argument_site = if index == 0 + && matches!( + kind, + CollectionKind::Mapping | CollectionKind::MutableMapping + ) { + site.element_in(ElementContainer::MapKey) + } else { + element + }; + render_output(arg, Spelling::Element, argument_site, surface, context) + }) .collect::>(); Some(format!("{abc}[{}]", elements.join(", "))) } @@ -417,6 +452,21 @@ pub(super) fn py_collection_item_type( py_output_annotation(typ, OutputSite::element_of(container), surface, context) } +/// Key type of a projected map. WinRT maps reject null keys even when their +/// key ABI type is a reference. +pub(super) fn py_collection_key_type( + typ: &TypeMeta, + surface: AnnotationSurface, + context: &PythonProjectionContext, +) -> String { + py_output_annotation( + typ, + OutputSite::element_of(ElementContainer::MapKey), + surface, + context, + ) +} + /// `Sequence[T]` / `Mapping[K, V]` base of a projected collection of `kind`. pub(super) fn py_collection_base_type( kind: CollectionKind, @@ -428,7 +478,11 @@ pub(super) fn py_collection_base_type( let item = |typ| py_collection_item_type(typ, ElementContainer::of(kind), surface, context); match args { [element] => Some(format!("{abc}[{}]", item(element))), - [key, value] => Some(format!("{abc}[{}, {}]", item(key), item(value))), + [key, value] => Some(format!( + "{abc}[{}, {}]", + py_collection_key_type(key, surface, context), + item(value) + )), _ => None, } } @@ -679,6 +733,44 @@ pub(super) fn py_param_list( .join(", ") } +/// Method parameters with collection input roles applied. Only collection +/// elements and map values gain `None`; map keys and general WinRT inputs keep +/// their existing annotations. +pub(super) fn py_method_param_list( + method: &MethodMeta, + context: &PythonProjectionContext, +) -> String { + method + .params + .iter() + .enumerate() + .filter(|(_, param)| { + matches!( + param.direction, + crate::meta::ParamDirection::In | crate::meta::ParamDirection::OutFill + ) + }) + .map(|(index, param)| { + let role = method + .collection_inputs + .iter() + .find_map(|(parameter, role)| (*parameter == index).then_some(*role)); + let param_type = match role { + Some(CollectionInputRole::Element | CollectionInputRole::Value) => { + py_collection_contract_input_type(¶m.typ, context) + } + Some(CollectionInputRole::Key) => py_param_type_safe(¶m.typ, context), + None if context.is_delegate_type(¶m.typ) => { + py_delegate_param_type(¶m.typ, context) + } + None => py_param_type_safe(¶m.typ, context), + }; + format!("{}: {}", to_snake_case(¶m.name), param_type) + }) + .collect::>() + .join(", ") +} + /// Produce a typed Python annotation for a delegate parameter, with /// `TypedEventHandler` / `EventHandler` unwrapped. Bespoke non-parametric /// delegates fall back to `Callable[..., object]`. @@ -1008,7 +1100,7 @@ mod tests { ); assert_eq!( returned(&async_of(&widgets), stub, &context), - "WinRTCoroutine[Sequence[Widget]]" + "WinRTCoroutine[Sequence[Widget | None]]" ); assert_eq!( returned(&TypeMeta::Object, stub, &context), @@ -1026,7 +1118,7 @@ mod tests { ); assert_eq!( py_collection_item_type(&widget, ElementContainer::View, stub, &context), - "Widget" + "Widget | None" ); assert_eq!( py_collection_item_type(&widget, ElementContainer::Mutable, stub, &context), @@ -1039,7 +1131,7 @@ mod tests { stub, &context ), - Some("Sequence[Widget]".to_string()) + Some("Sequence[Widget | None]".to_string()) ); assert_eq!( py_collection_base_type( @@ -1112,7 +1204,7 @@ mod tests { ), ( &try_get_items, - "WinRTCoroutine[Sequence[Widget] | None]", + "WinRTCoroutine[Sequence[Widget | None] | None]", "WinRTCoroutine[Sequence[Widget | None] | None]", ), ( diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 729b504c..06413741 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -64,6 +64,10 @@ pub struct MethodMeta { /// Set on the members that read elements of the /// `Windows.Foundation.Collections` interface declaring them. pub element_access: Option, + /// Input parameters that represent collection elements, map keys, or map + /// values. Python uses this to validate and wrap `None` at the collection + /// boundary without changing general WinRT parameter conversion. + pub collection_inputs: Vec<(usize, CollectionInputRole)>, } /// How a member reads the elements of the `Windows.Foundation.Collections` @@ -75,6 +79,16 @@ pub enum ElementAccess { ReadOnly, /// `IVector` and `IMap`, in which anyone can store null. Mutable, + /// The key of an `IKeyValuePair`. Null keys are not valid map entries. + MapKey, +} + +/// The role of an input parameter on a collection interface. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CollectionInputRole { + Element, + Key, + Value, } fn collection_element_access( @@ -85,11 +99,11 @@ fn collection_element_access( if namespace != WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE { return None; } - let access = match definition { - "IIterator`1" | "IVectorView`1" | "IMapView`2" | "IKeyValuePair`2" => { - ElementAccess::ReadOnly - } - "IVector`1" | "IMap`2" => ElementAccess::Mutable, + let access = match (definition, member) { + ("IKeyValuePair`2", "get_Key") => return Some(ElementAccess::MapKey), + ("IKeyValuePair`2", "get_Value") => return Some(ElementAccess::ReadOnly), + ("IIterator`1" | "IVectorView`1" | "IMapView`2", _) => ElementAccess::ReadOnly, + ("IVector`1" | "IMap`2", _) => ElementAccess::Mutable, _ => return None, }; matches!( @@ -99,6 +113,25 @@ fn collection_element_access( .then_some(access) } +fn collection_input_roles( + namespace: &str, + definition: &str, + member: &str, +) -> Vec<(usize, CollectionInputRole)> { + if namespace != WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE { + return Vec::new(); + } + use CollectionInputRole::{Element, Key, Value}; + match (definition, member) { + ("IVector`1" | "IVectorView`1", "IndexOf") => vec![(0, Element)], + ("IVector`1", "SetAt" | "InsertAt") => vec![(1, Element)], + ("IVector`1", "Append" | "ReplaceAll") => vec![(0, Element)], + ("IMap`2" | "IMapView`2", "Lookup" | "HasKey") | ("IMap`2", "Remove") => vec![(0, Key)], + ("IMap`2", "Insert") => vec![(0, Key), (1, Value)], + _ => Vec::new(), + } +} + /// A WinRT interface with its methods. #[derive(Debug, Clone, Default)] pub struct InterfaceMeta { @@ -1831,6 +1864,7 @@ fn parse_interface_methods( }; let element_access = collection_element_access(namespace, def.name(), &raw_name); + let collection_inputs = collection_input_roles(namespace, def.name(), &raw_name); let mut method_meta = MethodMeta { name: method_name.clone(), vtable_index, @@ -1848,6 +1882,7 @@ fn parse_interface_methods( returns_doc: None, documented_null_result: false, element_access, + collection_inputs, }; method_meta.documented_null_result = crate::documented_nulls::documents_null_result(&documentation_owner, &method_meta); diff --git a/tools/dynwinrt-codegen/tests/observable_vector_test.rs b/tools/dynwinrt-codegen/tests/observable_vector_test.rs index 2d2b788a..8beeb8c1 100644 --- a/tools/dynwinrt-codegen/tests/observable_vector_test.rs +++ b/tools/dynwinrt-codegen/tests/observable_vector_test.rs @@ -102,11 +102,11 @@ fn observable_vector_projects_python_mutable_sequence_and_typed_events() { py.contains("_dynwinrt_symbol('i_vector_object', 'IVector_Object')._set_native(self, obj)") ); assert!(py.contains("self._observable_obj = obj.cast(IID_IObservableVector_Object)")); - let create_signature = "def create(items: Iterable['DynWinRTValue | _DynWinRTObject']) -> 'IObservableVector_Object':"; + let create_signature = "def create(items: Iterable[DynWinRTValue | _DynWinRTObject | None]) -> 'IObservableVector_Object':"; assert!(py.contains(create_signature), "{py}"); assert!( py.contains( - "_dynwinrt_new_vector(items, lambda item: getattr(item, '_obj', item), DynWinRTType.object())" + "_dynwinrt_new_vector(items, lambda item: _dynwinrt_collection_item(item, lambda item: getattr(item, '_obj', item), True, 'collection element'), DynWinRTType.object())" ), "{py}" ); diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 34c7b778..444efc56 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -864,7 +864,11 @@ from sdk.windows.storage.streams import DataReader, DataWriter, InMemoryRandomAc r#"{imports} def uri_demo() -> str: uri = Uri("https://example.com/a/b?x=1&y=two") - query = {{entry.name: entry.value for entry in uri.query_parsed}} + query = {{ + entry.name: entry.value + for entry in uri.query_parsed + if entry is not None + }} return uri.combine_uri("c/d").absolute_uri + str(query) def json_demo() -> list[str]: @@ -878,7 +882,8 @@ def sensor_demo() -> float | None: accelerometer = Accelerometer.get_default() if accelerometer is None: return None - return accelerometer.get_current_reading().acceleration_x + reading = accelerometer.get_current_reading() + return None if reading is None else reading.acceleration_x def calendar_demo(calendar: Calendar) -> str: languages: Sequence[str] = calendar.languages @@ -908,7 +913,7 @@ async def storage_demo(path: str) -> list[str]: assert_type(folder.create_file_async("a.txt"), WinRTCoroutine[StorageFile]) assert_type(folder.try_get_item_async("notes.txt"), WinRTCoroutine[IStorageItem | None]) assert_type(folder.get_parent_async(), WinRTCoroutine[StorageFolder | None]) - return [item.name for item in await folder.get_files_async()] + return [item.name for item in await folder.get_files_async() if item is not None] "# ), &[], @@ -930,7 +935,8 @@ fn mutable_collection_mutators_accept_none() { .arg(winmd) .args([ "--class-name", - "Windows.Storage.StorageLibrary,Windows.Data.Json.JsonObject", + "Windows.Storage.StorageLibrary,Windows.Data.Json.JsonObject,\ + Windows.Foundation.Collections.StringMap", "--lang", "py", "--output", @@ -951,6 +957,7 @@ from sdk.windows.foundation.collections import IObservableVector_StorageFolder from sdk.windows.storage import StorageFolder def vector(folders: IObservableVector_StorageFolder) -> None: + IObservableVector_StorageFolder.create([None]) folders.append(None) folders.extend([None]) folders.insert(0, None) @@ -965,6 +972,84 @@ def mapping(values: JsonObject) -> None: "#, &[], ); + + if has_implementation_runtime() { + let script = r#"from dynwinrt import DynWinRTType, DynWinRTValue, RoApartment, projected_lifetime_scope +from sdk.windows.foundation.collections import ( + IIterable_StorageFolder, + IMap_String_String, + IObservableVector_StorageFolder, + IVector_String, + IVector_StorageFolder, +) +from sdk.windows__data__json__json_object import IID_IJsonValue, IMap_String_IJsonValue + +with RoApartment(1), projected_lifetime_scope(): + for vector_type in (IVector_StorageFolder, IObservableVector_StorageFolder): + vector = vector_type.create([None]) + assert vector[0] is None + vector.append(None) + vector.insert(0, None) + vector[1] = None + vector[1:2] = [None, None] + vector.extend([None]) + assert list(vector) == [None] * len(vector) + + base = vector.as_vector() if hasattr(vector, "as_vector") else vector + view = base.get_view() + assert view is not None + assert view[0] is None + assert list(view) == [None] * len(view) + iterator = base.as_interface(IIterable_StorageFolder).first() + assert iterator is not None + assert next(iterator) is None + + native = DynWinRTValue.create_map( + [], [], DynWinRTType.hstring(), DynWinRTType.interface(IID_IJsonValue) + ) + mapping = IMap_String_IJsonValue.from_value(native) + mapping.update({"update": None}) + assert mapping.setdefault("default", None) is None + mapping["index"] = None + assert mapping["update"] is None + assert mapping["default"] is None + assert mapping["index"] is None + assert list(mapping.values()) == [None, None, None] + + strings = IMap_String_String.create({}) + invalid = ( + ("map key cannot be None", lambda: mapping.__setitem__(None, None)), + ("map value cannot be None", lambda: strings.__setitem__("key", None)), + ("map key cannot be None", lambda: IMap_String_String.create({None: "value"})), + ("map value cannot be None", lambda: IMap_String_String.create({"key": None})), + ("collection element cannot be None", lambda: IVector_String.create([None])), + ( + "collection element cannot be None", + lambda: IVector_String.create([]).append(None), + ), + ) + for expected, operation in invalid: + try: + operation() + except TypeError as error: + assert str(error) == expected + else: + raise AssertionError(f"{expected!r} was not raised") +print("nullable-collection-native-ok", flush=True) +"#; + fs::write(fixture.0.join("nullable_collections.py"), script).unwrap(); + let output = Command::new(python()) + .args(["-B", "nullable_collections.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + assert!( + String::from_utf8_lossy(&output.stdout).contains("nullable-collection-native-ok"), + "{}", + diagnostics(&output) + ); + } } #[test] diff --git a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs index a0dba5e7..ea0d2e8c 100644 --- a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs +++ b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs @@ -97,7 +97,7 @@ fn stub_outputs_follow_the_nullability_policy() { ); assert_contains( &folder, - "def get_files_async(self) -> WinRTCoroutine[Sequence[StorageFile]]: ...", + "def get_files_async(self) -> WinRTCoroutine[Sequence[StorageFile | None]]: ...", ); assert_contains( &folder, @@ -200,7 +200,96 @@ fn stub_outputs_follow_the_nullability_policy() { } #[test] -fn collection_elements_follow_the_mutability_of_their_collection() { +fn sensor_current_reading_docs_control_nullability_without_family_false_positives() { + let Some(generated) = Generated::new( + "sensorreadings", + "Windows.Devices.Sensors.Accelerometer,Windows.Devices.Sensors.Compass,\ + Windows.Devices.Sensors.Gyrometer,Windows.Devices.Sensors.Inclinometer,\ + Windows.Devices.Sensors.LightSensor,Windows.Devices.Sensors.OrientationSensor,\ + Windows.Devices.Sensors.Custom.CustomSensor,Windows.Devices.Sensors.Altimeter,\ + Windows.Devices.Sensors.Barometer,Windows.Devices.Sensors.HumanPresenceSensor,\ + Windows.Devices.Sensors.Magnetometer,Windows.Devices.Sensors.ProximitySensor,\ + Windows.Devices.Sensors.ActivitySensor,Windows.Devices.Sensors.HingeAngleSensor,\ + Windows.Devices.Sensors.Pedometer", + ) else { + return; + }; + + for (module, signature) in [ + ( + "windows__devices__sensors__accelerometer.pyi", + "def get_current_reading(self) -> AccelerometerReading | None: ...", + ), + ( + "windows__devices__sensors__compass.pyi", + "def get_current_reading(self) -> CompassReading | None: ...", + ), + ( + "windows__devices__sensors__custom__custom_sensor.pyi", + "def get_current_reading(self) -> CustomSensorReading | None: ...", + ), + ( + "windows__devices__sensors__gyrometer.pyi", + "def get_current_reading(self) -> GyrometerReading | None: ...", + ), + ( + "windows__devices__sensors__inclinometer.pyi", + "def get_current_reading(self) -> InclinometerReading | None: ...", + ), + ( + "windows__devices__sensors__light_sensor.pyi", + "def get_current_reading(self) -> LightSensorReading | None: ...", + ), + ( + "windows__devices__sensors__orientation_sensor.pyi", + "def get_current_reading(self) -> OrientationSensorReading | None: ...", + ), + ] { + assert_contains(&generated.module(module), signature); + } + + // The other GetCurrentReading variants at the pinned docs revision do + // not contain the required-null-check wording and remain non-null. + for (module, signature) in [ + ( + "windows__devices__sensors__activity_sensor.pyi", + "def get_current_reading_async(self) -> WinRTCoroutine[ActivitySensorReading]: ...", + ), + ( + "windows__devices__sensors__altimeter.pyi", + "def get_current_reading(self) -> AltimeterReading: ...", + ), + ( + "windows__devices__sensors__barometer.pyi", + "def get_current_reading(self) -> BarometerReading: ...", + ), + ( + "windows__devices__sensors__hinge_angle_sensor.pyi", + "def get_current_reading_async(self) -> WinRTCoroutine[HingeAngleReading]: ...", + ), + ( + "windows__devices__sensors__human_presence_sensor.pyi", + "def get_current_reading(self) -> HumanPresenceSensorReading: ...", + ), + ( + "windows__devices__sensors__magnetometer.pyi", + "def get_current_reading(self) -> MagnetometerReading: ...", + ), + ( + "windows__devices__sensors__proximity_sensor.pyi", + "def get_current_reading(self) -> ProximitySensorReading: ...", + ), + ] { + assert_contains(&generated.module(module), signature); + } + assert_contains( + &generated.module("windows__devices__sensors__pedometer.pyi"), + "def get_current_readings(self) -> Mapping['PedometerStepKind', PedometerReading | None]: ...", + ); +} + +#[test] +fn reference_collection_elements_are_nullable_regardless_of_provenance() { let Some(generated) = Generated::new( "elements", "Windows.Storage.StorageFolder,Windows.Data.Json.JsonObject,\ @@ -218,23 +307,28 @@ fn collection_elements_follow_the_mutability_of_their_collection() { let observable = generated .module("windows__foundation__collections__i_observable_vector_media_playback_item.pyi"); - // Views keep non-null elements, including their item positions. - assert_contains(&files, "def get_at(self, index: int) -> StorageFile: ..."); + // Collection interfaces carry no provenance. A view or iterator obtained + // from a mutable collection can expose a null slot, so view item + // positions conservatively keep None too. + assert_contains( + &files, + "def get_at(self, index: int) -> StorageFile | None: ...", + ); assert_contains( &files, - "def __getitem__(self, index: int) -> StorageFile: ...", + "def __getitem__(self, index: int) -> StorageFile | None: ...", ); assert_contains( &resources, - "class ResourceMap(_ResourceMapIdentity, Mapping[str, NamedResource], _DynWinRTRuntimeClass):", + "class ResourceMap(_ResourceMapIdentity, Mapping[str, NamedResource | None], _DynWinRTRuntimeClass):", ); assert_contains( &resources, - "def lookup(self, key: str) -> NamedResource: ...", + "def lookup(self, key: str) -> NamedResource | None: ...", ); - // Anyone can store null in a mutable collection, so its elements, item - // positions and element-reading members keep None. + // Mutable collection elements, item positions and element-reading + // members keep None for the same reason. assert_contains( &array, "class JsonArray(_JsonArrayIdentity, MutableSequence[IJsonValue | None], _DynWinRTRuntimeClass):", diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py index 2937caea..1dfba475 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py @@ -14,6 +14,7 @@ _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) +from ._runtime import _dynwinrt_collection_item from dynwinrt.dynwinrt import _WinRTIteratorMixin if TYPE_CHECKING: diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py index f9baae09..aa611932 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py @@ -14,6 +14,7 @@ _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) +from ._runtime import _dynwinrt_collection_item from dynwinrt.dynwinrt import _WinRTIterableMixin, _WinRTSequenceMixin if TYPE_CHECKING: @@ -98,8 +99,8 @@ def size(self) -> int: def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: return (lambda value: None if value.is_null() else _dynwinrt_symbol('windows__foundation__i_www_form_url_decoder_entry', 'IWwwFormUrlDecoderEntry')(value))(_IVectorView_IWwwFormUrlDecoderEntry.method(6).invoke(self._collection_obj, [DynWinRTValue.from_u32(index)])) - def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: - _results = _IVectorView_IWwwFormUrlDecoderEntry.method(8).invoke_all(self._collection_obj, [getattr(value, '_obj', value)]) + def index_of(self, value: IWwwFormUrlDecoderEntry | None) -> tuple[int, bool]: + _results = _IVectorView_IWwwFormUrlDecoderEntry.method(8).invoke_all(self._collection_obj, [_dynwinrt_collection_item(value, lambda item: getattr(item, '_obj', item), True, 'collection element')]) return (_results[0].to_u32(), _results[1].to_bool()) def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: @@ -150,8 +151,8 @@ def size(self) -> int: def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: return (lambda value: None if value.is_null() else _dynwinrt_symbol('windows__foundation__i_www_form_url_decoder_entry', 'IWwwFormUrlDecoderEntry')(value))(_IVectorView_IWwwFormUrlDecoderEntry.method(6).invoke(self._obj, [DynWinRTValue.from_u32(index)])) - def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: - _results = _IVectorView_IWwwFormUrlDecoderEntry.method(8).invoke_all(self._obj, [getattr(value, '_obj', value)]) + def index_of(self, value: IWwwFormUrlDecoderEntry | None) -> tuple[int, bool]: + _results = _IVectorView_IWwwFormUrlDecoderEntry.method(8).invoke_all(self._obj, [_dynwinrt_collection_item(value, lambda item: getattr(item, '_obj', item), True, 'collection element')]) return (_results[0].to_u32(), _results[1].to_bool()) def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi index 4194cea1..ae2e8fc3 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/i_iterator_i_www_form_url_decoder_entry.pyi @@ -19,25 +19,25 @@ IID_IIterator_IWwwFormUrlDecoderEntry: WinGUID class _IIterator_IWwwFormUrlDecoderEntryIdentity(Protocol): def _dynwinrt_iid_g2bb0b33cef11eb455ace3197fce2b56f216f6802e23b208c88ebad8f950f8628(self) -> None: ... -class IIterator_IWwwFormUrlDecoderEntry(_IIterator_IWwwFormUrlDecoderEntryIdentity, Iterator[IWwwFormUrlDecoderEntry]): +class IIterator_IWwwFormUrlDecoderEntry(_IIterator_IWwwFormUrlDecoderEntryIdentity, Iterator[IWwwFormUrlDecoderEntry | None]): @builtins.property def _obj(self) -> DynWinRTValue: ... # Windows.Foundation.Collections.IIterator_IWwwFormUrlDecoderEntry cannot be implemented: generic interface implementations are not supported def __init__(self, obj: DynWinRTValue) -> None: ... - def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... - def __next__(self) -> IWwwFormUrlDecoderEntry: ... + def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... + def __next__(self) -> IWwwFormUrlDecoderEntry | None: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... @builtins.property - def current(self) -> IWwwFormUrlDecoderEntry: ... + def current(self) -> IWwwFormUrlDecoderEntry | None: ... @builtins.property def has_current(self) -> bool: ... def move_next(self) -> bool: ... - def get_many(self, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... + def get_many(self, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi index c223db0a..cab1e75a 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_pyi/www_form_url_decoder.pyi @@ -34,48 +34,48 @@ class WwwFormUrlDecoderLike(_WwwFormUrlDecoderIdentity, Protocol): def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... def get_first_value_by_name(self, name: str) -> str: ... @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... - def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... + def index_of(self, value: IWwwFormUrlDecoderEntry | None) -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... -class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderEntry], _DynWinRTRuntimeClass): +class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderEntry | None], _DynWinRTRuntimeClass): def __init__(self, query: str) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... def get_first_value_by_name(self, name: str) -> str: ... @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... - def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... + def index_of(self, value: IWwwFormUrlDecoderEntry | None) -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... @@ -83,16 +83,16 @@ class WwwFormUrlDecoder(_WwwFormUrlDecoderIdentity, Sequence[IWwwFormUrlDecoderE def create_www_form_url_decoder(query: str) -> 'WwwFormUrlDecoder': ... -class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry]): +class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry | None]): def __init__(self, obj: DynWinRTValue) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... def __len__(self) -> int: ... @overload - def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def __getitem__(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... @overload - def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry]: ... + def __getitem__(self, index: slice) -> list[IWwwFormUrlDecoderEntry | None]: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... @@ -101,22 +101,22 @@ class IVectorView_IWwwFormUrlDecoderEntry(Sequence[IWwwFormUrlDecoderEntry]): @builtins.property def size(self) -> int: ... - def get_at(self, index: int) -> IWwwFormUrlDecoderEntry: ... + def get_at(self, index: int) -> IWwwFormUrlDecoderEntry | None: ... - def index_of(self, value: 'IWwwFormUrlDecoderEntry') -> tuple[int, bool]: ... + def index_of(self, value: IWwwFormUrlDecoderEntry | None) -> tuple[int, bool]: ... - def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry]: ... + def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUrlDecoderEntry']) -> list[IWwwFormUrlDecoderEntry | None]: ... -class IIterable_IWwwFormUrlDecoderEntry(Iterable[IWwwFormUrlDecoderEntry]): +class IIterable_IWwwFormUrlDecoderEntry(Iterable[IWwwFormUrlDecoderEntry | None]): def __init__(self, obj: DynWinRTValue) -> None: ... @builtins.property def _obj(self) -> DynWinRTValue: ... - def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... + def __iter__(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... @classmethod def from_value(cls, obj: DynWinRTValue) -> Self: ... def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ... - def first(self) -> Iterator[IWwwFormUrlDecoderEntry]: ... + def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None]: ... From ae224a24dbb3c0dbcf6e3a7de37ca5a7cb3a23b6 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 10:49:30 +0800 Subject: [PATCH 07/12] Emit nested IReference helpers and run native collection CI Collection item wrapping can recurse into IReference 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> --- .github/workflows/build.yml | 7 + .../src/codegen/winrt/python/generator/mod.rs | 138 +++++++++++++++++- .../tests/python_consumer_typing_test.rs | 97 +++++++++++- 3 files changed, 239 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ce079b2..a4f9fd61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -549,6 +549,13 @@ jobs: $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' cargo test -p dynwinrt-codegen --test implementation_naming_test + - name: Test generated nullable collection writes + shell: pwsh + run: | + $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path + $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' + cargo test -p dynwinrt-codegen --test python_consumer_typing_test ` + mutable_collection_mutators_accept_none -- --exact - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index 47ee1993..c631efca 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -189,9 +189,30 @@ def _dynwinrt_unbox_reference(value): "; fn has_ireference_input<'a>(methods: impl IntoIterator) -> bool { + fn contains(typ: &TypeMeta) -> bool { + if ireference_inner_type(typ).is_some() { + return true; + } + match typ { + TypeMeta::Array(inner) + | TypeMeta::AsyncOperation(inner) + | TypeMeta::AsyncActionWithProgress(inner) => contains(inner), + TypeMeta::AsyncOperationWithProgress(result, progress) => { + contains(result) || contains(progress) + } + TypeMeta::Parameterized { args, .. } => args.iter().any(contains), + // Struct inputs use their generated pack helpers; those modules + // emit IREFERENCE_HELPER through has_ireference_struct_field. + _ => false, + } + } + methods.into_iter().any(|method| { method.params.iter().any(|param| { - param.direction == ParamDirection::In && ireference_inner_type(¶m.typ).is_some() + matches!( + param.direction, + ParamDirection::In | ParamDirection::OutFill + ) && contains(¶m.typ) }) }) } @@ -233,7 +254,47 @@ pub use types::{generate_enum, generate_interface}; #[cfg(test)] mod tests { - use super::generate_runtime_support_module; + use super::*; + use crate::meta::{InterfaceMeta, ParamDirection, ParamMeta}; + + fn reference() -> TypeMeta { + TypeMeta::Parameterized { + namespace: "Windows.Foundation".into(), + name: "IReference`1".into(), + piid: "61c17706-2d65-11e0-9ae8-d48564015472".into(), + args: vec![TypeMeta::I32], + } + } + + fn parameterized(name: &str, piid: &str, args: Vec) -> TypeMeta { + TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: name.into(), + piid: piid.into(), + args, + } + } + + fn method(typ: TypeMeta) -> MethodMeta { + MethodMeta { + params: vec![ParamMeta { + name: "value".into(), + typ, + direction: ParamDirection::In, + }], + ..Default::default() + } + } + + fn consumer(methods: Vec) -> InterfaceMeta { + InterfaceMeta { + namespace: "Contoso".into(), + name: "IConsumer".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + methods, + ..Default::default() + } + } #[test] fn generated_delegates_use_thread_affine_callback_contexts() { @@ -243,4 +304,77 @@ mod tests { assert!(runtime.contains("_dynwinrt_wrap_delegate_callback(callback),")); assert!(!runtime.contains("copy_context")); } + + #[test] + fn ireference_helper_detection_follows_nested_input_wrappers() { + let iterable = parameterized( + "IIterable`1", + super::super::collections::IITERABLE_PIID, + vec![reference()], + ); + let mapping = parameterized( + "IMap`2", + super::super::collections::IMAP_PIID, + vec![TypeMeta::String, reference()], + ); + assert!(has_ireference_input([&method(iterable)])); + assert!(has_ireference_input([&method(mapping)])); + assert!(has_ireference_input([&method(TypeMeta::Array(Box::new( + reference() + )))])); + } + + #[test] + fn ireference_helper_detection_ignores_non_reference_nested_inputs() { + let iterable = parameterized( + "IIterable`1", + super::super::collections::IITERABLE_PIID, + vec![TypeMeta::I32], + ); + let mapping = parameterized( + "IMap`2", + super::super::collections::IMAP_PIID, + vec![TypeMeta::String, TypeMeta::I32], + ); + assert!(!has_ireference_input([&method(iterable)])); + assert!(!has_ireference_input([&method(mapping)])); + } + + #[test] + fn generated_nested_ireference_inputs_emit_the_boxing_helper() { + let reference = reference(); + let iterable = parameterized( + "IIterable`1", + super::super::collections::IITERABLE_PIID, + vec![reference.clone()], + ); + let mapping = parameterized( + "IMap`2", + super::super::collections::IMAP_PIID, + vec![TypeMeta::String, reference], + ); + let interface = consumer(vec![method(iterable), method(mapping)]); + let context = PythonProjectionContext::standalone([interface.type_identity()]).unwrap(); + let generated = generate_interface(&context, &interface); + + assert!(generated.contains("def _dynwinrt_box_reference(value, value_type, wrap):")); + assert!(generated.contains( + "_dynwinrt_vector(value, lambda item: _dynwinrt_collection_item(item, lambda item: _dynwinrt_box_reference(" + )); + assert!(generated.contains( + "_dynwinrt_map(value, lambda item: _dynwinrt_collection_item(item, lambda item: DynWinRTValue.from_hstring(item)" + )); + assert!(generated.contains( + "lambda item: _dynwinrt_collection_item(item, lambda item: _dynwinrt_box_reference(" + )); + + let scalar = consumer(vec![method(parameterized( + "IIterable`1", + super::super::collections::IITERABLE_PIID, + vec![TypeMeta::I32], + ))]); + let generated = generate_interface(&context, &scalar); + assert!(!generated.contains("def _dynwinrt_box_reference(value, value_type, wrap):")); + assert!(!generated.contains("_dynwinrt_box_reference(")); + } } diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 444efc56..52380c98 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -974,6 +974,88 @@ def mapping(values: JsonObject) -> None: ); if has_implementation_runtime() { + // Nested IReference collection wrappers call the local boxing + // helper. Generate an otherwise synthetic module to prove recursive + // helper detection emits it; the runtime script below imports and + // invokes the emitted helper. A scalar collection is the negative + // control and must not emit it. + let reference = TypeMeta::Parameterized { + namespace: "Windows.Foundation".into(), + name: "IReference`1".into(), + piid: "61c17706-2d65-11e0-9ae8-d48564015472".into(), + args: vec![TypeMeta::I32], + }; + let iterable = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IIterable`1".into(), + piid: "faa585ea-6214-4217-afda-7f46de5869b3".into(), + args: vec![reference.clone()], + }; + let mapping = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IMap`2".into(), + piid: "3c2925fe-8519-45c1-aa79-197b6718c1c1".into(), + args: vec![TypeMeta::String, reference.clone()], + }; + let nested = interface( + "INestedReferences", + 99, + vec![ + MethodMeta { + name: "SetValues".into(), + raw_name: "SetValues".into(), + params: vec![parameter(iterable.clone())], + ..Default::default() + }, + MethodMeta { + name: "SetMapping".into(), + raw_name: "SetMapping".into(), + params: vec![parameter(mapping.clone())], + ..Default::default() + }, + ], + ); + let nested_context = python::PythonProjectionContext::new( + [ + nested.type_identity(), + iterable.type_identity(), + mapping.type_identity(), + reference.type_identity(), + ], + true, + ) + .unwrap(); + let nested_module = nested_context.implementation_module_for_interface(&nested); + let nested_source = python::generate_interface(&nested_context, &nested); + assert!(nested_source.contains("def _dynwinrt_box_reference(value, value_type, wrap):")); + assert!(nested_source.contains("_dynwinrt_vector(value, lambda item:")); + assert!(nested_source.contains("_dynwinrt_map(value, lambda item:")); + assert!(nested_source.contains("_dynwinrt_box_reference(")); + fs::write( + fixture.0.join("sdk").join(format!("{nested_module}.py")), + nested_source, + ) + .unwrap(); + + let scalar = interface( + "IScalarCollection", + 100, + vec![MethodMeta { + name: "SetValues".into(), + raw_name: "SetValues".into(), + params: vec![parameter(TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IIterable`1".into(), + piid: "faa585ea-6214-4217-afda-7f46de5869b3".into(), + args: vec![TypeMeta::I32], + })], + ..Default::default() + }], + ); + let scalar_source = python::generate_interface(&nested_context, &scalar); + assert!(!scalar_source.contains("def _dynwinrt_box_reference(value, value_type, wrap):")); + assert!(!scalar_source.contains("_dynwinrt_box_reference(")); + let script = r#"from dynwinrt import DynWinRTType, DynWinRTValue, RoApartment, projected_lifetime_scope from sdk.windows.foundation.collections import ( IIterable_StorageFolder, @@ -983,8 +1065,20 @@ from sdk.windows.foundation.collections import ( IVector_StorageFolder, ) from sdk.windows__data__json__json_object import IID_IJsonValue, IMap_String_IJsonValue +from sdk.__NESTED_MODULE__ import _dynwinrt_box_reference with RoApartment(1), projected_lifetime_scope(): + boxed = _dynwinrt_box_reference( + 17, DynWinRTType.i32_type(), DynWinRTValue.from_i32 + ) + assert not boxed.is_null() + boxed.release() + null = _dynwinrt_box_reference( + None, DynWinRTType.i32_type(), DynWinRTValue.from_i32 + ) + assert null.is_null() + null.release() + for vector_type in (IVector_StorageFolder, IObservableVector_StorageFolder): vector = vector_type.create([None]) assert vector[0] is None @@ -1036,7 +1130,8 @@ with RoApartment(1), projected_lifetime_scope(): else: raise AssertionError(f"{expected!r} was not raised") print("nullable-collection-native-ok", flush=True) -"#; +"# + .replace("__NESTED_MODULE__", &nested_module); fs::write(fixture.0.join("nullable_collections.py"), script).unwrap(); let output = Command::new(python()) .args(["-B", "nullable_collections.py"]) From 6bd96786a3dfe5988362f3f75a159923136dd9ce Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 12:43:52 +0800 Subject: [PATCH 08/12] Type map split and reference keys by native contract 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> --- bindings/py/README.md | 28 +- .../codegen/winrt/python/generator/types.rs | 45 +- .../src/codegen/winrt/python/nullability.rs | 66 ++- .../src/codegen/winrt/python/signature.rs | 13 +- .../src/codegen/winrt/python/stubs.rs | 3 +- .../src/codegen/winrt/python/type_helpers.rs | 37 +- tools/dynwinrt-codegen/src/meta.rs | 105 ++++- .../tests/python_consumer_typing_test.rs | 395 +++++++++++++++++- .../tests/python_stub_nullability_test.rs | 13 +- 9 files changed, 664 insertions(+), 41 deletions(-) diff --git a/bindings/py/README.md b/bindings/py/README.md index efd13ca8..a3797462 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -42,19 +42,23 @@ These values keep `| None`: values, which are often null. Reference-type elements read from WinRT collection interfaces are always typed -`T | None`, including vectors, views, iterables, iterators, map values and -key-value-pair values. Map keys remain non-null. A view or iterator obtained -from a mutable collection can expose a null slot, and WinRT collection -interfaces do not retain enough provenance for the stubs to distinguish that -case. For example, a `JsonArray` holds `IJsonValue | None`, and -`get_files_async()` returns `WinRTCoroutine[Sequence[StorageFile | None]]`. -Value-type elements remain non-null. - -Mutable collections accept `None` when their element or map-value type is a -WinRT reference type and store a real null WinRT value. This includes +`T | None`, including vectors, views, iterables, iterators, map keys and values, +and key-value-pair keys and values. Here reference means the projection's +supported COM-pointer shapes: `Object`, interfaces, runtime classes, delegates, +and parameterized interfaces. Async wrappers are not collection element +shapes. String, GUID, scalar, enum, and struct keys remain non-null. A view or +iterator obtained from a mutable collection can expose a null slot, and WinRT +collection interfaces do not retain enough provenance for the stubs to +distinguish that case. For example, a `JsonArray` holds +`IJsonValue | None`, and `get_files_async()` returns +`WinRTCoroutine[Sequence[StorageFile | None]]`. Value-type elements remain +non-null. + +Mutable collections accept `None` when their element, map-key, or map-value +type is a WinRT reference type and store a real null WinRT value. This includes `append()`, `insert()`, index and slice assignment, `extend()`, `update()` and -`setdefault()`. Map keys and value-type elements reject `None` with -`TypeError`. +`setdefault()`. String, GUID, scalar, enum, and struct keys and value-type +elements reject `None` with `TypeError`. Other arguments keep accepting `None` where they did before. The stubs are optimistic, like the generated TypeScript declarations: the runtime still diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 9ebe8dfd..b99bc0e8 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -420,10 +420,11 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe } else if piid == "3c2925fe-8519-45c1-aa79-197b6718c1c1" && iface.generic_args.len() == 2 { let key_type = py_dynwinrt_type(&iface.generic_args[0]); let val_type = py_dynwinrt_type(&iface.generic_args[1]); - let key_annotation = crate::codegen::winrt::python::type_helpers::py_param_type_safe( - &iface.generic_args[0], - context, - ); + let key_annotation = + crate::codegen::winrt::python::type_helpers::py_collection_input_type( + &iface.generic_args[0], + context, + ); let val_annotation = crate::codegen::winrt::python::type_helpers::py_collection_input_type( &iface.generic_args[1], @@ -773,4 +774,40 @@ mod tests { "_dynwinrt_collection_item(item, lambda item: getattr(item, '_obj', item), True, 'collection element')" )); } + + #[test] + fn map_factory_key_nullability_follows_the_declared_key_type() { + let reference_key = TypeMeta::Interface { + namespace: "Windows.Foundation".into(), + name: "IStringable".into(), + iid: "96369f54-8eb6-48f0-abce-c1b211e627c3".into(), + }; + let iface = |key| InterfaceMeta { + name: "IMap_Key_Object".into(), + iid: "3c2925fe-8519-45c1-aa79-197b6718c1c1".into(), + generic_piid: Some("3c2925fe-8519-45c1-aa79-197b6718c1c1".into()), + generic_args: vec![key, TypeMeta::Object], + ..Default::default() + }; + let context = PythonProjectionContext::default(); + + let reference = generate_interface(&context, &iface(reference_key)); + let create = reference + .lines() + .find(|line| line.contains("def create(items: Mapping[")) + .expect("map factory"); + assert!( + create.contains("None"), + "reference-key map factory must accept None: {create}" + ); + assert!(reference.contains("True, 'map key'")); + assert!(reference.contains("True, 'map value'")); + + for key in [TypeMeta::String, TypeMeta::Guid] { + let generated = generate_interface(&context, &iface(key)); + assert!(generated.contains("False, 'map key'")); + assert!(!generated.contains("Mapping[str | None")); + assert!(!generated.contains("Mapping[UUID | None")); + } + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index c820bef0..d614f58d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -59,7 +59,8 @@ pub(crate) enum ElementContainer { View, /// `IVector`, `IMap` or their observable forms. Mutable, - /// A map key or `IKeyValuePair.Key`. Null keys are invalid. + /// A map key or `IKeyValuePair.Key`. Reference keys can be null; string + /// and value-type keys cannot. MapKey, /// An array returned by a member that does not read collection elements. Array, @@ -109,6 +110,9 @@ pub(crate) struct OutputSite { pub(crate) try_method: bool, /// The Windows SDK documentation says the member's result can be null. pub(crate) documented_null: bool, + /// The native member contract marks this exact logical output as an + /// optional interface pointer (`IMapView.Split`'s two halves). + pub(crate) contract_nullable: bool, /// The collection holding the value: set on collection elements, and on /// the results of members that read elements of the collection declaring /// them (`get_at`, `lookup`, `current`, ...). @@ -121,6 +125,7 @@ impl OutputSite { position, try_method: false, documented_null: false, + contract_nullable: false, container: None, } } @@ -130,10 +135,24 @@ impl OutputSite { position, try_method: is_try_method(method), documented_null: method.documented_null_result, + contract_nullable: false, container: method.element_access.map(ElementContainer::from), } } + /// A specific logical method output, carrying per-output native contract + /// facts rather than widening every result of the member. + pub(crate) fn for_method_output( + method: &MethodMeta, + position: OutputPosition, + index: usize, + ) -> Self { + Self { + contract_nullable: method.contract_nullable_outputs.contains(&index), + ..Self::for_method(method, position) + } + } + /// An element read from a collection of the given kind. pub(crate) fn element_of(container: ElementContainer) -> Self { Self::of(OutputPosition::CollectionElement).element_in(container) @@ -214,7 +233,10 @@ fn stub_output_admits_none( }; if site.container == Some(ElementContainer::MapKey) { - return false; + // output_admits_none only reaches this branch after + // may_project_none(typ), which deliberately covers the Python + // projection's supported COM-pointer shapes (not async wrappers). + return true; } // `Object` positions are frequently null, e.g. the arguments of a // `TypedEventHandler`. @@ -231,7 +253,7 @@ fn stub_output_admits_none( site.position, Return | OutParam | Property | AsyncResult | Activation ); - if member_result && (site.try_method || site.documented_null) { + if member_result && (site.try_method || site.documented_null || site.contract_nullable) { return true; } // Collection elements, including the results of `get_at`, `lookup` and @@ -271,6 +293,7 @@ mod tests { position: OutputPosition::AsyncResult, try_method: true, documented_null: false, + contract_nullable: false, container: None, } ); @@ -280,6 +303,7 @@ mod tests { position: OutputPosition::CollectionElement, try_method: true, documented_null: false, + contract_nullable: false, container: Some(ElementContainer::Array), } ); @@ -437,8 +461,14 @@ mod tests { assert!(admits(&TypeMeta::Object, array, stub)); assert!(admits(&nullable_u32(), array, stub)); let key = OutputSite::element_of(ElementContainer::MapKey); - assert!(!admits(&widget(), key, stub)); - assert!(!admits(&TypeMeta::Object, key, stub)); + assert!(admits(&widget(), key, stub)); + assert!(admits(&TypeMeta::Object, key, stub)); + assert!(admits(&nullable_u32(), key, stub)); + assert!(!admits(&TypeMeta::String, key, stub)); + assert!(!admits(&TypeMeta::Guid, key, stub)); + // Async projection shapes are not ordinary collection values in the + // current Python codegen, so this scope stays fail closed. + assert!(!admits(&TypeMeta::AsyncAction, key, stub)); for access in [ElementAccess::Mutable, ElementAccess::ReadOnly] { let get_at = MethodMeta { @@ -469,10 +499,34 @@ mod tests { element_access: Some(ElementAccess::MapKey), ..method("get_Key") }; - assert!(!admits( + assert!(admits( &widget(), OutputSite::for_method(&key, OutputPosition::Property), stub )); } + + #[test] + fn native_contract_nullability_is_per_logical_output() { + let split = MethodMeta { + contract_nullable_outputs: vec![0, 1], + ..method("Split") + }; + for (index, expected) in [(0, true), (1, true), (2, false)] { + assert_eq!( + admits( + &widget(), + OutputSite::for_method_output(&split, OutputPosition::OutParam, index), + AnnotationSurface::Stub + ), + expected, + "logical output {index}" + ); + } + assert!(!admits( + &widget(), + OutputSite::for_method(&split, OutputPosition::Return), + AnnotationSurface::Stub + )); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 9d0cbf4d..9510b843 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -488,7 +488,10 @@ pub(crate) fn py_wrap_collection_item( role: CollectionInputRole, context: &PythonProjectionContext, ) -> String { - let allow_none = role != CollectionInputRole::Key && super::nullability::may_project_none(typ); + // The native collection plan accepts null for the Python projection's + // supported COM-pointer shapes in every role, including map keys. String, + // Guid, scalar, enum and struct keys still fail closed here. + let allow_none = super::nullability::may_project_none(typ); let label = match role { CollectionInputRole::Element => "collection element", CollectionInputRole::Key => "map key", @@ -1010,7 +1013,13 @@ mod tests { assert!(nullable.contains("True, 'collection element'")); let key = py_wrap_collection_item("key", &geometry, CollectionInputRole::Key, &context); - assert!(key.contains("False, 'map key'")); + assert!(key.contains("True, 'map key'")); + let string_key = + py_wrap_collection_item("key", &TypeMeta::String, CollectionInputRole::Key, &context); + assert!(string_key.contains("False, 'map key'")); + let guid_key = + py_wrap_collection_item("key", &TypeMeta::Guid, CollectionInputRole::Key, &context); + assert!(guid_key.contains("False, 'map key'")); let scalar = py_wrap_collection_item( "value", &TypeMeta::I32, diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index caecfdfe..4c7b1277 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -540,7 +540,8 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf element, iface.name )); } else if piid == "3c2925fe-8519-45c1-aa79-197b6718c1c1" && iface.generic_args.len() == 2 { - let key = super::type_helpers::py_param_type_safe(&iface.generic_args[0], context); + let key = + super::type_helpers::py_collection_input_type(&iface.generic_args[0], context); let value = super::type_helpers::py_collection_input_type(&iface.generic_args[1], context); out.push('\n'); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index e35171ff..714fd4da 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -592,10 +592,11 @@ pub(super) fn py_method_return_type( ) -> String { let outputs = py_method_output_positions(method) .into_iter() - .map(|(typ, position)| { + .enumerate() + .map(|(index, (typ, position))| { py_output_annotation( typ, - OutputSite::for_method(method, position), + OutputSite::for_method_output(method, position, index), surface, context, ) @@ -692,8 +693,8 @@ fn py_collection_param_type(typ: &TypeMeta, context: &PythonProjectionContext) - }; return Some(format!( "Mapping[{}, {}]", - py_native_param_element_type(key, context), - py_native_param_element_type(value, context) + py_collection_input_type(key, context), + py_collection_input_type(value, context) )); } let element = args.first()?; @@ -710,7 +711,7 @@ fn py_collection_param_type(typ: &TypeMeta, context: &PythonProjectionContext) - } else { "Sequence" }, - py_native_param_element_type(element, context) + py_collection_input_type(element, context) )), _ => None, } @@ -759,7 +760,7 @@ pub(super) fn py_method_param_list( Some(CollectionInputRole::Element | CollectionInputRole::Value) => { py_collection_contract_input_type(¶m.typ, context) } - Some(CollectionInputRole::Key) => py_param_type_safe(¶m.typ, context), + Some(CollectionInputRole::Key) => py_collection_input_type(¶m.typ, context), None if context.is_delegate_type(¶m.typ) => { py_delegate_param_type(¶m.typ, context) } @@ -945,13 +946,13 @@ mod tests { "IIterable`1", "faa585ea-6214-4217-afda-7f46de5869b3", vec![TypeMeta::Object], - "Iterable['DynWinRTValue | _DynWinRTObject']", + "Iterable[DynWinRTValue | _DynWinRTObject | None]", ), ( "IMap`2", "3c2925fe-8519-45c1-aa79-197b6718c1c1", vec![TypeMeta::String, TypeMeta::Object], - "Mapping[str, 'DynWinRTValue | _DynWinRTObject']", + "Mapping[str, DynWinRTValue | _DynWinRTObject | None]", ), ] { let typ = TypeMeta::Parameterized { @@ -990,7 +991,7 @@ mod tests { }; assert_eq!( py_param_type_safe(&mapping, &context), - "Mapping[str, 'DynWinRTValue | _DynWinRTObject_2']" + "Mapping[str, DynWinRTValue | _DynWinRTObject_2 | None]" ); assert_eq!( py_collection_input_type(&TypeMeta::Object, &context), @@ -1142,6 +1143,24 @@ mod tests { ), Some("MutableMapping[str, Widget | None]".to_string()) ); + assert_eq!( + py_collection_base_type( + CollectionKind::MutableMapping, + &[widget.clone(), TypeMeta::Object], + stub, + &context + ), + Some("MutableMapping[Widget | None, DynWinRTValue | None]".to_string()) + ); + assert_eq!( + py_collection_base_type( + CollectionKind::MutableMapping, + &[TypeMeta::Guid, TypeMeta::Object], + stub, + &context + ), + Some("MutableMapping[UUID, DynWinRTValue | None]".to_string()) + ); let vector = |piid: &str| TypeMeta::Parameterized { namespace: "Windows.Foundation.Collections".into(), name: "IVector`1".into(), diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 06413741..126f5039 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -68,6 +68,10 @@ pub struct MethodMeta { /// values. Python uses this to validate and wrap `None` at the collection /// boundary without changing general WinRT parameter conversion. pub collection_inputs: Vec<(usize, CollectionInputRole)>, + /// Logical output indexes whose native contract permits a null reference + /// independently of general result nullability. `IMapView.Split` uses this + /// for its two optional map-view halves. + pub contract_nullable_outputs: Vec, } /// How a member reads the elements of the `Windows.Foundation.Collections` @@ -75,11 +79,12 @@ pub struct MethodMeta { /// `Value`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElementAccess { - /// `IIterator`, `IVectorView`, `IMapView` and `IKeyValuePair`. + /// `IIterator`, `IVectorView`, `IMapView` and `IKeyValuePair.Value`. ReadOnly, /// `IVector` and `IMap`, in which anyone can store null. Mutable, - /// The key of an `IKeyValuePair`. Null keys are not valid map entries. + /// The key of an `IKeyValuePair`. Whether null is valid depends on the + /// declared key ABI type. MapKey, } @@ -132,6 +137,99 @@ fn collection_input_roles( } } +fn collection_nullable_output_indices( + namespace: &str, + definition: &str, + member: &str, +) -> Vec { + if namespace == WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE + && definition == "IMapView`2" + && member == "Split" + { + vec![0, 1] + } else { + Vec::new() + } +} + +#[cfg(test)] +mod collection_contract_tests { + use super::*; + + #[test] + fn map_view_split_alone_has_two_optional_outputs() { + assert_eq!( + collection_nullable_output_indices( + WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, + "IMapView`2", + "Split" + ), + vec![0, 1] + ); + for (definition, member) in [ + ("IMapView`2", "Lookup"), + ("IMap`2", "Split"), + ("IVectorView`1", "Split"), + ] { + assert!( + collection_nullable_output_indices( + WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, + definition, + member + ) + .is_empty() + ); + } + } + + #[test] + fn map_key_inputs_cover_mutable_and_view_operations() { + for (definition, member) in [ + ("IMap`2", "Lookup"), + ("IMap`2", "HasKey"), + ("IMap`2", "Remove"), + ("IMapView`2", "Lookup"), + ("IMapView`2", "HasKey"), + ] { + assert_eq!( + collection_input_roles( + WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, + definition, + member + ), + vec![(0, CollectionInputRole::Key)] + ); + } + assert_eq!( + collection_input_roles(WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, "IMap`2", "Insert"), + vec![ + (0, CollectionInputRole::Key), + (1, CollectionInputRole::Value) + ] + ); + } + + #[test] + fn key_value_pair_key_and_value_have_distinct_element_contracts() { + assert_eq!( + collection_element_access( + WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, + "IKeyValuePair`2", + "get_Key" + ), + Some(ElementAccess::MapKey) + ); + assert_eq!( + collection_element_access( + WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE, + "IKeyValuePair`2", + "get_Value" + ), + Some(ElementAccess::ReadOnly) + ); + } +} + /// A WinRT interface with its methods. #[derive(Debug, Clone, Default)] pub struct InterfaceMeta { @@ -1865,6 +1963,8 @@ fn parse_interface_methods( let element_access = collection_element_access(namespace, def.name(), &raw_name); let collection_inputs = collection_input_roles(namespace, def.name(), &raw_name); + let contract_nullable_outputs = + collection_nullable_output_indices(namespace, def.name(), &raw_name); let mut method_meta = MethodMeta { name: method_name.clone(), vtable_index, @@ -1883,6 +1983,7 @@ fn parse_interface_methods( documented_null_result: false, element_access, collection_inputs, + contract_nullable_outputs, }; method_meta.documented_null_result = crate::documented_nulls::documents_null_result(&documentation_owner, &method_meta); diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 52380c98..2bb226e6 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -138,6 +138,31 @@ fn typecheck(fixture: &Fixture, packages: &[&str], consumer: &str, errors: &[&st } } +fn pyright_typecheck(fixture: &Fixture, consumer: &str, expected_errors: usize) { + let Some(pyright) = std::env::var_os("DYNWINRT_PYRIGHT") else { + return; + }; + fs::write( + fixture.0.join("pyright_consumer.py"), + format!("# pyright: strict\n{consumer}"), + ) + .unwrap(); + let output = Command::new(pyright) + .args(["--pythonpath"]) + .arg(python()) + .arg("pyright_consumer.py") + .current_dir(&fixture.0) + .output() + .unwrap(); + let text = diagnostics(&output); + let actual = text + .lines() + .filter(|line| line.contains(" - error: ")) + .count(); + assert_eq!(actual, expected_errors, "{text}"); + assert_eq!(output.status.success(), expected_errors == 0, "{text}"); +} + fn parameter(typ: TypeMeta) -> ParamMeta { ParamMeta { name: "value".into(), @@ -252,7 +277,7 @@ fn generate_fixture(fixture: &Fixture, package: &str) { "IVector_Resource", "IVector`1", "913337e9-11a1-4345-a3a2-4e7f956e222d", - vec![resource_type], + vec![resource_type.clone()], ), ( "IMap_Object_Object", @@ -260,6 +285,24 @@ fn generate_fixture(fixture: &Fixture, package: &str) { "3c2925fe-8519-45c1-aa79-197b6718c1c1", vec![TypeMeta::Object, TypeMeta::Object], ), + ( + "IMap_String_Object", + "IMap`2", + "3c2925fe-8519-45c1-aa79-197b6718c1c1", + vec![TypeMeta::String, TypeMeta::Object], + ), + ( + "IMap_Resource_Resource", + "IMap`2", + "3c2925fe-8519-45c1-aa79-197b6718c1c1", + vec![resource_type.clone(), resource_type], + ), + ( + "IMap_Guid_Object", + "IMap`2", + "3c2925fe-8519-45c1-aa79-197b6718c1c1", + vec![TypeMeta::Guid, TypeMeta::Object], + ), ] { let methods = if definition == "IVector`1" { vec![ @@ -281,6 +324,30 @@ fn generate_fixture(fixture: &Fixture, package: &str) { ..Default::default() }, ] + } else if definition == "IMap`2" { + vec![MethodMeta { + name: "Insert".into(), + raw_name: "Insert".into(), + vtable_index: 10, + params: vec![ + ParamMeta { + name: "key".into(), + typ: args[0].clone(), + direction: ParamDirection::In, + }, + ParamMeta { + name: "value".into(), + typ: args[1].clone(), + direction: ParamDirection::In, + }, + ], + return_type: Some(TypeMeta::Bool), + collection_inputs: vec![ + (0, dynwinrt_codegen::meta::CollectionInputRole::Key), + (1, dynwinrt_codegen::meta::CollectionInputRole::Value), + ], + ..Default::default() + }] } else { Vec::new() }; @@ -498,6 +565,92 @@ def invalid_collections(resource: Resource, unrelated: Unrelated, "[assignment]", ], ); + typecheck( + &fixture, + &["first"], + r#"from typing import assert_type +from dynwinrt import DynWinRTValue +from first.contoso__resource import Resource +from first.windows__foundation__collections__i_map_object_object import IMap_Object_Object +from first.windows__foundation__collections__i_map_resource_resource import IMap_Resource_Resource + +def nullable_reference_keys( + objects: IMap_Object_Object, resources: IMap_Resource_Resource +) -> None: + objects[None] = None + assert_type(objects[None], DynWinRTValue | None) + resources[None] = None + resources.update({None: None}) + assert_type(resources.setdefault(None, None), Resource | None) + assert_type(resources[None], Resource | None) +"#, + &[], + ); + pyright_typecheck( + &fixture, + r#"from typing import assert_type +from dynwinrt import DynWinRTValue +from first.contoso__resource import Resource +from first.windows__foundation__collections__i_map_object_object import IMap_Object_Object +from first.windows__foundation__collections__i_map_resource_resource import IMap_Resource_Resource + +def nullable_reference_keys( + objects: IMap_Object_Object, resources: IMap_Resource_Resource +) -> None: + objects[None] = None + assert_type(objects[None], DynWinRTValue | None) + resources[None] = None + resources.update({None: None}) + assert_type(resources.setdefault(None, None), Resource | None) + assert_type(resources[None], Resource | None) +"#, + 0, + ); + typecheck( + &fixture, + &["first"], + r#"from first.windows__foundation__collections__i_map_guid_object import IMap_Guid_Object +from first.windows__foundation__collections__i_map_string_object import IMap_String_Object +from uuid import UUID + +def invalid_key(guid: IMap_Guid_Object, string: IMap_String_Object) -> None: + guid[None] = None + guid.get(None) + guid.update({None: None}) + guid.setdefault(None, None) + string[None] = None + string.get(None) + string.update({None: None}) + string.setdefault(None, None) +"#, + &[ + "[index]", + "[call-overload]", + "[dict-item]", + "[call-overload]", + "[index]", + "[call-overload]", + "[dict-item]", + "[call-overload]", + ], + ); + pyright_typecheck( + &fixture, + r#"from first.windows__foundation__collections__i_map_guid_object import IMap_Guid_Object +from first.windows__foundation__collections__i_map_string_object import IMap_String_Object + +def invalid_key(guid: IMap_Guid_Object, string: IMap_String_Object) -> None: + guid[None] = None + guid.get(None) + guid.update({None: None}) + guid.setdefault(None, None) + string[None] = None + string.get(None) + string.update({None: None}) + string.setdefault(None, None) +"#, + 12, + ); } #[test] @@ -936,7 +1089,8 @@ fn mutable_collection_mutators_accept_none() { .args([ "--class-name", "Windows.Storage.StorageLibrary,Windows.Data.Json.JsonObject,\ - Windows.Foundation.Collections.StringMap", + Windows.Foundation.Collections.StringMap,Windows.Foundation.Uri,\ + Windows.Foundation.Collections.PropertySet,Windows.UI.Xaml.ResourceDictionary", "--lang", "py", "--output", @@ -945,6 +1099,141 @@ fn mutable_collection_mutators_accept_none() { .output() .unwrap(); assert!(output.status.success(), "{}", diagnostics(&output)); + + let stringable = TypeMeta::Interface { + namespace: "Windows.Foundation".into(), + name: "IStringable".into(), + iid: "96369f54-8eb6-48f0-abce-c1b211e627c3".into(), + }; + let stringable_map_type = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IMap`2".into(), + piid: "3c2925fe-8519-45c1-aa79-197b6718c1c1".into(), + args: vec![stringable.clone(), stringable.clone()], + }; + let stringable_map = InterfaceMeta { + namespace: "Windows.Foundation.Collections".into(), + name: "IMap_IStringable_IStringable".into(), + generic_name: Some("IMap`2".into()), + generic_piid: Some("3c2925fe-8519-45c1-aa79-197b6718c1c1".into()), + generic_args: vec![stringable.clone(), stringable.clone()], + methods: vec![ + MethodMeta { + name: "Lookup".into(), + raw_name: "Lookup".into(), + vtable_index: 6, + params: vec![parameter(stringable.clone())], + return_type: Some(stringable.clone()), + element_access: Some(dynwinrt_codegen::meta::ElementAccess::Mutable), + collection_inputs: vec![(0, dynwinrt_codegen::meta::CollectionInputRole::Key)], + ..Default::default() + }, + MethodMeta { + name: "get_Size".into(), + raw_name: "get_Size".into(), + vtable_index: 7, + is_property_getter: true, + return_type: Some(TypeMeta::U32), + ..Default::default() + }, + MethodMeta { + name: "HasKey".into(), + raw_name: "HasKey".into(), + vtable_index: 8, + params: vec![parameter(stringable.clone())], + return_type: Some(TypeMeta::Bool), + collection_inputs: vec![(0, dynwinrt_codegen::meta::CollectionInputRole::Key)], + ..Default::default() + }, + MethodMeta { + name: "GetView".into(), + raw_name: "GetView".into(), + vtable_index: 9, + return_type: Some(TypeMeta::Object), + ..Default::default() + }, + MethodMeta { + name: "Insert".into(), + raw_name: "Insert".into(), + vtable_index: 10, + params: vec![ + ParamMeta { + name: "key".into(), + typ: stringable.clone(), + direction: ParamDirection::In, + }, + ParamMeta { + name: "value".into(), + typ: stringable.clone(), + direction: ParamDirection::In, + }, + ], + return_type: Some(TypeMeta::Bool), + collection_inputs: vec![ + (0, dynwinrt_codegen::meta::CollectionInputRole::Key), + (1, dynwinrt_codegen::meta::CollectionInputRole::Value), + ], + ..Default::default() + }, + MethodMeta { + name: "Remove".into(), + raw_name: "Remove".into(), + vtable_index: 11, + params: vec![parameter(stringable.clone())], + collection_inputs: vec![(0, dynwinrt_codegen::meta::CollectionInputRole::Key)], + ..Default::default() + }, + MethodMeta { + name: "Clear".into(), + raw_name: "Clear".into(), + vtable_index: 12, + ..Default::default() + }, + ], + ..Default::default() + }; + let stringable_context = python::PythonProjectionContext::new( + [ + stringable.type_identity(), + stringable_map_type.type_identity(), + ], + true, + ) + .unwrap(); + let stringable_module = stringable_context.implementation_module_for_interface(&stringable_map); + fs::write( + fixture + .0 + .join("sdk") + .join(format!("{stringable_module}.py")), + python::generate_interface(&stringable_context, &stringable_map), + ) + .unwrap(); + fs::write( + fixture + .0 + .join("sdk") + .join(format!("{stringable_module}.pyi")), + python_stub::generate_interface_stub(&stringable_context, &stringable_map), + ) + .unwrap(); + let object_map_stub = fs::read_to_string( + fixture + .0 + .join("sdk") + .join("windows__foundation__collections__i_map_object_object.pyi"), + ) + .unwrap(); + assert!(object_map_stub.contains("MutableMapping[DynWinRTValue | None, DynWinRTValue | None]")); + let pair_stub = fs::read_to_string( + fixture + .0 + .join("sdk") + .join("windows__foundation__collections__i_key_value_pair_object_object.pyi"), + ) + .unwrap(); + assert!(pair_stub.contains("def key(self) -> DynWinRTValue | None: ...")); + assert!(pair_stub.contains("def value(self) -> DynWinRTValue | None: ...")); // Inherited MutableSequence and MutableMapping mutators take the element // type of the collection base, which keeps `| None` for mutable // collections, like the generated item setters. @@ -952,9 +1241,19 @@ fn mutable_collection_mutators_accept_none() { &fixture, &["sdk"], r#"from typing import assert_type +from dynwinrt import DynWinRTValue from sdk.windows.data.json import IJsonValue, JsonObject -from sdk.windows.foundation.collections import IObservableVector_StorageFolder +from sdk.windows.foundation import IStringable, Uri +from sdk.windows.foundation.collections import ( + IMap_Object_Object, + IMap_String_String, + IObservableVector_StorageFolder, +) from sdk.windows.storage import StorageFolder +from sdk.windows__foundation__collections__property_set import IMap_String_Object +from sdk.windows__foundation__collections__i_map_i_stringable_i_stringable import ( + IMap_IStringable_IStringable, +) def vector(folders: IObservableVector_StorageFolder) -> None: IObservableVector_StorageFolder.create([None]) @@ -969,6 +1268,23 @@ def mapping(values: JsonObject) -> None: values.setdefault("k", None) values["k"] = None assert_type(values["k"], IJsonValue | None) + +def object_values(values: IMap_String_Object, uri: Uri) -> None: + values["none"] = None + values["uri"] = uri + assert_type(values["none"], DynWinRTValue | None) + +def reference_keys( + objects: IMap_Object_Object, stringable: IMap_IStringable_IStringable +) -> None: + objects[None] = None + stringable[None] = None + assert_type(objects[None], DynWinRTValue | None) + assert_type(stringable[None], IStringable | None) + +def string_keys(values: IMap_String_String) -> None: + values["key"] = "value" + values.get("key") "#, &[], ); @@ -1056,14 +1372,21 @@ def mapping(values: JsonObject) -> None: assert!(!scalar_source.contains("def _dynwinrt_box_reference(value, value_type, wrap):")); assert!(!scalar_source.contains("_dynwinrt_box_reference(")); - let script = r#"from dynwinrt import DynWinRTType, DynWinRTValue, RoApartment, projected_lifetime_scope + let script = r#"from dynwinrt import ( + DynWinRTMethodSig, DynWinRTType, DynWinRTValue, RoApartment, WinGUID, + projected_lifetime_scope, +) from sdk.windows.foundation.collections import ( IIterable_StorageFolder, + IMap_Object_Object, IMap_String_String, IObservableVector_StorageFolder, IVector_String, IVector_StorageFolder, ) +from sdk.windows__foundation__collections__i_map_i_stringable_i_stringable import ( + IMap_IStringable_IStringable, +) from sdk.windows__data__json__json_object import IID_IJsonValue, IMap_String_IJsonValue from sdk.__NESTED_MODULE__ import _dynwinrt_box_reference @@ -1110,6 +1433,70 @@ with RoApartment(1), projected_lifetime_scope(): assert mapping["index"] is None assert list(mapping.values()) == [None, None, None] + objects = IMap_Object_Object.create({None: None}) + assert None in objects + assert objects[None] is None + objects[None] = None + assert objects.setdefault(None, None) is None + assert next(iter(objects.items())) == (None, None) + assert dict(objects) == {None: None} + assert objects == {None: None} + try: + hash(objects) + except TypeError: + pass + else: + raise AssertionError("mutable maps must remain unhashable") + object_view = objects.get_view() + assert object_view is not None + assert object_view[None] is None + assert dict(object_view) == {None: None} + del objects[None] + assert None not in objects + + stringable = DynWinRTType.interface( + WinGUID.parse("96369F54-8EB6-48F0-ABCE-C1B211E627C3") + ) + stringables = IMap_IStringable_IStringable.create({None: None}) + assert None in stringables + assert stringables[None] is None + stringables[None] = None + stringable_pair = DynWinRTType.parameterized( + WinGUID.parse("02B51929-C1C4-4A7E-8940-0312B5C18500"), + [stringable, stringable], + ) + stringable_iterator = DynWinRTType.parameterized( + WinGUID.parse("6A79E863-4300-459A-9966-CBB660963EE1"), + [stringable_pair], + ) + iterable = DynWinRTType.register_interface( + "IIterable_IStringablePair", + DynWinRTType.parameterized( + WinGUID.parse("FAA585EA-6214-4217-AFDA-7F46DE5869B3"), + [stringable_pair], + ).iid(), + ).add_method("First", DynWinRTMethodSig().add_out(stringable_iterator)) + iterator = DynWinRTType.register_interface( + "IIterator_IStringablePair", stringable_iterator.iid() + ).add_method("get_Current", DynWinRTMethodSig().add_out(stringable_pair)) + pair = DynWinRTType.register_interface( + "IKeyValuePair_IStringable_IStringable", stringable_pair.iid() + ).add_method("get_Key", DynWinRTMethodSig().add_out(stringable)).add_method( + "get_Value", DynWinRTMethodSig().add_out(stringable) + ) + iterable_value = stringables._obj.cast(iterable.iid()) + iterator_value = iterable.method(6).invoke(iterable_value, []) + pair_value = iterator.method(6).invoke(iterator_value, []) + assert pair.method(6).invoke(pair_value, []).is_null() + assert pair.method(7).invoke(pair_value, []).is_null() + del stringables[None] + assert None not in stringables + + empty_string_map = IMap_String_String.create({}) + string_view = empty_string_map.get_view() + assert string_view is not None + assert string_view.split() == (None, None) + strings = IMap_String_String.create({}) invalid = ( ("map key cannot be None", lambda: mapping.__setitem__(None, None)), diff --git a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs index ea0d2e8c..59c0896d 100644 --- a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs +++ b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs @@ -293,7 +293,8 @@ fn reference_collection_elements_are_nullable_regardless_of_provenance() { let Some(generated) = Generated::new( "elements", "Windows.Storage.StorageFolder,Windows.Data.Json.JsonObject,\ - Windows.ApplicationModel.Resources.Core.ResourceMap,Windows.Media.Playback.MediaPlaybackList", + Windows.ApplicationModel.Resources.Core.ResourceMap,Windows.Media.Playback.MediaPlaybackList,\ + Windows.Foundation.Collections.StringMap", ) else { return; }; @@ -306,6 +307,10 @@ fn reference_collection_elements_are_nullable_regardless_of_provenance() { let playlist = generated.module("windows__media__playback__media_playback_list.pyi"); let observable = generated .module("windows__foundation__collections__i_observable_vector_media_playback_item.pyi"); + let string_map_view = + generated.module("windows__foundation__collections__i_map_view_string_string.pyi"); + let string_map_view_py = + generated.module("windows__foundation__collections__i_map_view_string_string.py"); // Collection interfaces carry no provenance. A view or iterator obtained // from a mutable collection can expose a null slot, so view item @@ -366,4 +371,10 @@ fn reference_collection_elements_are_nullable_regardless_of_provenance() { &observable, "def __getitem__(self, index: int) -> MediaPlaybackItem | None: ...", ); + // Split succeeds with two optional view pointers; no other IMapView + // result is widened by this native contract. + let split = "def split(self) -> tuple[Mapping[str, str] | None, Mapping[str, str] | None]: ..."; + assert_contains(&string_map_view, split); + assert_contains(&string_map_view, "def lookup(self, key: str) -> str: ..."); + assert_contains(&string_map_view_py, split.trim_end_matches(" ...")); } From a406f78cd05b98b5f5409b7e7be24078fcd0c6a9 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 17:20:37 +0800 Subject: [PATCH 09/12] Keep returned reference array elements optional 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> --- .github/workflows/build.yml | 5 +- bindings/py/README.md | 10 +- tests/e2e/runners/py_runner.py | 16 ++- .../src/codegen/winrt/python/nullability.rs | 12 +- .../src/codegen/winrt/python/type_helpers.rs | 80 ++++++++++- .../tests/python_consumer_typing_test.rs | 134 ++++++++++++++++++ .../tests/python_stub_nullability_test.rs | 115 ++++++++++++++- 7 files changed, 354 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4f9fd61..138d3bae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -549,13 +549,16 @@ jobs: $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' cargo test -p dynwinrt-codegen --test implementation_naming_test - - name: Test generated nullable collection writes + - name: Test generated nullable collection values shell: pwsh run: | $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' cargo test -p dynwinrt-codegen --test python_consumer_typing_test ` mutable_collection_mutators_accept_none -- --exact + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo test -p dynwinrt-codegen --test python_consumer_typing_test ` + reference_array_results_preserve_null_elements -- --exact - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/bindings/py/README.md b/bindings/py/README.md index a3797462..e828a69a 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -46,10 +46,12 @@ Reference-type elements read from WinRT collection interfaces are always typed and key-value-pair keys and values. Here reference means the projection's supported COM-pointer shapes: `Object`, interfaces, runtime classes, delegates, and parameterized interfaces. Async wrappers are not collection element -shapes. String, GUID, scalar, enum, and struct keys remain non-null. A view or -iterator obtained from a mutable collection can expose a null slot, and WinRT -collection interfaces do not retain enough provenance for the stubs to -distinguish that case. For example, a `JsonArray` holds +shapes. Reference-type elements of returned WinRT arrays are also `T | None`; +the array value itself remains non-null. String, GUID, scalar, enum, and struct +array elements and keys remain non-null. A view or iterator obtained from a +mutable collection can expose a null slot, and WinRT collection interfaces do +not retain enough provenance for the stubs to distinguish that case. For +example, a `JsonArray` holds `IJsonValue | None`, and `get_files_async()` returns `WinRTCoroutine[Sequence[StorageFile | None]]`. Value-type elements remain non-null. diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index 9bde333f..05c95e44 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -367,6 +367,7 @@ async def run_check( elif kind == 'nullable_object_array_roundtrip': uri_cls = generated_type(pkg_name, 'Uri') + property_value_cls = generated_type(pkg_name, 'IPropertyValue') uri = uri_cls.create_uri('https://example.com/null-array') boxed = getattr(cls, member)( [dw.DynWinRTValue.null_value(), uri._obj] @@ -374,18 +375,23 @@ async def run_check( if boxed is None: cr['error'] = 'CreateInspectableArray returned None' return cr - values = boxed.call_0( - 38, - dw.DynWinRTType.array_type(dw.DynWinRTType.object()), - ).as_array().to_values() + # Cross the generated interface method and its array converter, + # not a raw call_0/manual as_array path. + property_value = property_value_cls.from_value(boxed) + values = property_value.get_inspectable_array() if len(values) != 2: cr['error'] = f'expected 2 inspectable values, got {len(values)}' - elif not values[0].is_null(): + elif values[0] is not None: cr['error'] = 'null inspectable array element was not preserved' + elif values[1] is None: + cr['error'] = 'non-null inspectable array element became None' elif values[1].identity_raw() != uri._obj.identity_raw(): cr['error'] = 'inspectable array element lost COM identity' else: cr['pass'] = True + if values[1] is not None: + values[1].release() + dw.release_projected(property_value) elif kind == 'projection_identity': import weakref diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index d614f58d..f72af0cb 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -93,11 +93,13 @@ impl From for ElementContainer { /// the provenance needed to prove that a reference-type element is non-null. /// In particular, a view or iterator obtained from a mutable collection can /// expose a null slot. All WinRT collection element reads therefore admit -/// `None`; returned arrays remain snapshots and use the ordinary output rule. +/// `None`. Returned arrays also preserve null reference slots at runtime, so +/// their reference elements follow the same rule while the array itself +/// remains non-null. pub(crate) fn element_admits_none(container: ElementContainer) -> bool { match container { - ElementContainer::View | ElementContainer::Mutable => true, - ElementContainer::MapKey | ElementContainer::Array => false, + ElementContainer::View | ElementContainer::Mutable | ElementContainer::Array => true, + ElementContainer::MapKey => false, } } @@ -457,9 +459,11 @@ mod tests { assert!(!admits(&TypeMeta::String, site, stub)); } let array = OutputSite::element_of(ElementContainer::Array); - assert!(!admits(&widget(), array, stub)); + assert!(admits(&widget(), array, stub)); assert!(admits(&TypeMeta::Object, array, stub)); assert!(admits(&nullable_u32(), array, stub)); + assert!(!admits(&TypeMeta::String, array, stub)); + assert!(!admits(&TypeMeta::Guid, array, stub)); let key = OutputSite::element_of(ElementContainer::MapKey); assert!(admits(&widget(), key, stub)); assert!(admits(&TypeMeta::Object, key, stub)); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index 714fd4da..96515bca 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -403,7 +403,8 @@ fn spell_collection( } /// The elements of an array filled by a member that reads collection -/// elements (`get_many`) follow that collection; other arrays are snapshots. +/// elements (`get_many`) follow that collection; other arrays preserve their +/// own nullable reference slots. fn array_element(site: OutputSite) -> OutputSite { site.element_in(site.container.unwrap_or(ElementContainer::Array)) } @@ -930,6 +931,81 @@ mod tests { } } + #[test] + fn ordinary_reference_array_elements_are_nullable_but_the_array_is_not() { + let runtime_class = TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + }; + let interface = TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IWidget".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + }; + let delegate = TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "WidgetHandler".into(), + iid: "22222222-2222-2222-2222-222222222222".into(), + }; + let reference = TypeMeta::Parameterized { + namespace: "Windows.Foundation".into(), + name: "IReference`1".into(), + piid: "61c17706-2d65-11e0-9ae8-d48564015472".into(), + args: vec![TypeMeta::U32], + }; + let view = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: crate::codegen::winrt::python::collections::IVECTOR_VIEW_PIID.into(), + args: vec![runtime_class.clone()], + }; + let context = PythonProjectionContext::standalone([ + runtime_class.type_identity(), + interface.type_identity(), + delegate.type_identity(), + reference.type_identity(), + view.type_identity(), + ]) + .unwrap(); + let stub = AnnotationSurface::Stub; + + for (inner, expected) in [ + (runtime_class.clone(), "list[Widget | None]"), + (interface, "list[IWidget | None]"), + (TypeMeta::Object, "list[DynWinRTValue | None]"), + (delegate, "list[DynWinRTValue | None]"), + (reference, "list[IReference_UInt32 | None]"), + (view, "list[IVectorView_Widget | None]"), + ] { + let annotation = returned(&TypeMeta::Array(Box::new(inner)), stub, &context); + assert_eq!(annotation, expected); + assert!( + !annotation.ends_with("] | None"), + "the array value itself must stay non-null: {annotation}" + ); + } + assert_eq!( + returned( + &TypeMeta::AsyncOperation(Box::new(TypeMeta::Array(Box::new(runtime_class)))), + stub, + &context + ), + "WinRTCoroutine[list[Widget | None]]" + ); + for (inner, expected) in [ + (TypeMeta::String, "list[str]"), + (TypeMeta::Guid, "list[UUID]"), + (TypeMeta::I32, "list[int]"), + (TypeMeta::U8, "bytes"), + ] { + assert_eq!( + returned(&TypeMeta::Array(Box::new(inner)), stub, &context), + expected + ); + } + } + #[test] fn object_inputs_accept_native_wrappers_without_widening_outputs() { let context = PythonProjectionContext::default(); @@ -1093,7 +1169,7 @@ mod tests { assert_eq!(returned(&unknown, stub, &context), "DynWinRTValue"); assert_eq!( returned(&TypeMeta::Array(Box::new(widget.clone())), stub, &context), - "list[Widget]" + "list[Widget | None]" ); assert_eq!( returned(&async_of(&widget), stub, &context), diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 2bb226e6..e8bb229b 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -252,6 +252,39 @@ fn generate_fixture(fixture: &Fixture, package: &str) { params: vec![parameter(TypeMeta::Array(Box::new(TypeMeta::Object)))], ..Default::default() }, + MethodMeta { + name: "GetResources".into(), + raw_name: "GetResources".into(), + vtable_index: 10, + return_type: Some(TypeMeta::Array(Box::new(TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Resource".into(), + default_interface: Some(Box::new(TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IItem".into(), + iid: "00000001-1111-1111-1111-111111111111".into(), + })), + }))), + ..Default::default() + }, + MethodMeta { + name: "GetCounts".into(), + raw_name: "GetCounts".into(), + vtable_index: 11, + return_type: Some(TypeMeta::Array(Box::new(TypeMeta::I32))), + ..Default::default() + }, + MethodMeta { + name: "GetItems".into(), + raw_name: "GetItems".into(), + vtable_index: 12, + return_type: Some(TypeMeta::Array(Box::new(TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IItem".into(), + iid: "00000001-1111-1111-1111-111111111111".into(), + }))), + ..Default::default() + }, ], ); let classes = [ @@ -653,6 +686,98 @@ def invalid_key(guid: IMap_Guid_Object, string: IMap_String_Object) -> None: ); } +#[test] +fn reference_array_results_preserve_null_elements() { + if !has_mypy() { + return; + } + let fixture = Fixture::new(); + generate_fixture(&fixture, "views"); + let source = fs::read_to_string(fixture.0.join("views").join("contoso__content.py")).unwrap(); + assert!(source.contains("def get_resources(self) -> list[Resource | None]:")); + assert!(source.contains("_dynwinrt_wrap_values('contoso__resource', 'Resource'")); + assert!(source.contains("def get_items(self) -> list[IItem | None]:")); + assert!(source.contains("_dynwinrt_wrap_values('contoso__i_item', 'IItem'")); + assert!(source.contains("def get_counts(self) -> list[int]:")); + let valid = r#"from typing import assert_type +from views.contoso__content import Content +from views.contoso__i_item import IItem +from views.contoso__resource import Resource + +def consume(content: Content) -> list[str]: + resources = content.get_resources() + assert_type(resources, list[Resource | None]) + assert_type(content.get_items(), list[IItem | None]) + assert_type(content.get_counts(), list[int]) + return [resource.name for resource in resources if resource is not None] +"#; + typecheck(&fixture, &["views"], valid, &[]); + pyright_typecheck(&fixture, valid, 0); + + let invalid = r#"from views.contoso__content import Content + +def consume(content: Content) -> str: + return content.get_resources()[0].name +"#; + typecheck(&fixture, &["views"], invalid, &["[union-attr]"]); + pyright_typecheck(&fixture, invalid, 1); + + if has_implementation_runtime() { + let script = r#"from dynwinrt import RoApartment, project_as, projected_lifetime_scope +from views.contoso__content import Content +from views.contoso__i_content import IContent + +class ContentHandlers: + def get_content(self): + return None + + def set_content(self, _value): + pass + + def set_object(self, _value): + pass + + def set_objects(self, _value): + pass + + def get_resources(self): + return [None] + + def get_counts(self): + return [1, 2] + + def get_items(self): + return [None] + +with RoApartment(1), projected_lifetime_scope(): + with IContent.implement(ContentHandlers()) as owner: + content = project_as(owner.value, Content) + resources = content.get_resources() + assert isinstance(resources, list) + assert resources == [None] + items = content.get_items() + assert isinstance(items, list) + assert items == [None] + counts = content.get_counts() + assert isinstance(counts, list) + assert counts == [1, 2] +print("nullable-reference-array-native-ok", flush=True) +"#; + fs::write(fixture.0.join("reference_array_runtime.py"), script).unwrap(); + let output = Command::new(python()) + .args(["-B", "reference_array_runtime.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + assert!( + String::from_utf8_lossy(&output.stdout).contains("nullable-reference-array-native-ok"), + "{}", + diagnostics(&output) + ); + } +} + #[test] fn real_windows_consumers_accept_file_stream_content_and_composition_instances() { let winmd = Path::new( @@ -1577,6 +1702,15 @@ class ContentHandlers: def set_objects(self, value: list[DynWinRTValue | None]) -> None: self.items = value + def get_resources(self): + return [] + + def get_counts(self): + return [] + + def get_items(self): + return [] + class WrongObject: _obj = 42 diff --git a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs index 59c0896d..e095a9e8 100644 --- a/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs +++ b/tools/dynwinrt-codegen/tests/python_stub_nullability_test.rs @@ -5,13 +5,17 @@ //! from the projection are non-null in `.pyi` stubs by default, while //! `IReference`, `Try*` results, members documented to return null, //! `Object` and delegate values keep `| None`. Collection elements follow the -//! mutability of the collection holding them. Inputs, implementation -//! protocols and the runtime `.py` annotations are unchanged. +//! native nullable-reference contract. Inputs, implementation protocols and +//! the runtime `.py` annotations are unchanged. use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use dynwinrt_codegen::codegen::{python, python_stub}; +use dynwinrt_codegen::meta::{InterfaceMeta, MethodMeta}; +use dynwinrt_codegen::types::TypeMeta; + const WINDOWS_WINMD: &str = r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; @@ -67,6 +71,113 @@ fn assert_contains(text: &str, expected: &str) { assert!(text.contains(expected), "missing `{expected}`"); } +#[test] +fn ordinary_reference_array_stub_elements_are_nullable() { + let widget = TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + }; + let widget_interface = TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IWidget".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + }; + let handler = TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "WidgetHandler".into(), + iid: "22222222-2222-2222-2222-222222222222".into(), + }; + let reference = TypeMeta::Parameterized { + namespace: "Windows.Foundation".into(), + name: "IReference`1".into(), + piid: "61c17706-2d65-11e0-9ae8-d48564015472".into(), + args: vec![TypeMeta::U32], + }; + let view = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56".into(), + args: vec![widget.clone()], + }; + let method = |name: &str, typ: TypeMeta, slot: usize| MethodMeta { + name: name.into(), + raw_name: name.into(), + vtable_index: slot, + return_type: Some(typ), + ..Default::default() + }; + let source = InterfaceMeta { + namespace: "Contoso".into(), + name: "IArraySource".into(), + iid: "33333333-3333-3333-3333-333333333333".into(), + methods: vec![ + method("GetWidgets", TypeMeta::Array(Box::new(widget.clone())), 6), + method( + "GetInterfaces", + TypeMeta::Array(Box::new(widget_interface.clone())), + 7, + ), + method("GetObjects", TypeMeta::Array(Box::new(TypeMeta::Object)), 8), + method( + "GetDelegates", + TypeMeta::Array(Box::new(handler.clone())), + 9, + ), + method( + "GetReferences", + TypeMeta::Array(Box::new(reference.clone())), + 10, + ), + method("GetViews", TypeMeta::Array(Box::new(view.clone())), 11), + method( + "GetWidgetsAsync", + TypeMeta::AsyncOperation(Box::new(TypeMeta::Array(Box::new(widget.clone())))), + 12, + ), + method( + "GetStrings", + TypeMeta::Array(Box::new(TypeMeta::String)), + 13, + ), + method("GetGuids", TypeMeta::Array(Box::new(TypeMeta::Guid)), 14), + method("GetNumbers", TypeMeta::Array(Box::new(TypeMeta::I32)), 15), + method("GetBytes", TypeMeta::Array(Box::new(TypeMeta::U8)), 16), + ], + ..Default::default() + }; + let context = python::PythonProjectionContext::standalone([ + source.type_identity(), + widget.type_identity(), + widget_interface.type_identity(), + handler.type_identity(), + reference.type_identity(), + view.type_identity(), + ]) + .unwrap(); + let stub = python_stub::generate_interface_stub(&context, &source); + + for expected in [ + "def get_widgets(self) -> list[Widget | None]: ...", + "def get_interfaces(self) -> list[IWidget | None]: ...", + "def get_objects(self) -> list[DynWinRTValue | None]: ...", + "def get_delegates(self) -> list[DynWinRTValue | None]: ...", + "def get_references(self) -> list[IReference_UInt32 | None]: ...", + "def get_views(self) -> list[IVectorView_Widget | None]: ...", + "def get_widgets_async(self) -> WinRTCoroutine[list[Widget | None]]: ...", + "def get_strings(self) -> list[str]: ...", + "def get_guids(self) -> list[UUID]: ...", + "def get_numbers(self) -> list[int]: ...", + "def get_bytes(self) -> bytes: ...", + ] { + assert_contains(&stub, expected); + } + assert!( + !stub.contains("list[Widget | None] | None"), + "array values must remain non-null" + ); +} + #[test] fn stub_outputs_follow_the_nullability_policy() { let Some(generated) = Generated::new( From bbe613edc0b0efb8442285d06a3ca73741fd376e Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 19:30:32 +0800 Subject: [PATCH 10/12] Cover generated PropertyValue array projections 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> --- tests/e2e/runners/py_runner.py | 91 ++++++++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index e7b42e51..5bc94b79 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -448,17 +448,31 @@ async def run_check( elif kind == 'nullable_object_array_roundtrip': uri_cls = generated_type(pkg_name, 'Uri') property_value_cls = generated_type(pkg_name, 'IPropertyValue') + property_type_cls = generated_type(pkg_name, 'PropertyType') uri = uri_cls.create_uri('https://example.com/null-array') - boxed = getattr(cls, member)( - [dw.DynWinRTValue.null_value(), uri._obj] - ) + + boxed = getattr(cls, member)([dw.DynWinRTValue.null_value(), uri._obj]) if boxed is None: - cr['error'] = 'CreateInspectableArray returned None' + cr['error'] = 'create_inspectable_array returned None' return cr - # Cross the generated interface method and its array converter, - # not a raw call_0/manual as_array path. - property_value = property_value_cls.from_value(boxed) - values = property_value.get_inspectable_array() + try: + property_value = property_value_cls.from_value(boxed) + finally: + boxed.release() + try: + if property_value.type != property_type_cls.InspectableArray: + cr['error'] = ( + 'inspectable array reported property type ' + f'{property_value.type!r}' + ) + return cr + if property_value.is_numeric_scalar: + cr['error'] = 'inspectable array reported a numeric scalar' + return cr + values = property_value.get_inspectable_array() + finally: + dw.release_projected(property_value) + if len(values) != 2: cr['error'] = f'expected 2 inspectable values, got {len(values)}' elif values[0] is not None: @@ -469,9 +483,66 @@ async def run_check( cr['error'] = 'inspectable array element lost COM identity' else: cr['pass'] = True - if values[1] is not None: + if len(values) > 1 and values[1] is not None: values[1].release() - dw.release_projected(property_value) + if not cr['pass']: + return cr + + def unused(_self): + raise AssertionError('unexpected IPropertyValue callback') + + value_names = ( + 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', + 'uint64', 'single', 'double', 'char16', 'boolean', 'string', + 'guid', 'date_time', 'time_span', 'point', 'size', 'rect', + ) + callbacks = { + f'get_{name}': unused + for name in value_names + } + callbacks.update({ + f'get_{name}_array': unused + for name in value_names + }) + callbacks.update({ + 'get_type': lambda _self: property_type_cls.Int32, + 'get_is_numeric_scalar': lambda _self: True, + 'get_int32_array': lambda _self: [1, -2], + 'get_inspectable_array': lambda _self: [None, uri._obj], + }) + handlers = type('PropertyValueHandlers', (), callbacks)() + with property_value_cls.implement(handlers) as implementation: + view = implementation.value + if view.type != property_type_cls.Int32: + cr['error'] = 'implemented property value returned the wrong type' + return cr + if not view.is_numeric_scalar: + cr['error'] = 'implemented property value was not numeric' + return cr + if view.get_int32_array() != [1, -2]: + cr['error'] = 'implemented Int32 array did not round-trip' + return cr + values = view.get_inspectable_array() + try: + if len(values) != 2 or values[0] is not None: + cr['error'] = ( + 'implemented inspectable array did not preserve null' + ) + return cr + if values[1] is None: + cr['error'] = ( + 'implemented inspectable array lost its object' + ) + return cr + if values[1].identity_raw() != uri._obj.identity_raw(): + cr['error'] = ( + 'implemented inspectable array lost COM identity' + ) + return cr + finally: + if len(values) > 1 and values[1] is not None: + values[1].release() + cr['pass'] = True elif kind == 'projection_identity': import weakref From 2b4077c6e3ee85d2394bba8e62ab91a7a8dad4fb Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Mon, 28 Sep 2026 22:09:21 +0800 Subject: [PATCH 11/12] Avoid Python collection helper name collisions 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> --- .github/workflows/build.yml | 4 + .../codegen/winrt/python/generator/class.rs | 3 +- .../src/codegen/winrt/python/generator/mod.rs | 10 +- .../codegen/winrt/python/generator/types.rs | 85 +++- .../src/codegen/winrt/python/naming.rs | 9 + .../src/codegen/winrt/python/signature.rs | 5 +- ...python_collection_helper_collision_test.rs | 445 ++++++++++++++++++ 7 files changed, 554 insertions(+), 7 deletions(-) create mode 100644 tools/dynwinrt-codegen/tests/python_collection_helper_collision_test.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 926d8c94..20ae17a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -554,11 +554,15 @@ jobs: run: | $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' + $env:DYNWINRT_REQUIRE_MYPY = '1' cargo test -p dynwinrt-codegen --test python_consumer_typing_test ` mutable_collection_mutators_accept_none -- --exact if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cargo test -p dynwinrt-codegen --test python_consumer_typing_test ` reference_array_results_preserve_null_elements -- --exact + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo test -p dynwinrt-codegen --test python_collection_helper_collision_test ` + collection_helper_aliases_execute_for_packaged_and_standalone_outputs -- --exact - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs index 64fbfc38..22bf3af1 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -65,7 +65,8 @@ pub fn generate_class( out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); out.push_str(&import_line(context)); - out.push_str(collection_item_import( + out.push_str(&collection_item_import( + context, class .all_interfaces() .flat_map(|interface| interface.methods.iter()), diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index ef3d090c..874911c4 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -63,9 +63,10 @@ from ._runtime import ( } fn collection_item_import<'a>( + context: &PythonProjectionContext, methods: impl IntoIterator, collection_interface: bool, -) -> &'static str { +) -> String { let method_uses_helper = methods.into_iter().any(|method| { !method.collection_inputs.is_empty() || method.params.iter().any(|parameter| { @@ -74,9 +75,12 @@ fn collection_item_import<'a>( }) }); if collection_interface || method_uses_helper { - "from ._runtime import _dynwinrt_collection_item\n" + format!( + "from ._runtime import {}\n", + context.support_symbol_import(PythonSupportSymbol::CollectionItem) + ) } else { - "" + String::new() } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 98212ce7..0d6ddf2f 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -94,7 +94,8 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); out.push_str(&import_line(context)); - out.push_str(collection_item_import( + out.push_str(&collection_item_import( + context, iface.methods.iter(), interface_kind(iface).is_some(), )); @@ -774,6 +775,88 @@ mod tests { )); } + #[test] + fn collection_factories_alias_a_colliding_item_helper() { + let colliding = TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "_dynwinrt_collection_item".into(), + default_interface: None, + }; + let vector = InterfaceMeta { + name: "IVector_Collision".into(), + iid: "913337e9-11a1-4345-a3a2-4e7f956e222d".into(), + generic_piid: Some("913337e9-11a1-4345-a3a2-4e7f956e222d".into()), + generic_args: vec![colliding.clone()], + methods: vec![MethodMeta { + name: "Append".into(), + raw_name: "Append".into(), + vtable_index: 13, + params: vec![crate::meta::ParamMeta { + name: "value".into(), + typ: colliding.clone(), + direction: ParamDirection::In, + }], + collection_inputs: vec![(0, crate::meta::CollectionInputRole::Element)], + ..Default::default() + }], + ..Default::default() + }; + let map = InterfaceMeta { + name: "IMap_Collision_Collision".into(), + iid: "3c2925fe-8519-45c1-aa79-197b6718c1c1".into(), + generic_piid: Some("3c2925fe-8519-45c1-aa79-197b6718c1c1".into()), + generic_args: vec![colliding.clone(), colliding.clone()], + methods: vec![MethodMeta { + name: "Insert".into(), + raw_name: "Insert".into(), + vtable_index: 10, + params: vec![ + crate::meta::ParamMeta { + name: "key".into(), + typ: colliding.clone(), + direction: ParamDirection::In, + }, + crate::meta::ParamMeta { + name: "value".into(), + typ: colliding.clone(), + direction: ParamDirection::In, + }, + ], + return_type: Some(TypeMeta::Bool), + collection_inputs: vec![ + (0, crate::meta::CollectionInputRole::Key), + (1, crate::meta::CollectionInputRole::Value), + ], + ..Default::default() + }], + ..Default::default() + }; + let interfaces = [vector, map]; + let context = PythonProjectionContext::packaged( + interfaces + .iter() + .map(InterfaceMeta::type_identity) + .chain([colliding.type_identity()]), + ) + .unwrap(); + + for interface in &interfaces { + let code = generate_interface(&context, interface); + assert!( + code.contains( + "from ._runtime import _dynwinrt_collection_item as _dynwinrt_collection_item_2" + ), + "{code}" + ); + assert!(code.contains("def create("), "{code}"); + assert!(code.contains("_dynwinrt_collection_item_2(item,"), "{code}"); + assert!( + !code.contains("lambda item: _dynwinrt_collection_item(item,"), + "{code}" + ); + } + } + #[test] fn map_factory_key_nullability_follows_the_declared_key_type() { let reference_key = TypeMeta::Interface { diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs index ae2ab424..286e01d1 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs @@ -18,6 +18,7 @@ pub type PythonTypeIdentity = TypeIdentity; pub(crate) enum PythonSupportSymbol { ObjectInput, AsInterface, + CollectionItem, } impl PythonSupportSymbol { @@ -25,6 +26,7 @@ impl PythonSupportSymbol { match self { Self::ObjectInput => "_DynWinRTObject", Self::AsInterface => "_dynwinrt_as_interface", + Self::CollectionItem => "_dynwinrt_collection_item", } } } @@ -917,6 +919,7 @@ impl PythonProjectionContext { for helper in [ PythonSupportSymbol::ObjectInput, PythonSupportSymbol::AsInterface, + PythonSupportSymbol::CollectionItem, ] { let preferred = helper.name(); let mut name = preferred.to_string(); @@ -1444,6 +1447,7 @@ mod tests { let others = [ PythonSupportSymbol::ObjectInput, PythonSupportSymbol::AsInterface, + PythonSupportSymbol::CollectionItem, ] .into_iter() .filter(|other| *other != helper) @@ -1504,6 +1508,11 @@ mod tests { assert_support_helper_yields_to_visible_roles(PythonSupportSymbol::AsInterface); } + #[test] + fn collection_item_helper_yields_to_visible_roles_without_renaming_metadata() { + assert_support_helper_yields_to_visible_roles(PythonSupportSymbol::CollectionItem); + } + #[test] fn companion_aliases_freeze_roles_and_use_only_visible_symbols() { let owner = TypeIdentity::named(TypeIdentityKind::Class, "Audit", "Widget"); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 9510b843..e291b6a7 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -6,7 +6,7 @@ use crate::meta::{CollectionInputRole, InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::{TypeIdentity, TypeIdentityKind, TypeMeta}; -use super::naming::{PythonProjectionContext, PythonSymbol}; +use super::naming::{PythonProjectionContext, PythonSupportSymbol, PythonSymbol}; use crate::codegen::winrt::python::collections::{CollectionKind, is_mapping_input, type_kind}; use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; use crate::codegen::winrt::shared::imports::ireference_inner_type; @@ -498,7 +498,8 @@ pub(crate) fn py_wrap_collection_item( CollectionInputRole::Value => "map value", }; format!( - "_dynwinrt_collection_item({name}, lambda item: {}, {}, '{label}')", + "{}({name}, lambda item: {}, {}, '{label}')", + context.support_symbol_reference(PythonSupportSymbol::CollectionItem), py_wrap_arg("item", typ, context), if allow_none { "True" } else { "False" } ) diff --git a/tools/dynwinrt-codegen/tests/python_collection_helper_collision_test.rs b/tools/dynwinrt-codegen/tests/python_collection_helper_collision_test.rs new file mode 100644 index 00000000..6bc676be --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_collection_helper_collision_test.rs @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use dynwinrt_codegen::codegen::python::{self, PythonProjectionContext}; +use dynwinrt_codegen::meta::{self, ClassMeta, InterfaceMeta}; +use dynwinrt_codegen::types::{TypeIdentity, TypeIdentityKind}; +use windows_metadata::{MethodCallAttributes, Signature, Type, TypeAttributes, TypeName, writer}; + +const WINDOWS_WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; +const COLLIDING: &str = "_dynwinrt_collection_item"; +const HELPER_ALIAS: &str = "_dynwinrt_collection_item_2"; +const COLLECTIONS: &str = "Windows.Foundation.Collections"; + +static NEXT: AtomicU64 = AtomicU64::new(0); + +struct Fixture(PathBuf); + +impl Fixture { + fn new() -> Self { + let directory = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join(format!( + "python-collection-helper-collision-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + fs::create_dir_all(&directory).unwrap(); + Self(directory) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn python() -> PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("python")) +} + +fn success(output: Output) { + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +fn runtime_available() -> bool { + let available = Command::new(python()) + .args([ + "-c", + "from dynwinrt import DynWinRTValue; assert hasattr(DynWinRTValue, 'create_vector')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + available || std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref() != Ok("1"), + "the collection-helper collision probe requires the current Python binding" + ); + if !available { + eprintln!("Skipping the collection-helper runtime probe; set DYNWINRT_TEST_PYTHON."); + } + available +} + +fn closed(namespace: &str, name: &str, generics: Vec) -> Type { + Type::Name(TypeName { + namespace: namespace.into(), + name: name.into(), + generics, + }) +} + +fn default_attribute(file: &mut writer::File, implementation: writer::InterfaceImpl) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "DefaultAttribute"); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + writer::MemberRefParent::TypeRef(attribute), + ); + file.Attribute( + writer::HasAttribute::InterfaceImpl(implementation), + writer::AttributeType::MemberRef(constructor), + &[], + ); +} + +fn collection_class(file: &mut writer::File, namespace: &str, interface: Type) { + let object = file.TypeRef("System", "Object"); + let class = file.TypeDef( + namespace, + COLLIDING, + writer::TypeDefOrRef::TypeRef(object), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + let implementation = file.InterfaceImpl(class, &interface); + default_attribute(file, implementation); +} + +fn collision_metadata(path: &Path) { + let mut file = writer::File::new("PythonCollectionHelperCollision"); + collection_class( + &mut file, + "Audit.Vector", + closed(COLLECTIONS, "IVector`1", vec![Type::Object]), + ); + collection_class( + &mut file, + "Audit.Map", + closed(COLLECTIONS, "IMap`2", vec![Type::Object, Type::Object]), + ); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, file.into_stream()).unwrap(); +} + +fn metadata_set(custom: &Path) -> String { + format!("{};{WINDOWS_WINMD}", custom.display()) +} + +fn parse_classes(custom: &Path) -> Vec { + ["Audit.Vector", "Audit.Map"] + .into_iter() + .map(|namespace| { + meta::parse_class(&metadata_set(custom), namespace, COLLIDING) + .unwrap_or_else(|| panic!("parse {namespace}.{COLLIDING}")) + }) + .collect() +} + +fn class_identity(class: &ClassMeta) -> TypeIdentity { + TypeIdentity::named(TypeIdentityKind::Class, &class.namespace, &class.name) +} + +fn projection_context(classes: &[ClassMeta], packaged: bool) -> PythonProjectionContext { + let identities = classes.iter().map(class_identity).chain( + classes + .iter() + .flat_map(ClassMeta::all_interfaces) + .map(InterfaceMeta::type_identity), + ); + PythonProjectionContext::new(identities, packaged).unwrap() +} + +fn module_name(context: &PythonProjectionContext, namespace: &str) -> String { + context.implementation_module_for_named(TypeIdentityKind::Class, namespace, COLLIDING) +} + +fn assert_collision_safe(source: &str, operation: &str) { + assert!( + source.contains( + "from ._runtime import _dynwinrt_collection_item as _dynwinrt_collection_item_2" + ), + "{source}" + ); + assert!(source.contains(&format!("class {COLLIDING}(")), "{source}"); + assert!( + source.contains(&format!("{HELPER_ALIAS}(")), + "missing aliased helper in {operation}:\n{source}" + ); + assert!( + !source.contains("from ._runtime import _dynwinrt_collection_item\n"), + "{source}" + ); +} + +struct Modules { + vector_class: String, + map_class: String, +} + +fn modules(context: &PythonProjectionContext) -> Modules { + Modules { + vector_class: module_name(context, "Audit.Vector"), + map_class: module_name(context, "Audit.Map"), + } +} + +fn write_standalone(root: &Path, classes: &[ClassMeta]) -> Modules { + let package = root.join("pyviews"); + fs::create_dir_all(&package).unwrap(); + fs::write(package.join("__init__.py"), "").unwrap(); + fs::write( + package.join("_runtime.py"), + python::generate_runtime_support_module(), + ) + .unwrap(); + + let context = projection_context(classes, false); + for class in classes { + let module = context.implementation_module(&class_identity(class)); + let source = python::generate_class(&context, class, &Default::default()); + assert_collision_safe(&source, &module); + fs::write(package.join(format!("{module}.py")), source).unwrap(); + } + modules(&context) +} + +fn generate_packaged(root: &Path, custom: &Path, classes: &[ClassMeta]) -> Modules { + let package = root.join("pyviews"); + success( + Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(custom) + .args(["--ref", WINDOWS_WINMD, "--output"]) + .arg(&package) + .args([ + "--lang", + "py", + "--class-name", + "Audit.Vector._dynwinrt_collection_item,Audit.Map._dynwinrt_collection_item", + ]) + .output() + .unwrap(), + ); + let context = projection_context(classes, true); + let modules = modules(&context); + for (module, operation) in [ + (&modules.vector_class, "vector operations"), + (&modules.map_class, "map operations"), + ] { + let source = fs::read_to_string(package.join(format!("{module}.py"))).unwrap(); + assert_collision_safe(&source, operation); + } + modules +} + +fn runtime_probe(root: &Path, modules: &Modules) { + fs::write( + root.join("probe.py"), + format!( + r#" +import importlib +import os +import dynwinrt as dw + +support = importlib.import_module("pyviews._runtime") +vector_module = importlib.import_module("pyviews.{vector_class}") +map_module = importlib.import_module("pyviews.{map_class}") +Vector = vector_module.{COLLIDING} +Map = map_module.{COLLIDING} + +if "DYNWINRT_EXPECT_OLD_COLLECTION_HELPER" not in os.environ: + for module in (vector_module, map_module): + assert module.{HELPER_ALIAS} is support._dynwinrt_collection_item + assert module.{COLLIDING} is not support._dynwinrt_collection_item + +with dw.RoApartment(1), dw.projected_lifetime_scope(): + live = dw.DynWinRTValue.activation_factory("Windows.Foundation.Uri") + + vector = Vector._from_native( + dw.DynWinRTValue.create_vector([], dw.DynWinRTType.object()) + ) + vector.append(None) + vector.append(live) + assert vector[0] is None + item = vector[1] + assert item is not None and item.identity_raw() == live.identity_raw() + item.release() + vector.replace_all([live, None]) + assert vector[1] is None + + mapping = Map._from_native( + dw.DynWinRTValue.create_map( + [], [], dw.DynWinRTType.object(), dw.DynWinRTType.object() + ) + ) + assert mapping.insert(None, None) is False + assert mapping.lookup(None) is None + assert mapping.insert(live, live) is False + value = mapping.lookup(live) + assert value is not None and value.identity_raw() == live.identity_raw() + value.release() + + live.release() + +print("collection-helper-collision-ok") +"#, + vector_class = modules.vector_class, + map_class = modules.map_class, + ), + ) + .unwrap(); + let output = Command::new(python()) + .args(["-B", "probe.py"]) + .current_dir(root) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&output.stdout).contains("collection-helper-collision-ok"), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + success(output); +} + +fn old_hardcoded_binding_fails(root: &Path, modules: &Modules) { + for module in [&modules.vector_class, &modules.map_class] { + let path = root.join("pyviews").join(format!("{module}.py")); + let source = fs::read_to_string(&path).unwrap(); + let old = source + .replace( + "from ._runtime import _dynwinrt_collection_item as _dynwinrt_collection_item_2", + "from ._runtime import _dynwinrt_collection_item", + ) + .replace("_dynwinrt_collection_item_2(", "_dynwinrt_collection_item("); + fs::write(path, old).unwrap(); + } + let output = Command::new(python()) + .args(["-B", "probe.py"]) + .current_dir(root) + .env("DYNWINRT_EXPECT_OLD_COLLECTION_HELPER", "1") + .output() + .unwrap(); + assert!( + !output.status.success(), + "old hardcoded helper unexpectedly passed" + ); + assert!( + String::from_utf8_lossy(&output.stderr).contains("TypeError"), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +fn strict_typecheck(root: &Path, modules: &Modules) { + let available = Command::new(python()) + .args(["-m", "mypy", "--version"]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + available || std::env::var("DYNWINRT_REQUIRE_MYPY").as_deref() != Ok("1"), + "DYNWINRT_REQUIRE_MYPY=1 but mypy is unavailable" + ); + if !available { + eprintln!("Skipping collection-helper strict typing: mypy unavailable."); + return; + } + let consumer = format!( + r#" +from dynwinrt import DynWinRTValue +from pyviews.{vector_class} import {COLLIDING} as Vector +from pyviews.{map_class} import {COLLIDING} as Map + +def vectors(vector: Vector, value: DynWinRTValue) -> None: + vector.append(None) + vector.append(value) + vector.replace_all([None, value]) + +def maps(mapping: Map, value: DynWinRTValue) -> None: + mapping.insert(None, None) + mapping.insert(value, value) +"#, + vector_class = modules.vector_class, + map_class = modules.map_class, + ); + fs::write(root.join("typing_probe.py"), &consumer).unwrap(); + let binding_stubs = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("bindings") + .join("py") + .canonicalize() + .unwrap(); + success( + Command::new(python()) + .args([ + "-B", + "-m", + "mypy", + "--strict", + "--follow-imports=silent", + "--no-incremental", + "--cache-dir", + "mypy-cache", + "typing_probe.py", + ]) + .current_dir(root) + .env( + "MYPYPATH", + std::env::join_paths([binding_stubs, root.to_path_buf()]).unwrap(), + ) + .output() + .unwrap(), + ); + if let Some(pyright) = std::env::var_os("DYNWINRT_PYRIGHT") { + fs::write( + root.join("pyright_probe.py"), + format!("# pyright: strict, reportPrivateUsage=false\n{consumer}"), + ) + .unwrap(); + success( + Command::new(pyright) + .args(["--pythonpath"]) + .arg(python()) + .arg("pyright_probe.py") + .current_dir(root) + .output() + .unwrap(), + ); + } +} + +#[test] +fn collection_helper_aliases_execute_for_packaged_and_standalone_outputs() { + if !Path::new(WINDOWS_WINMD).is_file() { + eprintln!("Skipping collection-helper collision test: Windows.winmd unavailable."); + return; + } + let fixture = Fixture::new(); + let custom = fixture.0.join("metadata").join("Collision.winmd"); + collision_metadata(&custom); + let classes = parse_classes(&custom); + assert_eq!(classes.len(), 2); + + let standalone = fixture.0.join("standalone"); + let modules = write_standalone(&standalone, &classes); + if runtime_available() { + runtime_probe(&standalone, &modules); + old_hardcoded_binding_fails(&standalone, &modules); + } + + let packaged = fixture.0.join("packaged"); + let modules = generate_packaged(&packaged, &custom, &classes); + strict_typecheck(&packaged, &modules); + if runtime_available() { + runtime_probe(&packaged, &modules); + } +} From e17b94be88a7240e15fc9196403ba8d018f8a2dc Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 29 Sep 2026 12:06:00 +0800 Subject: [PATCH 12/12] Preserve nullable delegate arguments in Python callbacks Keep nested delegate callback stubs aligned with native null projection and avoid importing delegate types without Python classes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 3 + bindings/py/README.md | 13 +- .../src/codegen/winrt/python/delegates.rs | 11 +- .../src/codegen/winrt/python/mod.rs | 14 +- .../src/codegen/winrt/python/nullability.rs | 9 +- .../tests/python_delegate_callback_test.rs | 418 ++++++++++++++++++ 6 files changed, 449 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20ae17a0..e9972c52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -563,6 +563,9 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } cargo test -p dynwinrt-codegen --test python_collection_helper_collision_test ` collection_helper_aliases_execute_for_packaged_and_standalone_outputs -- --exact + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + cargo test -p dynwinrt-codegen --test python_delegate_callback_test ` + nested_delegate_callback_argument_preserves_native_null -- --exact --nocapture - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/bindings/py/README.md b/bindings/py/README.md index 82cb5893..0923729c 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -156,7 +156,9 @@ parameter such as `ThreadPool.run_async(handler)`, or a delegate-typed property) receives the delegate's arguments as projected Python values, typed from the delegate's `Invoke` signature. WinRT `Object` arguments stay `DynWinRTValue | None`, and `IReference` arguments are native values or -`None`. Async-operation arguments stay raw `DynWinRTValue` objects so a +`None`. Delegate-typed callback arguments are raw `DynWinRTValue | None` +because a null native delegate is passed to the callable as `None`. +Async-operation arguments stay raw `DynWinRTValue` objects so a callback projection cannot take over or cancel the operation's completion. For example, `map_changed` handlers of `PropertySet`, `StringMap`, `ValueSet`, and other `IObservableMap` implementations receive the @@ -174,10 +176,11 @@ value passed to a runtime class is reserved for wrapping an existing native instance before constructor overload dispatch. Keep the delegate object for a constructor, or pass the raw delegate to a named factory/method instead. -Callback parameter annotations are non-null by default, matching generated -method-output typing. This is an intentionally optimistic typing policy, not a -guarantee from the `Invoke` metadata: WinMD carries no nullability information, -and the runtime still passes `None` when WinRT supplies a null reference. +Callback parameter annotations are non-null by default except for `Object`, +`IReference`, and delegate-typed arguments. This is an intentionally +optimistic typing policy, not a guarantee from the `Invoke` metadata: WinMD +carries no nullability information, and the runtime still passes `None` when +WinRT supplies a null reference. Precise callback signatures live in the generated `.pyi` contract. Executable `.py` methods use the cycle-safe runtime annotation `Callable[..., object] | DynWinRTValue | DynWinRtDelegate`, so diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/delegates.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/delegates.rs index a68dd78e..c8221c1e 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/delegates.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/delegates.rs @@ -63,12 +63,11 @@ fn callback_params<'a>( /// Annotation of one argument passed to a Python callback. /// -/// Callback arguments are annotated as non-null, except WinRT `Object` and -/// `IReference`. WinMD metadata does not record nullability, so this is an -/// optimistic policy shared with method outputs: the runtime still passes -/// `None` for a null reference. Every callback-argument annotation goes through -/// this function so a position-aware output-nullability policy can take it -/// over. +/// Callback arguments are annotated as non-null, except WinRT `Object`, +/// `IReference`, and delegate-typed raw values. WinMD metadata does not +/// record nullability, so this is an optimistic policy shared with method +/// outputs: the runtime still passes `None` for a null reference. Every +/// callback-argument annotation goes through the central output policy. pub(crate) fn py_delegate_argument_type( typ: &TypeMeta, context: &PythonProjectionContext, diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs index d9b3c80c..4e12f391 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs @@ -95,8 +95,18 @@ pub(crate) fn collect_referenced_delegate_names( result: &mut std::collections::HashSet, ) { use crate::types::TypeMeta; - if context.is_delegate_type(typ) { - result.insert(context.identity_for_type(typ)); + if context.is_delegate_type(typ) + && result.insert(context.identity_for_type(typ)) + && let Some(invoke) = context.delegate_invoke(typ) + { + // Invoke arguments may themselves be delegates, whose stub modules + // export ABI constants rather than a Python class to import. + for parameter in &invoke.params { + collect(¶meter.typ, context, result); + } + if let Some(return_type) = &invoke.return_type { + collect(return_type, context, result); + } } match typ { TypeMeta::AsyncActionWithProgress(inner) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs index f72af0cb..23552453 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/nullability.rs @@ -230,9 +230,7 @@ fn stub_output_admits_none( site: OutputSite, context: &PythonProjectionContext, ) -> bool { - use OutputPosition::{ - Activation, AsyncResult, CallbackParam, CollectionElement, OutParam, Property, Return, - }; + use OutputPosition::{Activation, AsyncResult, CollectionElement, OutParam, Property, Return}; if site.container == Some(ElementContainer::MapKey) { // output_admits_none only reaches this branch after @@ -247,7 +245,7 @@ fn stub_output_admits_none( } // Delegate-typed values are raw handles that are null while unset. if context.is_delegate_type(typ) { - return site.position != CallbackParam; + return true; } // `Try*` members report "not found" through a null result, and the // Windows SDK documentation names the other members that return null. @@ -386,9 +384,8 @@ mod tests { let site = OutputSite::of(position); assert!(admits(&nullable_u32(), site, AnnotationSurface::Stub)); assert!(admits(&TypeMeta::Object, site, AnnotationSurface::Stub)); - assert_eq!( + assert!( admits(&handler(), site, AnnotationSurface::Stub), - position != OutputPosition::CallbackParam, "{position:?}" ); } diff --git a/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs b/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs index fd2a2e0f..ce5dd909 100644 --- a/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs +++ b/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs @@ -14,6 +14,10 @@ use std::sync::atomic::{AtomicU64, Ordering}; use dynwinrt_codegen::meta::{ClassMeta, InterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; use dynwinrt_codegen::types::{FieldMeta, TypeMeta}; +use windows_metadata::{ + MethodAttributes, MethodCallAttributes, MethodImplAttributes, ParamAttributes, Signature, Type, + TypeAttributes, Value, writer, +}; const WINDOWS_WINMD: &str = r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; @@ -75,6 +79,420 @@ fn class_wrapper(module: &str, class: &str, argument: &str) -> String { ) } +fn guid(file: &mut writer::File, definition: writer::TypeDef, id: u32) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "GuidAttribute"); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![ + Type::U32, + Type::U16, + Type::U16, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + ], + }, + writer::MemberRefParent::TypeRef(attribute), + ); + let values = [ + Value::U32(id), + Value::U16(0x6281), + Value::U16(0x4900), + Value::U8(0xb7), + Value::U8(0x82), + Value::U8(4), + Value::U8(3), + Value::U8(2), + Value::U8(1), + Value::U8(9), + Value::U8(0x10), + ] + .into_iter() + .map(|value| (String::new(), value)) + .collect::>(); + file.Attribute( + writer::HasAttribute::TypeDef(definition), + writer::AttributeType::MemberRef(constructor), + &values, + ); +} + +fn write_nested_delegate_metadata(path: &Path) { + let mut file = writer::File::new("NestedDelegateCallbacks"); + let base = file.TypeRef("System", "MulticastDelegate"); + for (name, id, argument) in [ + ("InnerHandler", 0x31d447a1, None), + ( + "OuterHandler", + 0x31d447a2, + Some(Type::named("Audit", "InnerHandler")), + ), + ] { + let definition = file.TypeDef( + "Audit", + name, + writer::TypeDefOrRef::TypeRef(base), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + guid(&mut file, definition, id); + file.MethodDef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + MethodAttributes::Public | MethodAttributes::SpecialName, + MethodImplAttributes::default(), + ); + file.MethodDef( + "Invoke", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: argument.iter().cloned().collect(), + }, + MethodAttributes::Public | MethodAttributes::Virtual | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + if argument.is_some() { + file.Param("inner", 1, ParamAttributes::In); + } + } + let emitter = file.TypeDef( + "Audit", + "IEmitter", + writer::TypeDefOrRef::default(), + TypeAttributes::Public + | TypeAttributes::Interface + | TypeAttributes::Abstract + | TypeAttributes::WindowsRuntime, + ); + guid(&mut file, emitter, 0x31d447a3); + file.MethodDef( + "SetHandler", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![Type::named("Audit", "OuterHandler")], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("handler", 1, ParamAttributes::In); + fs::write(path, file.into_stream()).unwrap(); +} + +fn generate_nested_delegate_views(binary: &Path, metadata: &Path, generated: &Path) { + let output = Command::new(binary) + .args(["generate", "--winmd"]) + .arg(metadata) + .args(["--namespace", "Audit", "--lang", "py", "--output"]) + .arg(generated) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn nested_delegate_callback_argument_preserves_native_null() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("target") + .join(format!( + "nd{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&root).unwrap(); + let fixture = Output(root); + let metadata = fixture.0.join("Nested.winmd"); + write_nested_delegate_metadata(&metadata); + let generated = fixture.0.join("projected"); + generate_nested_delegate_views( + Path::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")), + &metadata, + &generated, + ); + let stub = fs::read_to_string(generated.join("audit__i_emitter.pyi")).unwrap(); + let signature = stub + .lines() + .rev() + .find(|line| line.contains("def set_handler(")) + .expect("generated IEmitter.set_handler signature"); + assert_eq!( + signature, + " def set_handler(self, handler: Callable[[DynWinRTValue | None], object] | \ + 'DynWinRTValue | DynWinRtDelegate') -> None: ..." + ); + assert!( + !stub.contains("from .audit__inner_handler import IID_InnerHandler, InnerHandler"), + "{stub}" + ); + let runtime = fs::read_to_string(generated.join("audit__i_emitter.py")).unwrap(); + let null_projection = + "lambda __p0__: ((lambda value: None if value.is_null() else value)(__p0__),)"; + assert!(runtime.contains(null_projection), "{runtime}"); + + if let Some(main_codegen) = std::env::var_os("DYNWINRT_MAIN_CODEGEN") { + let baseline = fixture.0.join("main"); + generate_nested_delegate_views(Path::new(&main_codegen), &metadata, &baseline); + let main_stub = fs::read_to_string(baseline.join("audit__i_emitter.pyi")).unwrap(); + let main_signature = main_stub + .lines() + .rev() + .find(|line| line.contains("def set_handler(")) + .expect("main IEmitter.set_handler signature"); + assert_eq!(signature, main_signature); + let main_runtime = fs::read_to_string(baseline.join("audit__i_emitter.py")).unwrap(); + assert!(main_runtime.contains(null_projection), "{main_runtime}"); + eprintln!("#191 main and #190 IEmitter stubs: {signature}"); + } + + let python = std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("python")); + let checker = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("tests") + .join("e2e") + .join("check_generated_python.py"); + let checked = Command::new(&python) + .arg(checker) + .arg(&generated) + .output() + .unwrap(); + assert!( + checked.status.success(), + "{}\n{}", + String::from_utf8_lossy(&checked.stdout), + String::from_utf8_lossy(&checked.stderr) + ); + let mypy_available = Command::new(&python) + .args(["-m", "mypy", "--version"]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + mypy_available || std::env::var("DYNWINRT_REQUIRE_MYPY").as_deref() != Ok("1"), + "DYNWINRT_REQUIRE_MYPY=1 but mypy is unavailable" + ); + for (file, consumer, expected_errors) in [ + ( + "valid.py", + r#"from typing import assert_type +from dynwinrt import DynWinRTValue, DynWinRtDelegate +from projected.audit import IEmitter + +def use(emitter: IEmitter, raw: DynWinRTValue, native: DynWinRtDelegate) -> None: + def callback(inner: DynWinRTValue | None) -> None: + if inner is not None: + inner.identity_raw() + emitter.set_handler(callback) + emitter.set_handler(lambda inner: assert_type(inner, DynWinRTValue | None)) + emitter.set_handler(raw) + emitter.set_handler(native) +"#, + 0, + ), + ( + "invalid.py", + r#"from dynwinrt import DynWinRTValue +from projected.audit import IEmitter + +def nonnullable(inner: DynWinRTValue) -> None: ... +def invalid(emitter: IEmitter) -> None: + emitter.set_handler(nonnullable) +"#, + 1, + ), + ] { + fs::write( + fixture.0.join(file), + format!("# pyright: strict, reportPrivateUsage=false\n{consumer}"), + ) + .unwrap(); + if mypy_available { + let output = Command::new(&python) + .args([ + "-B", + "-m", + "mypy", + "--strict", + "--no-incremental", + "--follow-imports=silent", + "--no-pretty", + "--show-error-codes", + "--cache-dir", + ".mypy_cache", + ]) + .arg(file) + .current_dir(&fixture.0) + .env( + "MYPYPATH", + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("bindings") + .join("py"), + ) + .output() + .unwrap(); + let diagnostics = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let errors = diagnostics + .lines() + .filter(|line| line.contains(": error:")) + .count(); + assert_eq!(errors, expected_errors, "{diagnostics}"); + assert_eq!( + output.status.success(), + expected_errors == 0, + "{diagnostics}" + ); + if expected_errors != 0 { + assert!(diagnostics.contains("[arg-type]"), "{diagnostics}"); + } + } + if let Some(pyright) = std::env::var_os("DYNWINRT_PYRIGHT") { + let output = Command::new(pyright) + .args(["--pythonpath"]) + .arg(&python) + .arg(file) + .current_dir(&fixture.0) + .output() + .unwrap(); + let diagnostics = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let errors = diagnostics + .lines() + .filter(|line| line.contains(" - error: ")) + .count(); + assert_eq!(errors, expected_errors, "{diagnostics}"); + assert_eq!( + output.status.success(), + expected_errors == 0, + "{diagnostics}" + ); + if expected_errors != 0 { + assert!(diagnostics.contains("reportArgumentType"), "{diagnostics}"); + } + } + } + + let runtime_available = Command::new(&python) + .args(["-c", "from dynwinrt import DynWinRTImplementation"]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + runtime_available + || std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref() != Ok("1"), + "the nested delegate callback probe requires the matching Python binding" + ); + if !runtime_available { + eprintln!("Skipping nested delegate native callback: set DYNWINRT_TEST_PYTHON."); + return; + } + fs::write( + fixture.0.join("native.py"), + r#"from dynwinrt import ( + DynWinRTImplementation, DynWinRTImplementationMethod, DynWinRTInterfacePlan, + DynWinRTMethodSig, DynWinRTType, DynWinRTValue, DynWinRtDelegate, RoApartment, + WinGUID, projected_lifetime_scope, release_projected, +) +from projected.audit import IEmitter + +inner_iid = WinGUID.parse("31d447a1-6281-4900-b782-040302010910") +outer_iid = WinGUID.parse("31d447a2-6281-4900-b782-040302010910") +emitter_iid = WinGUID.parse("31d447a3-6281-4900-b782-040302010910") +emitter_sig = DynWinRTMethodSig().add_in(DynWinRTType.delegate(outer_iid)) +emitter_type = DynWinRTType.register_interface("Audit.IEmitter", emitter_iid) +emitter_type = emitter_type.add_method("SetHandler", emitter_sig) +plan = DynWinRTInterfacePlan.create( + "Audit.IEmitter", emitter_type, + [DynWinRTImplementationMethod("SetHandler", 6, emitter_sig)], +) +outer_sig = DynWinRTMethodSig().add_in(DynWinRTType.delegate(inner_iid)) +received = [] + +with RoApartment(1), projected_lifetime_scope(): + inner = DynWinRtDelegate.create(inner_iid, [], lambda: None) + inner_value = inner.to_value() + def callback(argument): + if argument is None: + received.append(None) + else: + assert isinstance(argument, DynWinRTValue), type(argument) + assert not argument.is_null() + received.append(argument.identity_raw() == inner_value.identity_raw()) + def dispatch(_interface, slot, args): + assert slot == 6 + args[0].invoke_delegate(outer_iid, outer_sig, [DynWinRTValue.null_value()]) + args[0].invoke_delegate(outer_iid, outer_sig, [inner_value]) + return [] + try: + with DynWinRTImplementation.create([plan], dispatch) as owner: + value = owner.to_value() + try: + emitter = IEmitter.from_value(value) + try: + emitter.set_handler(callback) + finally: + release_projected(emitter) + finally: + value.release() + finally: + inner_value.release() +assert received == [None, True], received +print("delegate-typed-native-null-ok", received) +"#, + ) + .unwrap(); + let output = Command::new(&python) + .args(["-B", "native.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + let diagnostics = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.status.success(), "{diagnostics}"); + assert!( + diagnostics.contains("delegate-typed-native-null-ok [None, True]"), + "{diagnostics}" + ); + eprintln!("{}", String::from_utf8_lossy(&output.stdout).trim()); +} + #[test] fn bespoke_event_delegates_are_typed_and_projected() { let Some(output) =