Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,45 @@ Generated package manifests pin `dynwinrt` to the exact version of
Generated `IReference<T>` 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 and out values. For example,
`StorageFolder.create_file_async()` returns `WinRTCoroutine[StorageFile]`.

These values keep `| None`:

- `IReference<T>` 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
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:
Expand Down
5 changes: 0 additions & 5 deletions samples/python/async-file-io/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 0 additions & 4 deletions samples/python/cryptography/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 0 additions & 2 deletions samples/python/device-watcher/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 1 addition & 10 deletions samples/python/ocr-image/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 0 additions & 2 deletions samples/python/text-to-speech/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 15 additions & 5 deletions tests/e2e/typecheck/python_generated_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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])
Expand Down Expand Up @@ -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)
17 changes: 17 additions & 0 deletions tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt
Original file line number Diff line number Diff line change
@@ -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(*)
Loading
Loading