diff --git a/.ci/release_check.py b/.ci/release_check.py index 4fdc7656..e6f24994 100644 --- a/.ci/release_check.py +++ b/.ci/release_check.py @@ -1,6 +1,6 @@ """Ensure that current version is not in conflict with published releases.""" -from pkg_resources import parse_version +from iminuit._parse_version import parse_version import subprocess as subp from pathlib import PurePath import urllib.request diff --git a/doc/update_changelog.py b/doc/update_changelog.py index 308e7c9d..471d27ff 100644 --- a/doc/update_changelog.py +++ b/doc/update_changelog.py @@ -1,25 +1,18 @@ from pathlib import Path import re import subprocess as subp -from packaging.version import Version, InvalidVersion import datetime import warnings import sys +from iminuit._parse_version import parse_version cwd = Path(__file__).parent -def parse_version_with_fallback(version_string): - try: - return Version(version_string) - except InvalidVersion: - return Version("0.0.1") - - version = ( subp.check_output([sys.executable, cwd.parent / "version.py"]).strip().decode() ) -new_version = parse_version_with_fallback(version) +new_version = parse_version(version) with warnings.catch_warnings(): @@ -28,7 +21,7 @@ def parse_version_with_fallback(version_string): iter( sorted( ( - parse_version_with_fallback(x) + parse_version(x) for x in subp.check_output(["git", "tag"]) .decode() .strip() @@ -44,7 +37,7 @@ def parse_version_with_fallback(version_string): # find latest entry m = re.search(r"([0-9]+\.[0-9]+\.[0-9]+) \([^\)]+\)\n-*", content, re.MULTILINE) -previous_version = Version(m.group(1)) +previous_version = parse_version(m.group(1)) position = m.span(0)[0] # sanity checks diff --git a/pyproject.toml b/pyproject.toml index 60c3d295..2dd1f738 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ "Operating System :: Unix", "Operating System :: MacOS", ] -dependencies = ["packaging", "numpy >=1.21"] +dependencies = ["numpy >=1.21"] [project.urls] repository = "http://github.com/scikit-hep/iminuit" diff --git a/python_releases.py b/python_releases.py index 08f9d067..47e9ed8f 100644 --- a/python_releases.py +++ b/python_releases.py @@ -4,7 +4,6 @@ import re from html.parser import HTMLParser import gzip -from packaging.version import Version class PythonVersionParser(HTMLParser): @@ -27,9 +26,12 @@ def handle_data(self, data): """Extract Python version from entry.""" if self.found_version: self.found_version = False - match = re.search(r"Python (\d+\.\d+)", data) + match = re.search(r"Python (\d+)\.(\d+)\.(\d+)?", data) if match: - self.versions.add(Version(match.group(1))) + major = int(match.group(1)) + minor = int(match.group(2)) + bugfix = int(match.group(3)) + self.versions.add((major, minor, bugfix)) def versions(): @@ -56,7 +58,8 @@ def latest(): def main(): """Print all discovered release versions.""" - print(" ".join(str(x) for x in sorted(versions()))) + for x in sorted(versions()): + print(x) if __name__ == "__main__": diff --git a/src/iminuit/_deprecated.py b/src/iminuit/_deprecated.py index f59514f2..aecb1e49 100644 --- a/src/iminuit/_deprecated.py +++ b/src/iminuit/_deprecated.py @@ -1,21 +1,23 @@ import warnings -from packaging.version import Version from typing import Callable, Any from importlib.metadata import version +from iminuit._parse_version import parse_version -CURRENT_VERSION = Version(version("iminuit")) + +CURRENT_VERSION = parse_version(version("iminuit")) class deprecated: def __init__(self, reason: str, removal: str = ""): self.reason = reason - self.removal = Version(removal) if removal else None + self.removal = parse_version(removal) if removal else None def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: category: Any = FutureWarning extra = "" if self.removal: - extra = f" and will be removed in version {self.removal}" + vstring = ".".join(str(x) for x in self.removal) + extra = f" and will be removed in version {vstring}" if CURRENT_VERSION >= self.removal: category = DeprecationWarning msg = f"{func.__name__} is deprecated{extra}: {self.reason}" diff --git a/src/iminuit/_parse_version.py b/src/iminuit/_parse_version.py new file mode 100644 index 00000000..45f0def0 --- /dev/null +++ b/src/iminuit/_parse_version.py @@ -0,0 +1,24 @@ +import re +from typing import Tuple, Union + + +def parse_version(s: str) -> Union[Tuple[int, int], Tuple[int, int, int]]: + """ + Parse version string and return tuple of integer parts to allow for comparison. + + This does not implement the full version spec for version parsing, see + https://packaging.python.org/en/latest/specifications/version-specifiers/. It is a + simplified approach, so we do not have to depend on the external packaging module. + + We only support correct ordering for major, mior, and micro segments, ie. version + strings of the form X.Y and X.Y.Z. Versions with pre- and post-release segments are + correctly parsed, but these segments are ignored, as well as development release + segments. + """ + match = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?", s) + if not match: + msg = f"could not parse version string {s}" + raise ValueError(msg) + if match.group(3): + return int(match.group(1)), int(match.group(2)), int(match.group(3)) + return int(match.group(1)), int(match.group(2)) diff --git a/src/iminuit/minuit.py b/src/iminuit/minuit.py index 5689913e..7f0bbc66 100644 --- a/src/iminuit/minuit.py +++ b/src/iminuit/minuit.py @@ -2141,12 +2141,12 @@ def draw_mncontour( from matplotlib import pyplot as plt from matplotlib.path import Path from matplotlib.contour import ContourSet - from packaging.version import Version + from ._parse_version import parse_version ix, xname = self._normalize_key(x) iy, yname = self._normalize_key(y) - mpl_version = Version(mpl_version_string) + mpl_version = parse_version(mpl_version_string) cls = [replace_none(x, 0.68) for x in mutil._iterate(cl)] @@ -2163,7 +2163,7 @@ def draw_mncontour( experimental=experimental, ) n_lineto = len(pts) - 2 - if (mpl_version.major, mpl_version.minor) < (3, 5): + if mpl_version < (3, 5): n_lineto -= 1 # pragma: no cover c_val.append(cl) c_pts.append([pts]) # level can have more than one contour in mpl diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 1fa83a34..9e234227 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -26,6 +26,18 @@ def func(x): func(1) +def test_deprecated_func_3(): + @deprecated("bla", removal="1000.0") + def func(x): + pass + + with pytest.warns( + FutureWarning, + match="func is deprecated and will be removed in version 1000.0: bla", + ): + func(1) + + def test_deprecated_parameter(): @deprecated_parameter(foo="bar") def some_function(x, y, foo): diff --git a/tests/test_parse_version.py b/tests/test_parse_version.py new file mode 100644 index 00000000..bbad51ca --- /dev/null +++ b/tests/test_parse_version.py @@ -0,0 +1,24 @@ +from iminuit._parse_version import parse_version +import pytest + + +@pytest.mark.parametrize( + "s,ref", + [ + ("1.2", (1, 2)), + ("1.2.3", (1, 2, 3)), + ("1.2a1", (1, 2)), + ("1.2.3a1", (1, 2, 3)), + ("1.2.post1", (1, 2)), + ("1.2.3.post1", (1, 2, 3)), + ("1.2a1.dev1", (1, 2)), + ("1.2.3a1.dev1", (1, 2, 3)), + ], +) +def test_parse_version(s, ref): + assert parse_version(s) == ref + + +def test_parse_version_bad(): + with pytest.raises(ValueError): + parse_version("a.b.c")