From 7b7473678bfb158f8936e4f4674b4125875c01fb Mon Sep 17 00:00:00 2001 From: Adeel Asghar Date: Thu, 24 Sep 2026 18:07:14 +0200 Subject: [PATCH] Add version resolution for OMPython with tests and CI checks Add OMPython._version to resolve the installed package version via importlib.metadata, falling back to reading pyproject.toml when running from an uninstalled source tree. Exposes __version__ and get_version(). Only install tomli on Python < 3.11, since tomllib is available in the standard library from 3.11 onward. Add unit tests covering both resolution paths: the installed-metadata path and the pyproject.toml fallback, including the case where both fail and resolution degrades to "unknown" instead of raising. Update CI workflow to print the resolved OMPython version, fail the build if it resolves to "unknown", and surface it via step output and job summary. --- .github/workflows/Test.yml | 15 +++++++++ OMPython/__init__.py | 6 ++++ OMPython/_version.py | 51 +++++++++++++++++++++++++++++ pyproject.toml | 1 + tests/test_version.py | 67 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 OMPython/_version.py create mode 100644 tests/test_version.py diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 133852cd..d60d39d2 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -69,6 +69,21 @@ jobs: - name: Build wheel and sdist packages run: python -m build --wheel --sdist --outdir dist + - name: OMPython version + id: ompython_version + shell: bash + run: | + V=$(python -c " + import OMPython + v = OMPython.__version__ + assert v != 'unknown', 'Version resolution failed' + print(v) + ") + echo "Resolved OMPython version: $V" + echo "version=$V" >> "$GITHUB_OUTPUT" + echo "### OMPython Version" >> "$GITHUB_STEP_SUMMARY" + echo "\`$V\`" >> "$GITHUB_STEP_SUMMARY" + - name: Check twine run: python -m twine check dist/* diff --git a/OMPython/__init__.py b/OMPython/__init__.py index 848421c5..3a81235a 100644 --- a/OMPython/__init__.py +++ b/OMPython/__init__.py @@ -9,8 +9,11 @@ omc.sendExpression("getVersion()") ``` + """ +from OMPython._version import __version__, get_version + from OMPython.model_execution import ( ModelExecutionConfig, ModelExecutionRun, @@ -77,6 +80,9 @@ # global names imported if import 'from OMPython import *' is used __all__ = [ + '__version__', + 'get_version', + 'doe_get_solutions', 'LinearizationResult', diff --git a/OMPython/_version.py b/OMPython/_version.py new file mode 100644 index 00000000..cebacd10 --- /dev/null +++ b/OMPython/_version.py @@ -0,0 +1,51 @@ +"""Version information for OMPython. + +Resolves the installed package version via importlib.metadata when +OMPython is installed (pip install, editable install, etc.), and falls +back to reading pyproject.toml directly when running from a source +tree with no installed metadata available. +""" + +import sys +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +_FALLBACK_VERSION = "unknown" + + +def _read_version_from_pyproject() -> str: + """Return the package version from the pyproject.toml file. + + Raises: + FileNotFoundError: if pyproject.toml cannot be found. + KeyError: if the version is not statically defined (e.g. dynamic versioning is used). + """ + pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml" + with open(pyproject_path, "rb") as pyproject_file: + pyproject = tomllib.load(pyproject_file) + return pyproject["project"]["version"] + + +def _resolve_version() -> str: + try: + return version(__package__ or "OMPython") + except PackageNotFoundError: + pass + + try: + return _read_version_from_pyproject() + except (FileNotFoundError, KeyError): + return _FALLBACK_VERSION + + +def get_version() -> str: + """Return the OMPython version string.""" + return __version__ + + +__version__ = _resolve_version() diff --git a/pyproject.toml b/pyproject.toml index 4b36b74b..ddff139c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "psutil", "pyparsing", "pyzmq", + "tomli; python_version < '3.11'", ] [tool.setuptools] diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..a9f9271f --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,67 @@ +"""Tests for OMPython._version.""" + +import importlib.metadata +from OMPython import _version + + +def test_version_module_resolves_installed_metadata(): + """When OMPython is installed, __version__ should come from + importlib.metadata and never fall back to 'unknown'.""" + assert _version.__version__ != "unknown" + assert isinstance(_version.__version__, str) + assert _version.__version__ != "" + + +def test_get_version_matches_module_version(): + """get_version() should return the same value as __version__.""" + assert _version.get_version() == _version.__version__ + + +def test_installed_metadata_matches_importlib(monkeypatch=None): + """Sanity check: the resolved version matches what importlib.metadata + reports directly, confirming the installed-metadata path is being used.""" + expected = importlib.metadata.version(_version.__package__ or "OMPython") + assert _version.__version__ == expected + + +def test_resolve_version_falls_back_when_package_not_found(monkeypatch): + """If importlib.metadata.version() raises PackageNotFoundError, + _resolve_version() should fall back to reading pyproject.toml.""" + + def raise_not_found(_name): + raise importlib.metadata.PackageNotFoundError + + monkeypatch.setattr(_version, "version", raise_not_found) + + result = _version._resolve_version() + + # Falls back to reading pyproject.toml; should not silently return + # "unknown" unless pyproject.toml is genuinely missing/malformed. + assert result != "unknown" + assert isinstance(result, str) + + +def test_resolve_version_returns_unknown_if_pyproject_unreadable(monkeypatch): + """If both importlib.metadata and the pyproject.toml fallback fail, + _resolve_version() should return 'unknown' rather than raising.""" + + def raise_not_found(_name): + raise importlib.metadata.PackageNotFoundError + + def raise_file_not_found(): + raise FileNotFoundError + + monkeypatch.setattr(_version, "version", raise_not_found) + monkeypatch.setattr(_version, "_read_version_from_pyproject", raise_file_not_found) + + result = _version._resolve_version() + + assert result == "unknown" + + +def test_read_version_from_pyproject_returns_string(): + """_read_version_from_pyproject() should return a non-empty string + when pyproject.toml is present and well-formed.""" + result = _version._read_version_from_pyproject() + assert isinstance(result, str) + assert result != ""