Skip to content
Merged
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
3 changes: 1 addition & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ body:
- "dynwinrt (core library)"
- "dynwinrt-codegen (code generator)"
- "JS bindings (@microsoft/dynwinrt)"
- "Python bindings (dynwinrt-py)"
- "Python bindings (dynwinrt)"
- "Other"
id: component
- type: dropdown
Expand All @@ -64,4 +64,3 @@ body:
- Node.js: 22.x (if using JS bindings)
- Python: 3.12 (if using Python bindings)
id: environment

3 changes: 1 addition & 2 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ body:
- "dynwinrt (core library)"
- "dynwinrt-codegen (code generator)"
- "JS bindings (@microsoft/dynwinrt)"
- "Python bindings (dynwinrt-py)"
- "Python bindings (dynwinrt)"
- "New component"
- type: textarea
attributes:
Expand All @@ -43,4 +43,3 @@ body:
label: Open Questions
description: |
Please list any open questions or issues that you think still need to be addressed before this suggestion can be implemented.

2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
- name: Test Python binding
run: |
.\bindings\py\.venv\Scripts\python.exe -m pytest bindings\py\tests -q
.\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt_py --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases
.\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases
- name: Run E2E tests
run: .\tests\e2e\e2e_test.ps1 -SkipBuild
- name: Test JS TSFN lifecycle and queue handling
Expand Down
96 changes: 66 additions & 30 deletions .github/workflows/python-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ on:
- '.github/workflows/python-release.yml'
- 'bindings/py/**'
- 'crates/dynwinrt/**'
- 'eng/release/python/prepare_python_release.py'
- 'eng/release/python/test_python_*_wheel.ps1'
- 'eng/release/python/verify_python_release.py'
- 'tools/dynwinrt-codegen/**'
- 'Cargo.lock'
- 'Cargo.toml'
push:
tags: ['python-v*']
tags: ['v*']
workflow_dispatch:
inputs:
release_version:
description: Cargo/package version expected from the selected ref
description: Version to build; required as confirmation when publishing to PyPI
required: false
type: string
publish_pypi:
Expand All @@ -41,6 +42,7 @@ jobs:
runs-on: windows-latest
outputs:
version: ${{ steps.version.outputs.version }}
python_version: ${{ steps.version.outputs.python_version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -57,27 +59,41 @@ jobs:
run: |
python -m pip install "packaging>=24,<27" --quiet
$cargo = Get-Content bindings\py\Cargo.toml -Raw
$version = [regex]::Match($cargo, '(?m)^version = "([^"]+)"$').Groups[1].Value
if (-not $version) { throw 'Could not read bindings/py Cargo version' }
$sourceVersion = [regex]::Match($cargo, '(?m)^version = "([^"]+)"$').Groups[1].Value
if (-not $sourceVersion) { throw 'Could not read bindings/py Cargo version' }

$arguments = @('eng\release\python\verify_python_release.py', 'source')
if ($env:RELEASE_VERSION) {
$arguments += @('--release-version', $env:RELEASE_VERSION)
}
python eng\release\python\verify_python_release.py source
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$version = $sourceVersion
if ($env:REF_TYPE -eq 'tag') {
$arguments += @('--tag', $env:REF_NAME)
if ($env:REF_NAME -notmatch '^v(.+)$') {
throw "Release tags must match v<version>, got '$env:REF_NAME'"
}
$version = $Matches[1]
}

if ($env:RELEASE_VERSION) {
if ($env:REF_TYPE -eq 'tag' -and $env:RELEASE_VERSION -ne $version) {
throw "Confirmed release_version '$env:RELEASE_VERSION' does not match tag version '$version'"
}
$version = $env:RELEASE_VERSION
}
python @arguments

$env:EFFECTIVE_VERSION = $version
$pythonVersion = (python -c "import os; from packaging.version import Version; print(Version(os.environ['EFFECTIVE_VERSION']))").Trim()
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Write-Host "Effective Python version: $pythonVersion"

if ($env:PUBLISH_PYPI -eq 'true' -and
($env:REF_TYPE -ne 'tag' -or $env:REF_NAME -notlike 'python-v*')) {
throw 'PyPI publication requires dispatching an existing python-v<version> tag'
($env:REF_TYPE -ne 'tag' -or $env:REF_NAME -notlike 'v*')) {
throw 'PyPI publication requires dispatching an existing v<version> tag'
}
if ($env:PUBLISH_PYPI -eq 'true' -and -not $env:RELEASE_VERSION) {
throw 'PyPI publication requires an explicit release_version confirmation'
}
"version=$version" >> $env:GITHUB_OUTPUT
"python_version=$pythonVersion" >> $env:GITHUB_OUTPUT

runtime-x64:
needs: validate
Expand All @@ -104,6 +120,10 @@ jobs:
run: |
python -c "import platform, sys; assert platform.machine().lower() in ('amd64', 'x86_64'); assert f'{sys.version_info.major}.{sys.version_info.minor}' == '${{ matrix.python }}'"
python -m pip install "maturin>=1.11,<2" "packaging>=24,<27" --quiet
python eng\release\python\prepare_python_release.py --version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python eng\release\python\verify_python_release.py source --release-version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Remove-Item artifacts\runtime -Recurse -Force -ErrorAction SilentlyContinue
python -m maturin build --release --locked `
--manifest-path bindings\py\Cargo.toml `
Expand All @@ -114,7 +134,7 @@ jobs:
if ($wheel.Count -ne 1) { throw "Expected one runtime wheel, found $($wheel.Count)" }
python eng\release\python\verify_python_release.py wheel `
--wheel $wheel[0].FullName `
--package dynwinrt-py `
--package dynwinrt `
--version $env:VERSION `
--python-tag '${{ matrix.tag }}' `
--abi-tag '${{ matrix.tag }}' `
Expand Down Expand Up @@ -151,6 +171,10 @@ jobs:
run: |
python -c "import platform, struct, sys; assert platform.machine().lower() in ('arm64', 'aarch64'); assert struct.calcsize('P') == 8; assert f'{sys.version_info.major}.{sys.version_info.minor}' == '${{ matrix.python }}'"
python -m pip install "maturin>=1.11,<2" "packaging>=24,<27" --quiet
python eng\release\python\prepare_python_release.py --version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python eng\release\python\verify_python_release.py source --release-version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Remove-Item artifacts\runtime -Recurse -Force -ErrorAction SilentlyContinue
python -m maturin build --release --locked `
--manifest-path bindings\py\Cargo.toml `
Expand All @@ -161,7 +185,7 @@ jobs:
if ($wheel.Count -ne 1) { throw "Expected one runtime wheel, found $($wheel.Count)" }
python eng\release\python\verify_python_release.py wheel `
--wheel $wheel[0].FullName `
--package dynwinrt-py `
--package dynwinrt `
--version $env:VERSION `
--python-tag '${{ matrix.tag }}' `
--abi-tag '${{ matrix.tag }}' `
Expand All @@ -188,6 +212,10 @@ jobs:
VERSION: ${{ needs.validate.outputs.version }}
run: |
python -m pip install "maturin>=1.11,<2" "packaging>=24,<27" --quiet
python eng\release\python\prepare_python_release.py --version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python eng\release\python\verify_python_release.py source --release-version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Remove-Item artifacts\codegen -Recurse -Force -ErrorAction SilentlyContinue
python -m maturin build --release --locked `
--manifest-path tools\dynwinrt-codegen\Cargo.toml `
Expand Down Expand Up @@ -226,6 +254,10 @@ jobs:
run: |
python -c "import platform, struct; assert platform.machine().lower() in ('arm64', 'aarch64'); assert struct.calcsize('P') == 8"
python -m pip install "maturin>=1.11,<2" "packaging>=24,<27" --quiet
python eng\release\python\prepare_python_release.py --version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python eng\release\python\verify_python_release.py source --release-version $env:VERSION
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Remove-Item artifacts\codegen -Recurse -Force -ErrorAction SilentlyContinue
python -m maturin build --release --locked `
--manifest-path tools\dynwinrt-codegen\Cargo.toml `
Expand Down Expand Up @@ -325,6 +357,7 @@ jobs:
-RuntimeWheelDirectory artifacts\runtime `
-CodegenWheelDirectory artifacts\codegen `
-Version '${{ needs.validate.outputs.version }}' `
-PythonPackageVersion '${{ needs.validate.outputs.python_version }}' `
-Architecture x64 -PythonMinor '${{ matrix.python }}'

consume-runtime-arm64:
Expand Down Expand Up @@ -360,6 +393,7 @@ jobs:
-RuntimeWheelDirectory artifacts\runtime `
-CodegenWheelDirectory artifacts\codegen `
-Version '${{ needs.validate.outputs.version }}' `
-PythonPackageVersion '${{ needs.validate.outputs.python_version }}' `
-Architecture arm64 -PythonMinor '${{ matrix.python }}'

assemble-release:
Expand Down Expand Up @@ -417,22 +451,24 @@ jobs:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
$wheels = (Get-ChildItem dist\*.whl -Recurse).FullName
& gh release view $env:TAG *> $null
if ($LASTEXITCODE -eq 0) {
& gh release upload $env:TAG --clobber @wheels
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
exit 0
$releaseFound = $false
for ($attempt = 1; $attempt -le 40; $attempt++) {
& gh release view $env:TAG --repo $env:GH_REPO *> $null
if ($LASTEXITCODE -eq 0) {
$releaseFound = $true
break
}
Write-Host "Waiting for the JavaScript release $env:TAG ($attempt/40)..."
Start-Sleep -Seconds 30
}
$arguments = @(
'release', 'create', $env:TAG,
'--verify-tag',
'--generate-notes',
'--title', "Python v$env:VERSION"
)
if ($env:VERSION -match '-') { $arguments += '--prerelease' }

if (-not $releaseFound) {
throw "JavaScript GitHub release $env:TAG was not found after 20 minutes"
}

$arguments = @('release', 'upload', $env:TAG, '--repo', $env:GH_REPO, '--clobber')
$arguments += $wheels
& gh @arguments
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Expand All @@ -442,7 +478,7 @@ jobs:
github.event_name == 'workflow_dispatch' &&
inputs.publish_pypi &&
github.ref_type == 'tag' &&
startsWith(github.ref_name, 'python-v')
startsWith(github.ref_name, 'v')
needs: [validate, assemble-release]
runs-on: ubuntu-latest
environment: pypi
Expand All @@ -458,7 +494,7 @@ jobs:
shell: bash
run: |
mkdir -p publish/runtime
find dist -type f -name 'dynwinrt_py-*.whl' -exec cp '{}' publish/runtime/ \;
find dist -type f -name 'dynwinrt-*.whl' -exec cp '{}' publish/runtime/ \;
test "$(find publish/runtime -type f -name '*.whl' | wc -l)" -eq 8
- uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand All @@ -471,7 +507,7 @@ jobs:
github.event_name == 'workflow_dispatch' &&
inputs.publish_pypi &&
github.ref_type == 'tag' &&
startsWith(github.ref_name, 'python-v')
startsWith(github.ref_name, 'v')
needs: [validate, assemble-release, publish-runtime]
runs-on: ubuntu-latest
environment: pypi
Expand Down
2 changes: 1 addition & 1 deletion .pipelines/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ extends:
script: |
.\bindings\py\.venv\Scripts\python.exe -m pytest bindings\py\tests -q
if ($LASTEXITCODE -ne 0) { Write-Error "Python binding tests failed"; exit 1 }
.\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt_py --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases
.\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases
if ($LASTEXITCODE -ne 0) { Write-Error "Python runtime stub validation failed"; exit 1 }

# E2E tests: winmd → generate → type-check → call real WinRT and COM APIs
Expand Down
2 changes: 1 addition & 1 deletion bindings/py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description = "Python bindings for dynwinrt via PyO3"
repository = "https://github.com/microsoft/dynwinrt"
authors = ["Microsoft"]
[lib]
name = "dynwinrt_py"
name = "dynwinrt"
crate-type = ["cdylib"]

[dependencies]
Expand Down
33 changes: 18 additions & 15 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# dynwinrt-py
# dynwinrt

`dynwinrt-py` is the native CPython runtime for generated dynwinrt projections.
`dynwinrt` is the native CPython runtime for generated dynwinrt projections.
Release wheels support CPython 3.11 through 3.14 on Windows x64 and ARM64.
The floor is intentionally the first CPython line for which the release
infrastructure can install and execute a native Windows ARM64 interpreter; the
Expand All @@ -10,12 +10,12 @@ Install the runtime and the standalone generator from an approved feed or a
downloaded release:

```powershell
python -m pip install dynwinrt-py dynwinrt-codegen
python -m pip install dynwinrt dynwinrt-codegen
dynwinrt-codegen generate --namespace Windows.Foundation --class-name Uri `
--lang py --output generated_uri
```

Generated Python package manifests pin `dynwinrt-py` to exactly the version of
Generated Python package manifests pin `dynwinrt` to exactly the version of
`dynwinrt-codegen` that emitted them.

## Develop
Expand All @@ -36,24 +36,27 @@ The wheel includes `__init__.pyi` and `py.typed` for static type checking.
installed command with Rust removed from `PATH`.

Pull requests run the x64 matrix. ARM64 jobs run only for manual dispatches and
`python-v*` tags on the repository's existing
`v*` release tags on the repository's existing
`[self-hosted, Windows, ARM64, winui]` runner. This avoids executing untrusted
pull-request code on a self-hosted machine. A release remains blocked rather
than producing ARM64 artifacts if that runner or one of its native CPython
versions is unavailable.

To release:

1. Set the same version in `bindings\py\Cargo.toml` and
`tools\dynwinrt-codegen\Cargo.toml`, update `Cargo.lock`, and merge it.
2. Create `python-v<version>` (for example, `python-v0.1.0`). The workflow
rejects mismatched or malformed tags and attaches only the complete tested
wheel set to the GitHub release. A hyphenated version is marked prerelease.
1. Create the ordinary repository release tag (for example,
`v0.1.0-preview.20`). The workflow derives the Python package version from
the tag and updates its build workspaces without modifying the repository.
PEP 440 normalizes `preview.N` wheel versions to `rcN`.
2. The JavaScript release pipeline creates the GitHub release. After all ten
Python wheels pass their consumption tests, this workflow waits for that
release and uploads the wheels alongside the npm tarballs. It never creates
a separate Python GitHub release.
3. Configure the protected `pypi` GitHub environment with required reviewers
and PyPI trusted publishers for both projects. To publish, manually dispatch
the workflow **from that existing tag** with `publish_pypi` enabled and
`release_version` set. OIDC trusted publishing is used; no API token is
stored in the repository.
and PyPI trusted publishers for `dynwinrt` and `dynwinrt-codegen`. To
publish, manually dispatch the workflow **from that existing tag** with
`publish_pypi` enabled and `release_version` set. OIDC trusted publishing is
used; no API token is stored in the repository.

Do not enable publication from a branch, pull request, or ordinary push.

Expand Down Expand Up @@ -223,7 +226,7 @@ Use a projected lifetime scope inside the COM apartment so generated wrappers
release their native values before `RoUninitialize`:

```python
from dynwinrt_py import RoApartment, projected_lifetime_scope
from dynwinrt import RoApartment, projected_lifetime_scope

with RoApartment(0), projected_lifetime_scope():
app = Application.create()
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion bindings/py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["maturin>=1.11,<2.0"]
build-backend = "maturin"

[project]
name = "dynwinrt-py"
name = "dynwinrt"
description = "Python bindings for dynamic WinRT API invocation"
requires-python = ">=3.11,<3.15"
license = {text = "MIT"}
Expand Down
6 changes: 3 additions & 3 deletions bindings/py/src/async_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,14 @@ impl AsyncOperation {

let converter = self.converter.clone_ref(py);
let convert_future = py
.import("dynwinrt_py.dynwinrt_py")?
.import("dynwinrt.dynwinrt")?
.getattr("_dynwinrt_convert_future")?;
let coroutine = convert_future.call1((raw_future.clone(), converter))?;
let future = py
.import("asyncio")?
.call_method0("get_running_loop")?
.call_method1("create_task", (coroutine,))?;
py.import("dynwinrt_py.dynwinrt_py")?
py.import("dynwinrt.dynwinrt")?
.getattr("_dynwinrt_link_cancellation")?
.call1((future.clone(), raw_future))?;
let future = future.unbind();
Expand Down Expand Up @@ -372,7 +372,7 @@ impl DynWinRTAsyncWithProgress {
})?;
let converter = self.progress_converter.clone_ref(py);
let dispatch_progress = py
.import("dynwinrt_py.dynwinrt_py")?
.import("dynwinrt.dynwinrt")?
.getattr("_dynwinrt_dispatch_progress")?
.unbind();
let callback_context = py
Expand Down
2 changes: 1 addition & 1 deletion bindings/py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod runtime;
mod values;

#[pymodule]
mod dynwinrt_py {
mod dynwinrt {
use pyo3::prelude::*;

#[pymodule_init]
Expand Down
2 changes: 1 addition & 1 deletion bindings/py/stubtest_allowlist.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
dynwinrt_py\.dynwinrt_py
dynwinrt\.dynwinrt
Loading
Loading