From c535dfc426d17de4418c109a4fbde51fd717f55d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Thu, 22 Sep 2022 13:14:57 -0500 Subject: [PATCH 1/3] Use static versioning over setuptools_scm --- .gitignore | 3 --- dev-environment.yml | 2 +- pyproject.toml | 7 ++---- src/scyjava/__init__.py | 32 +++++---------------------- src/scyjava/_version.py | 16 ++++++++++++++ tests/test_version.py | 48 +++++++++++++---------------------------- 6 files changed, 39 insertions(+), 69 deletions(-) create mode 100644 src/scyjava/_version.py diff --git a/.gitignore b/.gitignore index fbcac6bc..7fdf6cba 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,6 @@ /.eggs/ *egg-info/ -# setuptools_scm -/src/*/_version.py - # Vi *.swp diff --git a/dev-environment.yml b/dev-environment.yml index 871819e0..02d70dde 100644 --- a/dev-environment.yml +++ b/dev-environment.yml @@ -27,7 +27,7 @@ dependencies: - flake8 - pytest - pytest-cov - - setuptools-scm >= 6.2 + - toml # Project from source - pip - pip: diff --git a/pyproject.toml b/pyproject.toml index b8ea7251..dff67d12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "scyjava" +version = "1.5.2.dev0" authors = [ {name = "Curtis Rueden", email = "ctrueden@wisc.edu"}, {name = "Philipp Hanslovsky"}, @@ -38,8 +39,6 @@ dependencies = [ "jpype1 >= 1.3.0", "jgo", ] -dynamic = ["version"] - [project.urls] Homepage = "https://github.com/scijava/scyjava" @@ -58,7 +57,7 @@ dev = [ "pytest-cov", "numpy", "pandas", - "setuptools-scm >= 6.2", + "toml", ] [tool.setuptools] @@ -69,5 +68,3 @@ include-package-data = false [tool.setuptools.packages.find] where = ["src"] namespaces = false - -[tool.setuptools_scm] diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index c9eb3fb7..0bca244a 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -25,9 +25,14 @@ JLong, JShort, ) +from scyjava._version import _find_version import scyjava.config + +__version__ = _find_version() + + _logger = logging.getLogger(__name__) # Set of module properties @@ -65,33 +70,6 @@ def __getattr__(name): raise AttributeError(f"module '{__name__}' has no attribute '{name}'") -@constant -def ___version__(): - # First pass: use the version output by setuptools_scm - try: - import scyjava.version - - return scyjava.version.version - except ImportError: - pass - # Second pass: use importlib.metadata - try: - from importlib.metadata import PackageNotFoundError, version - - return version("scyjava") - except ImportError or PackageNotFoundError: - pass - # Third pass: use pkg_resources - try: - from pkg_resources import get_distribution - - return get_distribution("scyjava").version - except ImportError: - pass - # Fourth pass: Give up - return "Cannot determine version! Ensure pkg_resources is installed!" - - # -- JVM setup -- _startup_callbacks = [] diff --git a/src/scyjava/_version.py b/src/scyjava/_version.py new file mode 100644 index 00000000..ce45f429 --- /dev/null +++ b/src/scyjava/_version.py @@ -0,0 +1,16 @@ +from importlib.util import find_spec + + +def _find_version(): + # First pass: use importlib.metadata + if find_spec("importlib.metadata"): + from importlib.metadata import version + + return version("scyjava") + + if find_spec("pkg_resources"): + from pkg_resources import get_distribution + + return get_distribution("scyjava").version + # Fourth pass: Give up + return "Cannot determine version! Ensure pkg_resources is installed!" diff --git a/tests/test_version.py b/tests/test_version.py index b8a03860..71af4545 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,37 +1,25 @@ -import os import sys +import toml +from pathlib import Path import pytest - -setuptools_file = os.path.join(os.getcwd(), "src", "scyjava", "_version.py") +import scyjava +from scyjava._version import _find_version def _scyjava_version(): """ Get ScyJava's version. """ - import scyjava - - # It's important that we clear the cache here, - # so that we can test different behaviors. - scyjava.___version__.cache_clear() - # Get the version - return scyjava.__version__ - + pyproject = toml.load(Path(__file__).parents[1] / "pyproject.toml") + return pyproject["project"]["version"] -def test_version_file(): - """Ensures that, ideally, the version from setuptools_scm is used""" - # Get the version from setuptools_scm - from setuptools_scm import get_version - setuptools_version = get_version(write_to="src/scyjava/_version.py") - # Ensure that the version was written to file - assert os.path.isfile(setuptools_file) - # Ensure that scyjava.__version__ matches this. - assert _scyjava_version() == setuptools_version - # Cleanup - remove file - os.remove(setuptools_file) - assert not os.path.isfile(setuptools_file) +def test_version_dunder(): + """ + Ensures that the dunder variable matches _scyjava_version + """ + assert scyjava.__version__ == _scyjava_version() @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") @@ -43,9 +31,8 @@ def test_version_importlib(): # Remove scyjava.version sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - from importlib.metadata import version - assert _scyjava_version() == version("scyjava") + assert _scyjava_version() == _find_version() @pytest.mark.skipif( @@ -57,15 +44,12 @@ def test_version_pkg_resources(): importlib.metadata unavailable, pkg_resources is used next. """ - # Remove scyjava.version - sys.modules["scyjava.version"] = None # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - from pkg_resources import get_distribution - assert _scyjava_version() == get_distribution("scyjava").version + assert _scyjava_version() == _find_version() def test_version_unvailable(): @@ -73,14 +57,12 @@ def test_version_unvailable(): Ensures that no version is returned if none of these strategies works. """ - # Remove scyjava.version - sys.modules["scyjava.version"] = None # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Remove pkg_resources sys.modules["pkg_resources"] = None # Ensure scyjava.__version__ is an error message. assert ( - _scyjava_version() - == "Cannot determine version! Ensure pkg_resources is installed!" + "Cannot determine version! Ensure pkg_resources is installed!" + == _find_version() ) From 1d608ff71775283f0903a1813374825df5c6de98 Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 13:49:22 -0500 Subject: [PATCH 2/3] test_version: do some minor cleanups * Rename _scyjava_version to _expected_version, for clarity. * Use imperative tense for docstrings, as per Python convention. * Terminate each docstring sentence with a period. --- tests/test_version.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index 71af4545..2c7a8d1a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,9 +7,9 @@ from scyjava._version import _find_version -def _scyjava_version(): +def _expected_version(): """ - Get ScyJava's version. + Get the project version from pyproject.toml. """ pyproject = toml.load(Path(__file__).parents[1] / "pyproject.toml") return pyproject["project"]["version"] @@ -17,22 +17,22 @@ def _scyjava_version(): def test_version_dunder(): """ - Ensures that the dunder variable matches _scyjava_version + Ensure that the dunder variable matches _expected_version. """ - assert scyjava.__version__ == _scyjava_version() + assert _expected_version() == scyjava.__version__ @pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python >= 3.8") def test_version_importlib(): """ - Ensures that, with scyjava.version.version unavailable, - importlib.metadata is used next WITH python 3.8+ + Ensure that, with scyjava.version.version unavailable, + importlib.metadata is used next WITH python 3.8+. """ # Remove scyjava.version sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - assert _scyjava_version() == _find_version() + assert _expected_version() == _find_version() @pytest.mark.skipif( @@ -40,7 +40,7 @@ def test_version_importlib(): ) def test_version_pkg_resources(): """ - Ensures that, with scyjava.version.version AND + Ensure that, with scyjava.version.version AND importlib.metadata unavailable, pkg_resources is used next. """ @@ -49,12 +49,12 @@ def test_version_pkg_resources(): # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - assert _scyjava_version() == _find_version() + assert _expected_version() == _find_version() -def test_version_unvailable(): +def test_version_unavailable(): """ - Ensures that no version is returned if none of these + Ensure that no version is returned if none of these strategies works. """ # Remove importlib.metadata From b2c61a69f2483e6529b1e39e3753ebcbab2a322b Mon Sep 17 00:00:00 2001 From: Curtis Rueden Date: Thu, 22 Sep 2022 13:41:55 -0500 Subject: [PATCH 3/3] Unify Python module version logic into get_version In this way, it can be used easily by a downstream component to extract its version into its own __version__ dunder as well. And change get_version to raise an exception if extraction fails. --- README.md | 14 +++++++++----- src/scyjava/__init__.py | 43 ++++++++++++++++++++++++++++++----------- src/scyjava/_version.py | 16 --------------- tests/test_version.py | 18 ++++++++--------- 4 files changed, 50 insertions(+), 41 deletions(-) delete mode 100644 src/scyjava/_version.py diff --git a/README.md b/README.md index fac7a42e..4d29a330 100644 --- a/README.md +++ b/README.md @@ -164,9 +164,13 @@ FUNCTIONS leading underscore! :param func: The function to turn into a property - get_version(java_class) - Return the version of a Java class. - Requires org.scijava:scijava-common on the classpath. + get_version(java_class_or_python_package) + Return the version of a Java class or Python package. + + For Python packages, uses importlib.metadata.version if available + (Python 3.8+), with pkg_resources.get_distribution as a fallback. + + For Java classes, requires org.scijava:scijava-common on the classpath. The version string is extracted from the given class's associated JAR artifact (if any), either the embedded Maven POM if the project was built @@ -182,12 +186,12 @@ FUNCTIONS jimport a java.awt or javax.swing class. This can lead to deadlocks on macOS if you are not running in headless mode and did not invoke those actions via the jpype.setupGuiEnvironment wrapper function; - see the Troubleshooting section below for details. + see the Troubleshooting section of the scyjava README for details. is_jvm_headless() Return true iff Java is running in headless mode. - :raises RuntimeException: If the JVM has not started yet. + :raises RuntimeError: If the JVM has not started yet. is_version_at_least(actual_version, minimum_version) Return a boolean on a version comparison. diff --git a/src/scyjava/__init__.py b/src/scyjava/__init__.py index 0bca244a..10d21a43 100644 --- a/src/scyjava/__init__.py +++ b/src/scyjava/__init__.py @@ -7,6 +7,7 @@ import sys import typing from functools import lru_cache +from importlib.util import find_spec from pathlib import Path from typing import Any, Callable, Dict, NamedTuple @@ -25,14 +26,9 @@ JLong, JShort, ) -from scyjava._version import _find_version - import scyjava.config -__version__ = _find_version() - - _logger = logging.getLogger(__name__) # Set of module properties @@ -347,10 +343,14 @@ def when_jvm_stops(f): # -- Utility functions -- -def get_version(java_class): +def get_version(java_class_or_python_package): """ - Return the version of a Java class. - Requires org.scijava:scijava-common on the classpath. + Return the version of a Java class or Python package. + + For Python package, uses importlib.metadata.version if available + (Python 3.8+), with pkg_resources.get_distribution as a fallback. + + For Java classes, requires org.scijava:scijava-common on the classpath. The version string is extracted from the given class's associated JAR artifact (if any), either the embedded Maven POM if the project was built @@ -358,9 +358,27 @@ def get_version(java_class): See org.scijava.VersionUtils.getVersion(Class) for further details. """ - VersionUtils = jimport("org.scijava.util.VersionUtils") - version = VersionUtils.getVersion(java_class) - return version + + if isjava(java_class_or_python_package): + # Assume we were given a Java class object. + VersionUtils = jimport("org.scijava.util.VersionUtils") + return str(VersionUtils.getVersion(java_class_or_python_package)) + + # Assume we were given a Python package name. + + if find_spec("importlib.metadata"): + # Fastest, but requires Python 3.8+. + from importlib.metadata import version + + return version(java_class_or_python_package) + + if find_spec("pkg_resources"): + # Slower, but works on Python 3.7. + from pkg_resources import get_distribution + + return get_distribution(java_class_or_python_package).version + + raise RuntimeError("Cannot determine version! Is pkg_resources installed?") def is_version_at_least(actual_version, minimum_version): @@ -1136,6 +1154,9 @@ def _pandas_to_table(df): return table +__version__ = get_version("scyjava") + + # -- JVM startup callbacks -- # NB: These must be performed last, because if this class is imported after the diff --git a/src/scyjava/_version.py b/src/scyjava/_version.py deleted file mode 100644 index ce45f429..00000000 --- a/src/scyjava/_version.py +++ /dev/null @@ -1,16 +0,0 @@ -from importlib.util import find_spec - - -def _find_version(): - # First pass: use importlib.metadata - if find_spec("importlib.metadata"): - from importlib.metadata import version - - return version("scyjava") - - if find_spec("pkg_resources"): - from pkg_resources import get_distribution - - return get_distribution("scyjava").version - # Fourth pass: Give up - return "Cannot determine version! Ensure pkg_resources is installed!" diff --git a/tests/test_version.py b/tests/test_version.py index 2c7a8d1a..7d62db18 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest import scyjava -from scyjava._version import _find_version +from scyjava import get_version def _expected_version(): @@ -32,7 +32,7 @@ def test_version_importlib(): sys.modules["scyjava.version"] = None # Ensure scyjava.__version__ matches importlib.metadata.version() - assert _expected_version() == _find_version() + assert _expected_version() == get_version("scyjava") @pytest.mark.skipif( @@ -49,20 +49,20 @@ def test_version_pkg_resources(): # Ensure scyjava.__version__ matches # pkg_resources.get_distribution().version - assert _expected_version() == _find_version() + assert _expected_version() == get_version("scyjava") def test_version_unavailable(): """ - Ensure that no version is returned if none of these - strategies works. + Ensure that an exception is raised if none of these strategies works. """ # Remove importlib.metadata sys.modules["importlib.metadata"] = None # Remove pkg_resources sys.modules["pkg_resources"] = None - # Ensure scyjava.__version__ is an error message. + # Ensure scyjava.__version__ raises an exception. + with pytest.raises(RuntimeError) as e_info: + get_version("scyjava") assert ( - "Cannot determine version! Ensure pkg_resources is installed!" - == _find_version() - ) + "RuntimeError: Cannot determine version! Is pkg_resources installed?" + ) == e_info.exconly()