From 8a97ad01d19659b0f9d26e9f51702c354c6bda47 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 22 Jul 2025 11:52:42 -0500 Subject: [PATCH 1/2] Use hatch-vcs for dynamic version --- pyproject.toml | 5 ++++- src/spatch/__init__.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 717c8d4..fe93614 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "spatch" authors = [ {name = "Scientific Python Developers"}, ] -version = "0.0.0" +dynamic = ["version"] description = "Coming soon" readme = "README.md" license = "BSD-3-Clause" @@ -34,6 +34,9 @@ docs = [ "myst-parser", ] +[tool.hatch.version] +source = "vcs" + [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" diff --git a/src/spatch/__init__.py b/src/spatch/__init__.py index 81731bc..5db7839 100644 --- a/src/spatch/__init__.py +++ b/src/spatch/__init__.py @@ -1 +1,18 @@ +from importlib.metadata import version from .utils import from_identifier, get_identifier + +try: + __version__ = version("spatch") +except ModuleNotFoundError: + import warnings + + warnings.warn( + "No version metadata found for spatch, so `spatch.__version__` will be " + "set to None. This may mean that spatch was incorrectly installed or " + "not installed at all. For local development, consider doing an " + "editable install via `python -m pip install -e .` from within the " + "root `spatch/` repository folder." + ) + __version__ = None + del warnings +del version From 17f5b20918a6863b624e232656ebb271f22257af Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 22 Jul 2025 14:56:35 -0500 Subject: [PATCH 2/2] Create and use `get_project_version` utility function --- src/spatch/__init__.py | 19 ++------------- src/spatch/utils.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/spatch/__init__.py b/src/spatch/__init__.py index 5db7839..2bbb596 100644 --- a/src/spatch/__init__.py +++ b/src/spatch/__init__.py @@ -1,18 +1,3 @@ -from importlib.metadata import version -from .utils import from_identifier, get_identifier +from .utils import from_identifier, get_identifier, get_project_version -try: - __version__ = version("spatch") -except ModuleNotFoundError: - import warnings - - warnings.warn( - "No version metadata found for spatch, so `spatch.__version__` will be " - "set to None. This may mean that spatch was incorrectly installed or " - "not installed at all. For local development, consider doing an " - "editable install via `python -m pip install -e .` from within the " - "root `spatch/` repository folder." - ) - __version__ = None - del warnings -del version +__version__ = get_project_version("spatch") diff --git a/src/spatch/utils.py b/src/spatch/utils.py index 327b570..c82a25d 100644 --- a/src/spatch/utils.py +++ b/src/spatch/utils.py @@ -1,7 +1,9 @@ from importlib import import_module +from importlib.metadata import version from dataclasses import dataclass, field import re import sys +import warnings def get_identifier(obj): @@ -17,6 +19,57 @@ def from_identifier(ident): return obj +def get_project_version(project_name, *, action_if_not_found="warn", default=None): + """Get the version of a project from ``importlib.metadata``. + + This is useful to ensure a package is properly installed regardless of the + tools used to build the project and create the version. Proper installation + is important to ensure entry-points of the project are discoverable. If the + project is not found by ``importlib.metadata``, behavior is controlled by + the ``action_if_not_found`` and ``default`` keyword arguments. + + Parameters + ---------- + project_name : str + The name of the project of a package. For example, this is given by the + ``[project.name]`` metadata in the pyproject.toml file. This is often, + but not necessarily, the same as the package name. + action_if_not_found : {"ignore", "warn", "raise"} + What action to take if project metadata is not found by importlib: + "ignore" to return the default value, "warn" to emit a warning and + return the default value, and "raise" to raise a ModuleNotFoundError. + Default is "warn". + default : str or None, optional + The default version to return if project metadata is not found. + Default is None. + """ + if action_if_not_found not in {"ignore", "warn", "raise"}: + raise ValueError( + "`action=` keyword must be 'ignore', 'warn', or 'raise'; " + f"got: {action_if_not_found!r}." + ) + try: + project_version = version(project_name) + except ModuleNotFoundError as exc: + project_version = default + if action_if_not_found == "warn": + behavior = f", so the version will be set to {default!r}" + else: + behavior = "" + msg = ( + f"No version metadata found for {project_name}{behavior}. This " + f"may mean that {project_name} was incorrectly installed or not " + "installed at all. For local development, consider doing an " + "editable install via `python -m pip install -e .` from within " + f"the root `{project_name}/` repository folder." + ) + if action_if_not_found == "warn": + warnings.warn(msg, RuntimeWarning, stacklevel=2) + elif action_if_not_found == "raise": + raise ModuleNotFoundError(msg) from exc + return project_version + + # Valid recommended entry point name, but we could allow more, see: # https://packaging.python.org/en/latest/specifications/entry-points/#data-model _VALID_NAME_RE = re.compile(r"[\w.-]+")