diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a69967f8..cca3a40b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,6 +8,9 @@ on: paths: - 'docs/**' - 'scripts/generate-llms-full.mjs' + - 'scripts/check_docs_release.py' + - 'test/python/test_docs_release.py' + - 'tywrap_ir/pyproject.toml' - 'package.json' - 'package-lock.json' - '.github/workflows/docs.yml' @@ -36,6 +39,13 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --no-audit + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Test release availability gate + run: python -m unittest discover -s test/python -p 'test_docs_release.py' -v + - name: Build VitePress site run: npm run docs:build @@ -55,6 +65,7 @@ jobs: deploy: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' permissions: + contents: read pages: write id-token: write environment: @@ -63,6 +74,17 @@ jobs: needs: build runs-on: ubuntu-latest steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Check public package versions + id: packages + run: python scripts/check_docs_release.py + - name: Deploy to GitHub Pages + if: steps.packages.outputs.available == 'true' id: deployment uses: actions/deploy-pages@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7326d5bc..8641b429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,60 @@ # Changelog +## [0.11.0](https://github.com/bbopen/tywrap/compare/v0.10.0...v0.11.0) (2026-09-20) + +Python integers outside JavaScript's safe range now reject before JSON encoding. +NumPy and Torch float16 values decode to numbers instead of storage words. +Generated wrappers preserve supported overload relationships and await Python +coroutine results through Node and Pyodide. + +### Compatibility + +- Python integer results must be between -9007199254740991 and 9007199254740991. + This applies to nested values, supported NumPy scalars, and model dumps. + Errors identify the result path and suggest a string or an Arrow integer column. +- Float16 results preserve finite values, subnormals, and signed zero. Remove + application code that converts the returned storage words manually. +- Unresolved return annotations produce `unknown` with generation diagnostics. + Existing partial return checks remain. Type-only Protocol signatures remain + available, but an unresolved generic runtime result cannot promise `T`. +- Regenerate wrappers after upgrading the generator and runtime. The value-RPC + protocol remains `tywrap/1`, and the Python IR schema remains `0.4.0`. + +### Generation and execution + +- Ambiguous unions produce generation diagnostics when the same wire value can + decode into different representations. Disjoint supported alternatives remain usable. +- The pure callable compiler resolves arguments, overloads, conversions, + capabilities, and diagnostics before code generation. Declarations and return + validators use those resolved contracts. +- Supported overload calls infer their corresponding return types. Invalid calls + fail TypeScript compilation, and invalid results reject through the generated + validator. Registered overload extraction requires Python 3.11 or later. +- Node and Pyodide await Python coroutine results. Exceptions retain call context; + timeout and disposal checks cover subsequent requests. HTTP behavior depends + on the server's coroutine support. +- Node worker restarts isolate replacement requests from retired process errors + and stale write backpressure. +- Development reload preserves the working bridge and generated files when a + configured Python module changes during candidate preparation. +- Python encodes ASCII subprocess frames without per-character scanning, + preserving the wire format and Unicode handling. +- `tywrap-ir` 0.3.1 collects generic parameters used only in overload annotations. + Its package version changes independently from the npm package. + +### Validation and design work + +- Clean consumer checks install a packed npm package and a non-editable Python + wheel, then generate, compile, and execute wrappers outside the checkout. +- Actual Pyodide and browser smoke tests complement the existing mocked and + CPython checks. Scientific tests include an independent finite float16 oracle + and real NumPy and Torch generated calls. +- Exact bigint transport, dataclass transport, and explicit runtime binding have + bounded design prototypes. They are not default production features in 0.11. + +See the [migration guide](https://bbopen.github.io/tywrap/guide/migrating-to-0-11) +for package versions, fresh IR extraction, pinned contracts, and conversion examples. + ## [0.10.0](https://github.com/bbopen/tywrap/compare/v0.9.0...v0.10.0) (2026-07-13) The scientific codec correctness release. Codec envelopes for numpy, pandas, scipy, torch, and sklearn now compose inside ordinary containers, validate what they claim, and refuse to serialize what they cannot preserve. Every behavior change in this release landed by flipping named rows in the menagerie truth table, and the pinned scientific CI job holds the line against library drift. diff --git a/docs/guide/migrating-to-0-11.md b/docs/guide/migrating-to-0-11.md index 4a1cc4a6..d2e6ee96 100644 --- a/docs/guide/migrating-to-0-11.md +++ b/docs/guide/migrating-to-0-11.md @@ -1,19 +1,22 @@ # Migrating to 0.11 -This guide covers the planned 0.11 changes. The release is not published yet. +This guide covers tywrap 0.11.0 and tywrap-ir 0.3.1. The documentation +deployment requires both versions to be available on their public package +registries. ## Large Python integers Python `int` results still use TypeScript `number`. The supported range is `-9007199254740991` through `9007199254740991`, inclusive. -Earlier versions could round larger integers when JavaScript parsed the response. -The new serializer rejects those values before writing JSON. This applies to root -results, nested containers, supported NumPy scalars, and model dumps. +Earlier versions could round larger integers when JavaScript parsed the +response. The new serializer rejects those values before writing JSON. This +applies to root results, nested containers, supported NumPy scalars, and model +dumps. For example, a result containing `{"items": [9007199254740993]}` now fails at -`result.items[0]`. The error identifies the unsafe value and suggests an explicit -string or an Arrow integer column. +`result.items[0]`. The error identifies the unsafe value and suggests an +explicit string or an Arrow integer column. Return a string when the value represents an identifier or must remain an exact decimal integer: @@ -32,18 +35,19 @@ representation. Arrow int64 remains exact and can decode as JavaScript `bigint`. JSON fallback does not provide that representation and rejects unsafe integers. Booleans and finite floats remain supported. Converting an integer to a Python -`float` can lose precision before tywrap receives it. Use a string when exactness -matters. Tagged bigint transport remains a separate design proposal. +`float` can lose precision before tywrap receives it. Use a string when +exactness matters. Tagged bigint transport remains a separate design proposal. ## NumPy and Torch float16 The Arrow decoder now converts float16 storage words into their numeric values before reshaping the result. For example, `[15872, 49280]` becomes the intended -`[1.5, -2.25]`. Remove application code that manually decodes those storage words. +`[1.5, -2.25]`. Remove application code that manually decodes those storage +words. -The conversion preserves signed zero, finite subnormal values, and finite extrema. -NumPy arrays and nested Torch tensors use the same conversion. Non-finite results -continue to reject under the existing policy. +The conversion preserves signed zero, finite subnormal values, and finite +extrema. NumPy arrays and nested Torch tensors use the same conversion. +Non-finite results continue to reject under the existing policy. The scientific envelope format, JSON fallback, and Arrow int64 behavior stay compatible. Torch bfloat16 keeps its existing float32 transport conversion. @@ -51,7 +55,8 @@ compatible. Torch bfloat16 keeps its existing float32 transport conversion. ## Return types that cannot be validated An unresolved Python `object` result now produces `Promise`. Python -objects can include strings and numbers, which TypeScript's `object` type excludes. +objects can include strings and numbers, which TypeScript's `object` type +excludes. An unresolved generic result also becomes `unknown`. Earlier wrappers could declare `Promise` while accepting any returned value. A generic input remains @@ -70,10 +75,10 @@ if (typeof result !== 'string') { console.log(result.toUpperCase()); ``` -The current value contract does not resolve every Python annotation. For example, -`Literal` results become `unknown`, while their existing literal checks remain. -Other unresolved returns retain available marker, container or record checks. -These partial checks do not prove a precise TypeScript return type. +The current value contract does not resolve every Python annotation. For +example, `Literal` results become `unknown`, while their existing literal checks +remain. Other unresolved returns retain available marker, container or record +checks. These partial checks do not prove a precise TypeScript return type. Type-only Protocol methods keep their declared generic signatures. Supported string and integer overloads retain their corresponding return types. @@ -91,7 +96,7 @@ Python results; upgrading the TypeScript client does not update that server. ## Regenerate wrappers -After these versions are published, upgrade both packages: +Upgrade both packages: ```bash npm install tywrap@0.11.0 @@ -116,8 +121,8 @@ If you enable caching again, first remove the old generated IR files from If you use `contractInput`, temporarily omit it to extract fresh contracts with the updated Python package. Review each new `.contract.json` in your -output directory. Replace the corresponding pinned input file with that contract, -or point `contractInput` to the new file. Then restore the setting. +output directory. Replace the corresponding pinned input file with that +contract, or point `contractInput` to the new file. Then restore the setting. The `--no-cache` option does not replace a pinned input contract. Regenerate wrappers with the upgraded generator. Python `int` return validators @@ -127,6 +132,6 @@ Regeneration also applies the reviewed callable-contract diagnostics. Update any tests that expected a rounded integer or float16 storage words. Assert the corrected value or the specific conversion error instead. -Run your generated calls against the upgraded runtime before deploying them. -An old wrapper cannot add the new generated return checks by itself, although the +Run your generated calls against the upgraded runtime before deploying them. An +old wrapper cannot add the new generated return checks by itself, although the upgraded Python serializer still rejects unsafe integer results. diff --git a/docs/maintainers/architecture-decision-0.11.md b/docs/maintainers/architecture-decision-0.11.md new file mode 100644 index 00000000..1fb297b3 --- /dev/null +++ b/docs/maintainers/architecture-decision-0.11.md @@ -0,0 +1,104 @@ +# Architecture decision for 0.11 + +Decision: proceed with the reviewed core and retain the extensions as bounded +prototypes. This decision does not replace release checks or publication verification. + +## Decision + +Adopt the pure callable compiler and revision 2 value contracts for the 0.11 +corrections. Keep exact-integer transport, dataclass transport, and explicit +runtime binding as bounded design prototypes until their separate production +integration is approved. + +The callable compiler resolves validated Python IR before TypeScript emission. +Its result describes parameter binding, overloads, return representations, +validation, capabilities, and diagnostics. The generator consumes that result. +Interpreter discovery, caching, and file writes remain outside pure compilation. + +A precise generated return type requires a supported conversion and validation +path. Unresolved results become unknown, with diagnostics. Existing partial +runtime checks remain useful but cannot justify a precise declaration by themselves. +Type-only Protocol methods retain their declared signatures. + +Python serialization and TypeScript decoding remain separate implementations. +The value contracts and independent end-to-end tests bind their behavior. This +change does not claim that both languages execute one shared codec implementation. + +## What the design now proves + +| Case | Verified path | Evidence | +| --- | --- | --- | +| Safe integers | Python rejects unsafe integers before JSON; generated validators enforce safe integer results; clean consumers exercise boundaries and rejection. | Accepted numeric and consumer histories; integrated [CI 35503216347](https://github.com/bbopen/tywrap/actions/runs/35503216347). | +| Float16 values | Real NumPy and Torch annotations pass analysis, generated typing, TypeScript compilation, bridge execution, decoding, and validation. Wrong dtype rejects. | NumPy conformance `1736b68`; real Torch `4db7002`, [CI 35503315287](https://github.com/bbopen/tywrap/actions/runs/35503315287). | +| Nested overload results | Generated overloads retain supported input/output relationships. Strict consumer compilation rejects invalid calls, and runtime validation rejects a wrong selected result. | Independent conformance `1736b68`, [CI 35500783650](https://github.com/bbopen/tywrap/actions/runs/35500783650). | +| Coroutine results | Generated implementations and declarations return `Promise` without warnings. Node and actual Pyodide execute the calls and retain cancellation/reuse checks. | `54cef7c`, [CI 35503202358](https://github.com/bbopen/tywrap/actions/runs/35503202358). | + +The numeric oracle checks every finite binary16 storage word against Python's +standard-library decoder. Mutation tests reject incorrect dtype, shape, codec +version, encoding, capability, and contract information. + +Independent review also tested union ambiguity against the actual decoders. Core +correction `b11dae6` rejects colliding bytes and scientific records, including +extra keys and optional legacy fields. It preserves the reviewed disjoint +alternatives. [CI 35505214309](https://github.com/bbopen/tywrap/actions/runs/35505214309) +passed; `9e87968` changes only formatting. The combined merge still needs its +own complete checks. + +## Extension decisions + +The Point prototype at `f41bb44` uses actual Python analysis and a private adapter. +It derives field checks from the resolved contract and verifies capability before +dispatch. This is evidence for a dataclass design, not default dataclass support. + +The explicit binding prototype at `65bed95` derives legacy and bound renderings +from one compiled module and generator. Handles borrow a call-capable runtime; +disposing a handle does not dispose the shared bridge. Tests cover two Python +environments, registry compatibility, failures, reload, disposal, and mixed runtimes. +It has no public generation option or production output integration. + +Binding increases generated source by 38.5% and 27.0% in two measured fixtures. +Declarations increase by 164.8% and 117.0%. Public integration needs an explicit +size budget and should consider sharing declarations. + +Exact integers use a separate revision 3 prototype. Revision 2 continues to use +safe JavaScript numbers. The proposed v3 leaf uses a decimal-string envelope and +JavaScript bigint. Runtime capability must be checked before any tagged request. +Source `1862f3e` passed full CI and independent review. Generated proof `d06e4fa` +passed nested round trips, validation and byte-limit checks on Linux, macOS and +Windows. The independently reviewed follow-up `577800b` adds nested version +rejection and preserves the accepted source files. The combined candidate must +pass complete CI. This evidence does not establish default production bigint +transport. + +Measured compact argument envelopes grew from 28 to 247 bytes for a safe-integer +fixture and from 91 to 310 bytes for a larger-integer fixture. These measurements +cover argument envelopes, not complete RPC traffic. Default v2 safe-number +behavior and existing Arrow int64 semantics remain unchanged. + +## Compatibility and performance + +The default wire protocol remains tywrap/1. Unsafe JSON integers now reject instead +of risking rounding. Float16 values decode as numbers rather than storage words. +Some unresolved return annotations become unknown. Users must regenerate wrappers +and upgrade the matching runtime; migration guidance describes these changes. + +At integrated `737fcff`, all 25 performance/framing tests passed, as did the full +release-command CI run. The 20 MiB chunked response took 1.89 times its same-run +single-frame control, within the 3.5-times budget. Retained heap for an 80 MiB +response was 160 MiB, within the 384 MiB budget. + +These controls are not a benchmark against the published 0.10 package. They do not +prove zero release-to-release regression or measure Python application throughput. +The complete final candidate must pass the checks again after remaining changes. + +## Release conditions + +The architecture verdict is proceed. Independent source and prototype reviews +support the selected design. This verdict does not replace release acceptance. + +The combined candidate must pass `CI=1 npm run check:all`, the required matrix, +scientific and consumer proofs, browser checks, and performance budgets. Its +repository reviews and CodeRabbit conversations must be resolved before merge. +Publication must verify both package versions and execute fresh installed +registry artifacts. No prototype is promoted to default production support by +this decision. diff --git a/docs/public/llms-full.txt b/docs/public/llms-full.txt index ad186c8f..4ff12de8 100644 --- a/docs/public/llms-full.txt +++ b/docs/public/llms-full.txt @@ -444,20 +444,23 @@ const [sinValue, cosValue, tanValue] = await Promise.all([ # Migrating to 0.11 -This guide covers the planned 0.11 changes. The release is not published yet. +This guide covers tywrap 0.11.0 and tywrap-ir 0.3.1. The documentation +deployment requires both versions to be available on their public package +registries. ## Large Python integers Python `int` results still use TypeScript `number`. The supported range is `-9007199254740991` through `9007199254740991`, inclusive. -Earlier versions could round larger integers when JavaScript parsed the response. -The new serializer rejects those values before writing JSON. This applies to root -results, nested containers, supported NumPy scalars, and model dumps. +Earlier versions could round larger integers when JavaScript parsed the +response. The new serializer rejects those values before writing JSON. This +applies to root results, nested containers, supported NumPy scalars, and model +dumps. For example, a result containing `{"items": [9007199254740993]}` now fails at -`result.items[0]`. The error identifies the unsafe value and suggests an explicit -string or an Arrow integer column. +`result.items[0]`. The error identifies the unsafe value and suggests an +explicit string or an Arrow integer column. Return a string when the value represents an identifier or must remain an exact decimal integer: @@ -476,18 +479,19 @@ representation. Arrow int64 remains exact and can decode as JavaScript `bigint`. JSON fallback does not provide that representation and rejects unsafe integers. Booleans and finite floats remain supported. Converting an integer to a Python -`float` can lose precision before tywrap receives it. Use a string when exactness -matters. Tagged bigint transport remains a separate design proposal. +`float` can lose precision before tywrap receives it. Use a string when +exactness matters. Tagged bigint transport remains a separate design proposal. ## NumPy and Torch float16 The Arrow decoder now converts float16 storage words into their numeric values before reshaping the result. For example, `[15872, 49280]` becomes the intended -`[1.5, -2.25]`. Remove application code that manually decodes those storage words. +`[1.5, -2.25]`. Remove application code that manually decodes those storage +words. -The conversion preserves signed zero, finite subnormal values, and finite extrema. -NumPy arrays and nested Torch tensors use the same conversion. Non-finite results -continue to reject under the existing policy. +The conversion preserves signed zero, finite subnormal values, and finite +extrema. NumPy arrays and nested Torch tensors use the same conversion. +Non-finite results continue to reject under the existing policy. The scientific envelope format, JSON fallback, and Arrow int64 behavior stay compatible. Torch bfloat16 keeps its existing float32 transport conversion. @@ -495,7 +499,8 @@ compatible. Torch bfloat16 keeps its existing float32 transport conversion. ## Return types that cannot be validated An unresolved Python `object` result now produces `Promise`. Python -objects can include strings and numbers, which TypeScript's `object` type excludes. +objects can include strings and numbers, which TypeScript's `object` type +excludes. An unresolved generic result also becomes `unknown`. Earlier wrappers could declare `Promise` while accepting any returned value. A generic input remains @@ -514,10 +519,10 @@ if (typeof result !== 'string') { console.log(result.toUpperCase()); ``` -The current value contract does not resolve every Python annotation. For example, -`Literal` results become `unknown`, while their existing literal checks remain. -Other unresolved returns retain available marker, container or record checks. -These partial checks do not prove a precise TypeScript return type. +The current value contract does not resolve every Python annotation. For +example, `Literal` results become `unknown`, while their existing literal checks +remain. Other unresolved returns retain available marker, container or record +checks. These partial checks do not prove a precise TypeScript return type. Type-only Protocol methods keep their declared generic signatures. Supported string and integer overloads retain their corresponding return types. @@ -535,7 +540,7 @@ Python results; upgrading the TypeScript client does not update that server. ## Regenerate wrappers -After these versions are published, upgrade both packages: +Upgrade both packages: ```bash npm install tywrap@0.11.0 @@ -560,8 +565,8 @@ If you enable caching again, first remove the old generated IR files from If you use `contractInput`, temporarily omit it to extract fresh contracts with the updated Python package. Review each new `.contract.json` in your -output directory. Replace the corresponding pinned input file with that contract, -or point `contractInput` to the new file. Then restore the setting. +output directory. Replace the corresponding pinned input file with that +contract, or point `contractInput` to the new file. Then restore the setting. The `--no-cache` option does not replace a pinned input contract. Regenerate wrappers with the upgraded generator. Python `int` return validators @@ -571,8 +576,8 @@ Regeneration also applies the reviewed callable-contract diagnostics. Update any tests that expected a rounded integer or float16 storage words. Assert the corrected value or the specific conversion error instead. -Run your generated calls against the upgraded runtime before deploying them. -An old wrapper cannot add the new generated return checks by itself, although the +Run your generated calls against the upgraded runtime before deploying them. An +old wrapper cannot add the new generated return checks by itself, although the upgraded Python serializer still rejects unsafe integer results. --- diff --git a/docs/release.md b/docs/release.md index f2736ab4..13cd7f76 100644 --- a/docs/release.md +++ b/docs/release.md @@ -2,9 +2,10 @@ ## tywrap (npm) -Releases publish from `main` via the [`release.yml`](../.github/workflows/release.yml) -workflow. There is one path: bump the version on a branch, merge to `main`, and the -workflow tags, releases, and publishes. +Releases publish from `main` via the +[`release.yml`](../.github/workflows/release.yml) workflow. There is one path: +bump the version on a branch, merge to `main`, and the workflow tags, releases, +and publishes. 1. On a release branch, bump the version and update the changelog: - `package.json`: the new `version` @@ -15,6 +16,7 @@ workflow tags, releases, and publishes. - `package-lock.json`: refresh if dependencies changed 2. Run the release gate: + ```sh CI=1 npm run check:all ``` @@ -49,13 +51,18 @@ workflow tags, releases, and publishes. current IR schema used by `src/tywrap.ts`. 3. Validate the Python package: + ```sh python -m venv .venv-release ./.venv-release/bin/python -m pip install -e tywrap_ir - PATH="$PWD/.venv-release/bin:$PATH" python -m unittest discover -s tywrap_ir/tests -p 'test_*.py' -v + ./.venv-release/bin/python -I -m unittest discover -s tywrap_ir/tests -p 'test_*.py' -v ``` + Isolated mode prevents the repository directory from shadowing the installed + package. + 4. Tag the merged `main` commit and push the tag: + ```sh git tag tywrap-ir-v git push origin tywrap-ir-v @@ -64,6 +71,19 @@ workflow tags, releases, and publishes. 5. The [`publish-pypi.yml`](../.github/workflows/publish-pypi.yml) workflow publishes `tywrap-ir` to PyPI from that tag. +## Publish documentation + +The docs workflow checks both package versions against npm and PyPI before +deployment. It keeps the current site when either version is unavailable or a +registry check fails. After both packages publish, run the docs workflow on +`main`: + +```sh +gh workflow run docs.yml --ref main +``` + +Verify its deployment before announcing the release. + ## Notes - `tywrap` and `tywrap-ir` are versioned independently. diff --git a/package-lock.json b/package-lock.json index f95e87c4..429d4357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tywrap", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tywrap", - "version": "0.10.0", + "version": "0.11.0", "license": "MIT", "dependencies": { "typescript": "^6.0.3", diff --git a/package.json b/package.json index d44fa1f9..28aa4191 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tywrap", - "version": "0.10.0", + "version": "0.11.0", "description": "Generate TypeScript bindings for Python libraries with precise types where tywrap can resolve them — Node.js, experimental Deno, Bun, and browsers via Pyodide.", "type": "module", "main": "dist/index.js", diff --git a/scripts/check_docs_release.py b/scripts/check_docs_release.py new file mode 100644 index 00000000..495ba188 --- /dev/null +++ b/scripts/check_docs_release.py @@ -0,0 +1,68 @@ +"""Deploy release documentation only after both package versions are public.""" + +from collections.abc import Callable +import json +import os +from pathlib import Path +from urllib.error import URLError +from urllib.parse import quote +from urllib.request import urlopen + + +def fetch_json(url: str) -> object: + with urlopen(url, timeout=15) as response: + return json.load(response) + + +def packages_available( + npm_version: str, python_version: str, fetch: Callable[[str], object] = fetch_json +) -> bool: + """Require exact metadata matches from both public registries.""" + try: + npm = fetch(f"https://registry.npmjs.org/tywrap/{quote(npm_version, safe='')}") + python = fetch( + f"https://pypi.org/pypi/tywrap-ir/{quote(python_version, safe='')}/json" + ) + except (URLError, TimeoutError, ValueError, OSError): + return False + return ( + isinstance(npm, dict) + and npm.get("version") == npm_version + and isinstance(npm.get("dist"), dict) + and isinstance(npm["dist"].get("tarball"), str) + and bool(npm["dist"]["tarball"]) + and isinstance(python, dict) + and isinstance(python.get("info"), dict) + and python["info"].get("version") == python_version + and isinstance(python.get("urls"), list) + and any( + isinstance(file, dict) + and file.get("yanked") is False + and file.get("packagetype") in {"bdist_wheel", "sdist"} + and isinstance(file.get("url"), str) + and bool(file["url"]) + for file in python["urls"] + ) + ) + + +def main() -> None: + import tomllib + + root = Path(__file__).resolve().parent.parent + npm_version = json.loads((root / "package.json").read_text())["version"] + python_project = tomllib.loads((root / "tywrap_ir/pyproject.toml").read_text()) + python_version = python_project["project"]["version"] + available = packages_available(npm_version, python_version) + result = f"available={str(available).lower()}\n" + if os.environ.get("GITHUB_OUTPUT"): + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(result) + print(result.strip()) + if not available: + print("Docs deployment waits for both public packages.") + print("Run the docs workflow after publication.") + + +if __name__ == "__main__": + main() diff --git a/src/version.ts b/src/version.ts index 6a6bd737..e5078cdb 100644 --- a/src/version.ts +++ b/src/version.ts @@ -9,4 +9,4 @@ * Regenerate with: node scripts/generate-version.mjs */ -export const VERSION: string = "0.10.0"; +export const VERSION: string = "0.11.0"; diff --git a/test/python/test_docs_release.py b/test/python/test_docs_release.py new file mode 100644 index 00000000..02af5393 --- /dev/null +++ b/test/python/test_docs_release.py @@ -0,0 +1,77 @@ +"""Check that missing or invalid registry metadata cannot permit deployment.""" + +import importlib.util +from pathlib import Path +import unittest +from urllib.error import HTTPError, URLError + +_script = Path(__file__).resolve().parents[2] / "scripts/check_docs_release.py" +_spec = importlib.util.spec_from_file_location("check_docs_release", _script) +assert _spec is not None and _spec.loader is not None +gate = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(gate) + + +NPM = { + "version": "0.11.0", + "dist": {"tarball": "https://registry.npmjs.org/tywrap/-/tywrap-0.11.0.tgz"}, +} +PYPI = {"info": {"version": "0.3.1"}, "urls": [{ + "yanked": False, + "packagetype": "bdist_wheel", + "url": "https://files.pythonhosted.org/package.whl", +}]} + + +class DocsReleaseTests(unittest.TestCase): + def test_requires_both_exact_versions(self) -> None: + calls = [] + + def fetch(url: str) -> object: + calls.append(url) + return NPM if len(calls) == 1 else PYPI + + self.assertTrue(gate.packages_available("0.11.0", "0.3.1", fetch)) + self.assertEqual(calls, [ + "https://registry.npmjs.org/tywrap/0.11.0", + "https://pypi.org/pypi/tywrap-ir/0.3.1/json", + ]) + + def test_missing_either_package_blocks(self) -> None: + for missing in ("npmjs", "pypi"): + with self.subTest(missing=missing): + def fetch(url: str) -> object: + if missing in url: + raise HTTPError(url, 404, "Not Found", {}, None) + return NPM if "npmjs" in url else PYPI + + self.assertFalse(gate.packages_available("0.11.0", "0.3.1", fetch)) + + def test_wrong_or_malformed_metadata_blocks(self) -> None: + for npm, python in [ + ({**NPM, "version": "0.10.0"}, PYPI), + (NPM, {**PYPI, "info": {"version": "0.3.0"}}), + ([], PYPI), + (NPM, {"info": []}), + ({"version": "0.11.0"}, PYPI), + (NPM, {**PYPI, "urls": []}), + (NPM, {**PYPI, "urls": [{**PYPI["urls"][0], "yanked": True}]}), + ]: + with self.subTest(npm=npm, python=python): + responses = iter([npm, python]) + self.assertFalse( + gate.packages_available("0.11.0", "0.3.1", lambda _: next(responses)) + ) + + def test_registry_failure_blocks(self) -> None: + errors = (URLError("unavailable"), TimeoutError(), ValueError("invalid JSON")) + for error in errors: + with self.subTest(error=error): + def fetch(_: str) -> object: + raise error + + self.assertFalse(gate.packages_available("0.11.0", "0.3.1", fetch)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tywrap_ir/pyproject.toml b/tywrap_ir/pyproject.toml index e0871de6..a00243ae 100644 --- a/tywrap_ir/pyproject.toml +++ b/tywrap_ir/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tywrap-ir" -version = "0.3.0" +version = "0.3.1" description = "Python IR extractor for tywrap: emits versioned JSON IR for Python modules" readme = "README.md" authors = [ diff --git a/tywrap_ir/tywrap_ir/__init__.py b/tywrap_ir/tywrap_ir/__init__.py index e9b41190..f3c90daa 100644 --- a/tywrap_ir/tywrap_ir/__init__.py +++ b/tywrap_ir/tywrap_ir/__init__.py @@ -4,7 +4,7 @@ "__version__", ] -__version__ = "0.3.0" +__version__: str = "0.3.1" IR_VERSION = "0.4.0" from .ir import extract_module_ir # noqa: E402