From d03167242a9bf77eebda82fe7334a917b1bf6e2b Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 20:26:51 -0400 Subject: [PATCH 01/12] Update versioning strategy and implement dynamic versioning support --- .gitignore | 3 +++ pyproject.toml | 38 ++++++++++++++++++++++---- recipe.yaml | 6 +++-- simpeg_drivers/__init__.py | 11 +++++++- tests/version_test.py | 55 ++++++++++++++++++++++++++++---------- 5 files changed, 91 insertions(+), 22 deletions(-) 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..4a34e4b22 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -12,12 +12,21 @@ from __future__ import annotations -__version__ = "0.4.0a1" +from importlib.metadata import PackageNotFoundError, version import logging from pathlib import Path +try: + from ._version import __version__ +except ModuleNotFoundError: # pragma: no cover + from datetime import datetime + + __date_str = datetime.today().strftime("%Y%m%d") + __version__ = "0.0.0.dev0+" + __date_str + + logging.basicConfig(level=logging.INFO) 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(): From 36c8ed8ef5b3d819ffe7b9f900e63ae2641b632d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:28:47 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simpeg_drivers/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/simpeg_drivers/__init__.py b/simpeg_drivers/__init__.py index 4a34e4b22..ea4e98175 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -11,13 +11,11 @@ from __future__ import annotations - -from importlib.metadata import PackageNotFoundError, version - - import logging +from importlib.metadata import PackageNotFoundError, version from pathlib import Path + try: from ._version import __version__ except ModuleNotFoundError: # pragma: no cover @@ -27,7 +25,6 @@ __version__ = "0.0.0.dev0+" + __date_str - logging.basicConfig(level=logging.INFO) __all__ = ["DRIVER_MAP", "assets_path"] From 7e868280473bd85477259aa76037247e1673fdb6 Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 20:53:20 -0400 Subject: [PATCH 03/12] Reorder import statements for improved readability --- simpeg_drivers/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/simpeg_drivers/__init__.py b/simpeg_drivers/__init__.py index 4a34e4b22..ea4e98175 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -11,13 +11,11 @@ from __future__ import annotations - -from importlib.metadata import PackageNotFoundError, version - - import logging +from importlib.metadata import PackageNotFoundError, version from pathlib import Path + try: from ._version import __version__ except ModuleNotFoundError: # pragma: no cover @@ -27,7 +25,6 @@ __version__ = "0.0.0.dev0+" + __date_str - logging.basicConfig(level=logging.INFO) __all__ = ["DRIVER_MAP", "assets_path"] From 6d16ce5e25aa91ae5f630b82a4165912fd86696c Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 20:57:41 -0400 Subject: [PATCH 04/12] Fix version validation in UIJson to default to package version if not provided --- simpeg_drivers/__init__.py | 2 +- simpeg_drivers/uijson.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/simpeg_drivers/__init__.py b/simpeg_drivers/__init__.py index ea4e98175..5091c2f51 100644 --- a/simpeg_drivers/__init__.py +++ b/simpeg_drivers/__init__.py @@ -18,7 +18,7 @@ try: from ._version import __version__ -except ModuleNotFoundError: # pragma: no cover +except ModuleNotFoundError: from datetime import datetime __date_str = datetime.today().strftime("%Y%m%d") diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 9593fd9fd..59aa00324 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -30,16 +30,19 @@ 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: @@ -71,7 +74,7 @@ def write_default(cls): with open(cls.default_ui_json, encoding="utf-8") as file: data = json.load(file) - data["version"] = simpeg_drivers.__version__ + data["version"] = Version(simpeg_drivers.__version__).public uijson = cls.model_construct(**data) data = uijson.model_dump_json(indent=4, exclude_unset=False) From 81638e1a7f0f13b100b9d2aa1b88c75dd8100a70 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 00:58:06 +0000 Subject: [PATCH 05/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simpeg_drivers/uijson.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 59aa00324..3dc208eb1 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -35,7 +35,9 @@ def verify_and_update_version(cls, value: str) -> str: input_version = cls.comparable_version(value) input_public = Version(str(value)).public package_public = Version(simpeg_drivers.__version__).public - if cls.comparable_version(input_public) != cls.comparable_version(package_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.", From 0e7e6ae807cdbb8bddc426f43e3feb721227c071 Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 21:07:37 -0400 Subject: [PATCH 06/12] Update write_default method to clarify version writing as public only --- simpeg_drivers/uijson.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 3dc208eb1..81c30175b 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -72,11 +72,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"] = Version(simpeg_drivers.__version__).public + # 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) From eee91a0868600a5b574a10ec42694706a82294df Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 22:00:48 -0400 Subject: [PATCH 07/12] Add version serialization to SimPEGDriversUIJson for public version handling --- simpeg_drivers/uijson.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 81c30175b..c4c5139ca 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -22,6 +22,23 @@ class SimPEGDriversUIJson(BaseUIJson): + @classmethod + def __get_pydantic_core_schema__(cls, *args, **kwargs): + # This is a workaround for Pydantic v2 to allow field_serializer below to work on inherited models + from pydantic import GetCoreSchemaHandler + schema = super().__get_pydantic_core_schema__(*args, **kwargs) + handler = GetCoreSchemaHandler(schema) + return handler(schema) + + @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) """Base class for simpeg-drivers UIJson.""" icon: str From 911612747a63d136907c932f8c6b265e23219ed0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 02:02:03 +0000 Subject: [PATCH 08/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simpeg_drivers/uijson.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index c4c5139ca..2dae44a0b 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -26,6 +26,7 @@ class SimPEGDriversUIJson(BaseUIJson): def __get_pydantic_core_schema__(cls, *args, **kwargs): # This is a workaround for Pydantic v2 to allow field_serializer below to work on inherited models from pydantic import GetCoreSchemaHandler + schema = super().__get_pydantic_core_schema__(*args, **kwargs) handler = GetCoreSchemaHandler(schema) return handler(schema) @@ -36,9 +37,11 @@ def _version_public(value): return str(Version(str(value)).public) from pydantic import field_serializer - @field_serializer('version') + + @field_serializer("version") def serialize_version(self, value): return self._version_public(value) + """Base class for simpeg-drivers UIJson.""" icon: str From 5c475cb02ffa4c34e9bc401a98ced0ab849af3fe Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 22:09:46 -0400 Subject: [PATCH 09/12] Remove workaround for Pydantic v2 in SimPEGDriversUIJson to streamline schema handling --- simpeg_drivers/uijson.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index c4c5139ca..40b26a8cb 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -22,14 +22,6 @@ class SimPEGDriversUIJson(BaseUIJson): - @classmethod - def __get_pydantic_core_schema__(cls, *args, **kwargs): - # This is a workaround for Pydantic v2 to allow field_serializer below to work on inherited models - from pydantic import GetCoreSchemaHandler - schema = super().__get_pydantic_core_schema__(*args, **kwargs) - handler = GetCoreSchemaHandler(schema) - return handler(schema) - @staticmethod def _version_public(value): # Always return only the public part of the version string From d56832cfee9af9d91694afd30ea455e082bea16a Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 22:14:56 -0400 Subject: [PATCH 10/12] Remove unnecessary docstring from SimPEGDriversUIJson class --- simpeg_drivers/uijson.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 2dae44a0b..65027a23b 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -42,8 +42,6 @@ def _version_public(value): def serialize_version(self, value): return self._version_public(value) - """Base class for simpeg-drivers UIJson.""" - icon: str documentation: str = "https://mirageoscience-simpeg-drivers.readthedocs-hosted.com/en/stable/intro.html" From b70ab2cff30515e602cc24f427f21f671bd836fc Mon Sep 17 00:00:00 2001 From: saicheran Date: Wed, 27 Aug 2025 22:22:11 -0400 Subject: [PATCH 11/12] Remove Pydantic v2 workaround from SimPEGDriversUIJson class --- simpeg_drivers/uijson.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 65027a23b..0aef4448d 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -22,14 +22,6 @@ class SimPEGDriversUIJson(BaseUIJson): - @classmethod - def __get_pydantic_core_schema__(cls, *args, **kwargs): - # This is a workaround for Pydantic v2 to allow field_serializer below to work on inherited models - from pydantic import GetCoreSchemaHandler - - schema = super().__get_pydantic_core_schema__(*args, **kwargs) - handler = GetCoreSchemaHandler(schema) - return handler(schema) @staticmethod def _version_public(value): From 7d28f76d9cf1ae728be2fe9601ad180952b53d6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 02:22:39 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- simpeg_drivers/uijson.py | 1 - 1 file changed, 1 deletion(-) diff --git a/simpeg_drivers/uijson.py b/simpeg_drivers/uijson.py index 0aef4448d..8d55ad328 100644 --- a/simpeg_drivers/uijson.py +++ b/simpeg_drivers/uijson.py @@ -22,7 +22,6 @@ class SimPEGDriversUIJson(BaseUIJson): - @staticmethod def _version_public(value): # Always return only the public part of the version string