diff --git a/docs/python-extension.md b/docs/python-extension.md index 7cfe6644..523323a9 100644 --- a/docs/python-extension.md +++ b/docs/python-extension.md @@ -21,6 +21,13 @@ available there. | Target | `python` | | IR versions | `3`, `4` | | MEP version | `0.1` | +| Multiple source documents | `multiDocument: true`, from `0.4.2` | + +Extension `0.4.2` pairs with `morphir-python==0.1.0` and +`morphir-python-native==0.1.1`, qualified with `morphir==0.1.0`. +The native API reports compiler version `0.4.2`. +Use `0.4.2` or later with hosts that require an explicit multi-document claim; +`0.4.1` omits that claim. The declaration is in [`rust/extension.toml`](../rust/extension.toml). diff --git a/packages/morphir-tools/README.md b/packages/morphir-tools/README.md index de82c229..8131ba3b 100644 --- a/packages/morphir-tools/README.md +++ b/packages/morphir-tools/README.md @@ -63,7 +63,7 @@ format-version rules and the draft v4 status. ## Requirements - Python 3.14+ -- `morphir>=0.0.2` and `morphir-python-native==0.1.0` (installed as dependencies) +- `morphir>=0.1.0` and `morphir-python-native==0.1.1` (installed as dependencies) ## License diff --git a/packages/morphir-tools/pyproject.toml b/packages/morphir-tools/pyproject.toml index 0a894cd8..b9892451 100644 --- a/packages/morphir-tools/pyproject.toml +++ b/packages/morphir-tools/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "morphir-python" -version = "0.0.3" +version = "0.1.0" description = "CLI tools and extensions for Morphir Python" readme = "README.md" license = "Apache-2.0" @@ -22,9 +22,9 @@ authors = [ { name = "FINOS", email = "morphir@finos.org" } ] dependencies = [ - "morphir>=0.0.2", + "morphir>=0.1.0", # Exact qualified native compiler; checked again when the compiler first loads. - "morphir-python-native==0.1.0", + "morphir-python-native==0.1.1", "typer>=0.15", # Rich uses Pygments; older versions are affected by CVE-2026-4539. "pygments>=2.20.0", diff --git a/packages/morphir-tools/src/morphir_tools/__init__.py b/packages/morphir-tools/src/morphir_tools/__init__.py index 993831fc..22222dd3 100644 --- a/packages/morphir-tools/src/morphir_tools/__init__.py +++ b/packages/morphir-tools/src/morphir_tools/__init__.py @@ -6,8 +6,8 @@ Example: >>> import morphir_tools >>> morphir_tools.__version__ - '0.0.3' + '0.1.0' """ -__version__ = "0.0.3" +__version__ = "0.1.0" __all__ = ["__version__"] diff --git a/packages/morphir-tools/src/morphir_tools/python/_bridge.py b/packages/morphir-tools/src/morphir_tools/python/_bridge.py index 8d151a4c..e23a4bd1 100644 --- a/packages/morphir-tools/src/morphir_tools/python/_bridge.py +++ b/packages/morphir-tools/src/morphir_tools/python/_bridge.py @@ -8,7 +8,7 @@ from ._types import NativeCompilerInfo, NativeCompilerUnavailable -EXPECTED_NATIVE_VERSION = "0.1.0" +EXPECTED_NATIVE_VERSION = "0.1.1" """The exact ``morphir-python-native`` version this release was qualified with. It must equal the pin in this distribution's ``pyproject.toml``. diff --git a/packages/morphir/pyproject.toml b/packages/morphir/pyproject.toml index 11b120be..911bc14d 100644 --- a/packages/morphir/pyproject.toml +++ b/packages/morphir/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "morphir" -version = "0.0.2" +version = "0.1.0" description = "Python port of Morphir - functional domain modeling for the enterprise" readme = "README.md" license = "Apache-2.0" diff --git a/packages/morphir/src/morphir/__init__.py b/packages/morphir/src/morphir/__init__.py index 9f166001..8074c7d1 100644 --- a/packages/morphir/src/morphir/__init__.py +++ b/packages/morphir/src/morphir/__init__.py @@ -7,8 +7,8 @@ Example: >>> import morphir >>> morphir.__version__ - '0.0.2' + '0.1.0' """ -__version__ = "0.0.2" +__version__ = "0.1.0" __all__ = ["__version__"] diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 75938590..450204fc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1460,7 +1460,7 @@ dependencies = [ [[package]] name = "morphir-python-binding" -version = "0.4.1" +version = "0.4.2" dependencies = [ "cucumber", "morphir-core", @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "morphir-python-native" -version = "0.1.0" +version = "0.1.1" dependencies = [ "morphir-extension-sdk", "morphir-python-binding", diff --git a/rust/crates/morphir-python-binding/Cargo.toml b/rust/crates/morphir-python-binding/Cargo.toml index c9bc3b9a..81ff6904 100644 --- a/rust/crates/morphir-python-binding/Cargo.toml +++ b/rust/crates/morphir-python-binding/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-binding" -version = "0.4.1" +version = "0.4.2" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/rust/crates/morphir-python-binding/src/lib.rs b/rust/crates/morphir-python-binding/src/lib.rs index 3c8da561..77c08d98 100644 --- a/rust/crates/morphir-python-binding/src/lib.rs +++ b/rust/crates/morphir-python-binding/src/lib.rs @@ -58,6 +58,7 @@ impl Extension for PythonExtension { }], ir_versions: vec!["3".into(), "4".into()], compile: true, + multi_document: true, ..Default::default() }), backend: Some(BackendCapability { diff --git a/rust/crates/morphir-python-binding/tests/ir_versions.rs b/rust/crates/morphir-python-binding/tests/ir_versions.rs index 3054a881..ce1c2f1f 100644 --- a/rust/crates/morphir-python-binding/tests/ir_versions.rs +++ b/rust/crates/morphir-python-binding/tests/ir_versions.rs @@ -65,8 +65,15 @@ fn generate(ir: Value) -> GenerateResult { #[test] fn both_versions_preserve_imports_privacy_tuples_and_conditionals() { + assert!( + PythonExtension::capabilities() + .frontend + .unwrap() + .multi_document + ); for version in ["3", "3.0.0", "4", "4.0.0"] { let mut request = a_request(version); + assert_eq!(request.sources.documents.len(), 2); let ir = compile(request.clone()); if version.starts_with('3') { let decoded: classic::Distribution = serde_json::from_value(ir.clone()).unwrap(); diff --git a/rust/crates/morphir-python-binding/tests/pipeline.rs b/rust/crates/morphir-python-binding/tests/pipeline.rs index c464cb1d..41a1dc8a 100644 --- a/rust/crates/morphir-python-binding/tests/pipeline.rs +++ b/rust/crates/morphir-python-binding/tests/pipeline.rs @@ -107,6 +107,11 @@ fn rejects_unsupported_or_ambiguous_source_without_partial_ir() { #[test] fn advertises_and_enforces_the_initial_contract() { let capabilities = PythonExtension::capabilities(); + assert!(capabilities.frontend.as_ref().unwrap().multi_document); + assert_eq!( + serde_json::to_value(&capabilities).unwrap()["frontend"]["multiDocument"], + json!(true) + ); assert_eq!(capabilities.frontend.unwrap().ir_versions, ["3", "4"]); assert_eq!(capabilities.backend.unwrap().targets, ["python"]); let mut request = a_request(""); diff --git a/rust/crates/morphir-python-binding/tests/support/native.rs b/rust/crates/morphir-python-binding/tests/support/native.rs index 762d5a0c..cdcd69ee 100644 --- a/rust/crates/morphir-python-binding/tests/support/native.rs +++ b/rust/crates/morphir-python-binding/tests/support/native.rs @@ -1,7 +1,7 @@ //! Native protocol fixture with a completed initialization handshake. use morphir_extension_sdk::{ - NativeExtension, + Extension, NativeExtension, protocol::{ExtensionRequest, InitializeParams, MEP_VERSION, PeerInfo, methods}, }; use morphir_python_binding::PythonExtension; @@ -25,5 +25,13 @@ pub fn an_initialized_extension() -> NativeExtension { .unwrap(), ); assert!(response.error.is_none(), "{:?}", response.error); + let expected = serde_json::to_value(PythonExtension::capabilities()).unwrap(); + assert_eq!(expected["frontend"]["multiDocument"], true); + assert_eq!(response.result.unwrap()["capabilities"], expected); + let response = extension + .protocol() + .handle(ExtensionRequest::new(methods::CAPABILITIES, serde_json::json!({}), 1).unwrap()); + assert!(response.error.is_none(), "{:?}", response.error); + assert_eq!(response.result.unwrap(), expected); extension } diff --git a/rust/crates/morphir-python-native/Cargo.toml b/rust/crates/morphir-python-native/Cargo.toml index 488d1080..c6b9735d 100644 --- a/rust/crates/morphir-python-native/Cargo.toml +++ b/rust/crates/morphir-python-native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "morphir-python-native" -version = "0.1.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/scripts/extension_bundle.py b/scripts/extension_bundle.py index 8d057534..560aac84 100644 --- a/scripts/extension_bundle.py +++ b/scripts/extension_bundle.py @@ -198,6 +198,7 @@ def receipt(bundle: Path) -> dict[str, object]: if not ( isinstance(frontend, dict) and frontend.get("compile") is True + and frontend.get("multiDocument") is True and frontend.get("irVersions") == extension["ir_versions"] and isinstance(backend, dict) and backend.get("generate") is True @@ -212,6 +213,7 @@ def receipt(bundle: Path) -> dict[str, object]: "git_commit": descriptor.get("gitCommit"), "packager_rev": declaration()["packager"]["rev"], "capabilities": sorted(k for k, v in capabilities.items() if v), + "multiDocument": frontend["multiDocument"], } @@ -258,6 +260,8 @@ def morphir_cli() -> tuple[Path, str]: def host_check(bundle: Path) -> dict[str, object]: """Install the bundle with the pinned CLI and round trip offline.""" + from morphir_tools.python import native_compiler + bundle = bundle.resolve() described = receipt(bundle) cli, cli_digest = morphir_cli() @@ -335,6 +339,10 @@ def morphir(*args: str, offline: bool = False) -> str: return { **described, "host": {"morphir": declaration()["host"]["version"], "sha256": cli_digest}, + "python_api": { + "native_version": native_compiler().version, + "compiler_version": native_compiler().compiler_version, + }, "offline_round_trips": results, } @@ -348,8 +356,15 @@ def agrees_with_api(document: dict[str, Any], tag: str) -> bool: IrVersion, SourceDocument, compile_sources, + native_compiler, ) + compiler = native_compiler() + if compiler.compiler_version != crate_version(): + raise BundleError( + f"API compiler {compiler.compiler_version} differs from " + f"bundle {crate_version()}" + ) sources = [ SourceDocument(p.relative_to(SAMPLE).as_posix(), p.read_text()) for p in sorted(SAMPLE.rglob("*.py")) diff --git a/tests/conftest.py b/tests/conftest.py index 0ac7ca8b..f95a2880 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,4 +6,4 @@ @pytest.fixture def sample_version() -> str: """Provide the expected version string for tests.""" - return "0.0.2" + return "0.1.0" diff --git a/tests/tools/test_tools.py b/tests/tools/test_tools.py index 19dcf3bd..d672818f 100644 --- a/tests/tools/test_tools.py +++ b/tests/tools/test_tools.py @@ -14,7 +14,7 @@ def test_version_is_defined(self) -> None: def test_version_matches_expected(self) -> None: """The version should match the expected value.""" - assert morphir_tools.__version__ == "0.0.3" + assert morphir_tools.__version__ == "0.1.0" def test_installed_distribution_name(self) -> None: """The tools import belongs to the morphir-python distribution.""" @@ -32,7 +32,7 @@ def test_installed_console_command(self) -> None: def test_cli_requires_separate_core(self) -> None: """The CLI must install the independent library and SDK distribution.""" requirements = distribution("morphir-python").requires or [] - assert "morphir>=0.0.2" in requirements + assert "morphir>=0.1.0" in requirements def test_version_is_string(self) -> None: """The version should be a string.""" diff --git a/tests/unit/test_extension_bundle.py b/tests/unit/test_extension_bundle.py new file mode 100644 index 00000000..c4d81cee --- /dev/null +++ b/tests/unit/test_extension_bundle.py @@ -0,0 +1,53 @@ +"""Require explicit multi-document claims in Python release bundles.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING + +import pytest + +from scripts import extension_bundle as bundle + +if TYPE_CHECKING: + from pathlib import Path + + +@pytest.mark.parametrize("multi_document", [None, False, True]) +def test_bundle_requires_multi_document_claim( + tmp_path: Path, multi_document: bool | None +) -> None: + version = bundle.crate_version() + artifact = f"morphir-python-binding-{version}.wasm" + wasm = b"qualification fixture" + digest = bundle.sha256(wasm) + (tmp_path / artifact).write_bytes(wasm) + (tmp_path / f"{artifact}.sha256").write_text(f"{digest} {artifact}\n") + frontend: dict[str, object] = {"compile": True, "irVersions": ["3", "4"]} + if multi_document is not None: + frontend["multiDocument"] = multi_document + (tmp_path / "release.json").write_text( + json.dumps( + { + "extensionId": "morphir-python", + "version": version, + "artifacts": [ + { + "sha256": digest, + "claims": { + "extension": {"id": "morphir-python", "version": version}, + "capabilities": { + "frontend": frontend, + "backend": {"generate": True, "targets": ["python"]}, + }, + }, + } + ], + } + ) + ) + if multi_document is True: + assert bundle.receipt(tmp_path)["multiDocument"] is True + else: + with pytest.raises(bundle.BundleError, match="claims lack"): + bundle.receipt(tmp_path) diff --git a/uv.lock b/uv.lock index ac82decb..3d9732bb 100644 --- a/uv.lock +++ b/uv.lock @@ -196,12 +196,12 @@ wheels = [ [[package]] name = "morphir" -version = "0.0.2" +version = "0.1.0" source = { editable = "packages/morphir" } [[package]] name = "morphir-python" -version = "0.0.3" +version = "0.1.0" source = { editable = "packages/morphir-tools" } dependencies = [ { name = "morphir" },