diff --git a/.gitignore b/.gitignore index 052dc9336..5cb6fcc40 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,6 @@ dmypy.json # tempory generated files pyproject-sha.toml + +#version ignore +simpeg_drivers/_version.py diff --git a/pyproject.toml b/pyproject.toml index b5e4bb638..ea1d6cb13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,8 @@ -[build-system] -requires = ["poetry-core>=1.8.0"] -build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=1.8.0", "poetry-dynamic-versioning>=1.9.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" [project] name = "simpeg-drivers" -version = "0.4.0a1" requires-python = '>=3.10,<4.0' description = "Application to run SimPEG inversions with geoh5 files from Geoscience Analyst." @@ -24,7 +22,7 @@ keywords = [ "simpeg", ] readme = "package.rst" -dynamic = ["dependencies", "classifiers"] +dynamic = ["version", "dependencies", "classifiers"] authors = [{ name = "Mira Geoscience", email = "support@mirageoscience.com" }] maintainers = [ { name = "Benjamin Kary", email = "benjamink@mirageoscience.com" }, @@ -64,6 +62,8 @@ include = [ { path = "docs/**/THIRD_PARTY_SOFTWARE.rst" }, ] +version = "0.0.0.dev0" + [tool.poetry.dependencies] # note: py-deps-clock defines custom mapping from dask to dask-core dask = ">=2025.3, <2025.4.dev" # also in simpeg[dask] @@ -143,6 +143,34 @@ pydantic = ">=2.5.2, <3.0.dev" # from geoh5py, geoapps-utils pymatsolver = ">=0.3.0, <0.4.dev" # from simpeg zarr = ">=2.14.2, <2.15.dev" # from simpeg[dask] +[tool.poetry.requires-plugins] +poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] } + +[tool.poetry-dynamic-versioning] +bump = true +enable = true +fix-shallow-repository = true +strict = true +style = "pep440" +vcs = "git" + +[tool.poetry-dynamic-versioning.substitution] +files = ["simpeg_drivers/_version.py", "recipe.yaml"] +patterns = [ + { value = '''(^__version__\s*(?::.*?)?=\s*['"])[^'"]*(['"])''', mode = "str" }, + { value = '''(^\s*version\s*(?::.*?)?:\s*['"])[^'"]*(['"])''', mode = "str" }, +] + +[tool.poetry-dynamic-versioning.files."simpeg_drivers/_version.py"] +persistent-substitution = true +initial-content = """ + # Version placeholder that will be replaced during substitution + __version__ = "0.0.0" +""" + +[tool.poetry-dynamic-versioning.files."recipe.yaml"] +persistent-substitution = true + [tool.ruff] target-version = "py310" diff --git a/recipe.yaml b/recipe.yaml index 9fa5195a3..6198bf422 100644 --- a/recipe.yaml +++ b/recipe.yaml @@ -2,7 +2,7 @@ schema_version: 1 context: name: "simpeg-drivers" - version: "0.4.0a1" + version: "0.0.0.dev0" # This will be replaced by the actual version in the build process python_min: "3.10" package: @@ -20,7 +20,8 @@ build: requirements: host: - python 3.10.* - - poetry-core >=1.0.0 + - poetry-core >=1.8.0 + - poetry-dynamic-versioning >=1.9, <2.0.dev - setuptools - pip run: @@ -57,6 +58,7 @@ tests: - python: imports: - simpeg_drivers + - simpeg_drivers._version - simpeg - geoh5py - dask diff --git a/simpeg_drivers/__init__.py b/simpeg_drivers/__init__.py index 0658a454f..5091c2f51 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -11,12 +11,18 @@ from __future__ import annotations +import logging +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path -__version__ = "0.4.0a1" +try: + from ._version import __version__ +except ModuleNotFoundError: + from datetime import datetime -import logging -from pathlib import Path + __date_str = datetime.today().strftime("%Y%m%d") + __version__ = "0.0.0.dev0+" + __date_str logging.basicConfig(level=logging.INFO) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 9593fd9fd..8d55ad328 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -22,7 +22,16 @@ class SimPEGDriversUIJson(BaseUIJson): - """Base class for simpeg-drivers UIJson.""" + @staticmethod + def _version_public(value): + # Always return only the public part of the version string + return str(Version(str(value)).public) + + from pydantic import field_serializer + + @field_serializer("version") + def serialize_version(self, value): + return self._version_public(value) icon: str documentation: str = "https://mirageoscience-simpeg-drivers.readthedocs-hosted.com/en/stable/intro.html" @@ -30,16 +39,21 @@ class SimPEGDriversUIJson(BaseUIJson): @field_validator("version", mode="before") @classmethod def verify_and_update_version(cls, value: str) -> str: - package_version = cls.comparable_version(simpeg_drivers.__version__) + if not value: + value = simpeg_drivers.__version__ input_version = cls.comparable_version(value) - if input_version != package_version: + input_public = Version(str(value)).public + package_public = Version(simpeg_drivers.__version__).public + if cls.comparable_version(input_public) != cls.comparable_version( + package_public + ): logger.warning( "Provided ui.json file version '%s' does not match the current " "simpeg-drivers version '%s'. This may lead to unpredictable behavior.", value, simpeg_drivers.__version__, ) - return value + return input_public @staticmethod def comparable_version(value: str) -> str: @@ -67,11 +81,12 @@ def comparable_version(value: str) -> str: @classmethod def write_default(cls): - """Write the default UIJson file to disk with updated version.""" + """Write the default UIJson file to disk with updated version (public only).""" with open(cls.default_ui_json, encoding="utf-8") as file: data = json.load(file) - data["version"] = simpeg_drivers.__version__ + # Always write only the public part of the version (no local tag) + data["version"] = str(Version(simpeg_drivers.__version__).public) uijson = cls.model_construct(**data) data = uijson.model_dump_json(indent=4, exclude_unset=False) diff --git a/tests/version_test.py b/tests/version_test.py index 71fd152fe..7f967740e 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -11,9 +11,10 @@ from __future__ import annotations +import importlib from pathlib import Path -import tomli as toml +import pytest import yaml from jinja2 import Template from packaging.version import InvalidVersion, Version @@ -21,15 +22,6 @@ import simpeg_drivers -def get_pyproject_version(): - path = Path(__file__).resolve().parents[1] / "pyproject.toml" - - with open(str(path), encoding="utf-8") as file: - pyproject = toml.loads(file.read()) - - return pyproject["project"]["version"] - - def get_conda_recipe_version(): path = Path(__file__).resolve().parents[1] / "recipe.yaml" @@ -45,10 +37,45 @@ def get_conda_recipe_version(): def test_version_is_consistent(): - assert simpeg_drivers.__version__ == get_pyproject_version() - normalized_conda_version = Version(get_conda_recipe_version()) - normalized_version = Version(simpeg_drivers.__version__) - assert normalized_conda_version == normalized_version + project_version = Version(simpeg_drivers.__version__) + conda_version = Version(get_conda_recipe_version()) + assert conda_version.base_version == project_version.base_version + + +def _version_module_exists(): + try: + importlib.import_module("simpeg_drivers._version") + + return True + except ModuleNotFoundError: + return False + + +@pytest.mark.skipif( + _version_module_exists(), + reason="simpeg_drivers._version can be found: package is built", +) +def test_fallback_version_is_zero(): + project_version = Version(simpeg_drivers.__version__) + fallback_version = Version("0.0.0.dev0") + assert project_version.base_version == fallback_version.base_version + assert project_version.pre is None + assert project_version.post is None + assert project_version.dev == fallback_version.dev + + +@pytest.mark.skipif( + not _version_module_exists(), + reason="simpeg_drivers._version cannot be found: uses a fallback version", +) +def test_conda_version_is_consistent(): + project_version = Version(simpeg_drivers.__version__) + conda_version = Version(get_conda_recipe_version()) + + assert conda_version.is_devrelease == project_version.is_devrelease + assert conda_version.is_prerelease == project_version.is_prerelease + assert conda_version.is_postrelease == project_version.is_postrelease + assert conda_version == project_version def test_conda_version_is_pep440():