diff --git a/poetry.lock b/poetry.lock index 32ad02b97..91efbc060 100644 --- a/poetry.lock +++ b/poetry.lock @@ -439,7 +439,7 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", name = "packaging" version = "22.0" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -945,7 +945,7 @@ test = [] [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "6a05ee2efdef9d79f3c1f8283f8c6ada1be53819a998d129fb15e69477019cca" +content-hash = "20cb0283c8677c189b02f0dd7c1f7d29a5579f11116c68a6031e27d93ca6102c" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 48f0dcec0..a2b402c6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ Changes = "https://github.com/tmux-python/libtmux/blob/master/CHANGES" [tool.poetry.dependencies] python = "^3.7" +packaging = {version = "*", python="^3.10.0"} [tool.poetry.dev-dependencies] ### Docs ### diff --git a/src/libtmux/_compat.py b/src/libtmux/_compat.py index d7b6bb274..2650306e2 100644 --- a/src/libtmux/_compat.py +++ b/src/libtmux/_compat.py @@ -31,3 +31,108 @@ def str_from_console(s: t.Union[str, bytes]) -> str: return str(s) except UnicodeDecodeError: return str(s, encoding="utf_8") if isinstance(s, bytes) else s + + +try: + import re + from typing import Iterator, List, Tuple + + from packaging.version import Version, _BaseVersion + + ### + ### Legacy support for LooseVersion / LegacyVersion, e.g. 2.4-openbsd + ### https://github.com/pypa/packaging/blob/21.3/packaging/version.py#L106-L115 + ### License: BSD, Accessed: Jan 14th, 2022 + ### + + LegacyCmpKey = Tuple[int, Tuple[str, ...]] + + _legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) + _legacy_version_replacement_map = { + "pre": "c", + "preview": "c", + "-": "final-", + "rc": "c", + "dev": "@", + } + + def _parse_version_parts(s: str) -> Iterator[str]: + for part in _legacy_version_component_re.split(s): + part = _legacy_version_replacement_map.get(part, part) + + if not part or part == ".": + continue + + if part[:1] in "0123456789": + # pad for numeric comparison + yield part.zfill(8) + else: + yield "*" + part + + # ensure that alpha/beta/candidate are before final + yield "*final" + + def _legacy_cmpkey(version: str) -> LegacyCmpKey: + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch + # greater than or equal to 0. This will effectively put the LegacyVersion, + # which uses the defacto standard originally implemented by setuptools, + # as before all PEP 440 versions. + epoch = -1 + + # This scheme is taken from pkg_resources.parse_version setuptools prior to + # it's adoption of the packaging library. + parts: List[str] = [] + for part in _parse_version_parts(version.lower()): + if part.startswith("*"): + # remove "-" before a prerelease tag + if part < "*final": + while parts and parts[-1] == "*final-": + parts.pop() + + # remove trailing zeros from each series of numeric parts + while parts and parts[-1] == "00000000": + parts.pop() + + parts.append(part) + + return epoch, tuple(parts) + + class LegacyVersion(_BaseVersion): + def __init__(self, version: str) -> None: + self._version = str(version) + self._key = _legacy_cmpkey(self._version) + + def __str__(self) -> str: + return self._version + + def __lt__(self, other): + if isinstance(other, str): + other = LegacyVersion(other) + return super().__lt__(other) + + def __eq__(self, other) -> bool: + if isinstance(other, str): + other = LegacyVersion(other) + if not isinstance(other, LegacyVersion): + return NotImplemented + + return self._key == other._key + + def __repr__(self) -> str: + return "".format(repr(str(self))) + + @property + def public(self) -> str: + return self._version + + @property + def base_version(self) -> str: + return self._version + + @property + def epoch(self) -> int: + return -1 + + LooseVersion = LegacyVersion +except ImportError: + from distutils.version import LooseVersion, Version diff --git a/src/libtmux/common.py b/src/libtmux/common.py index 430053963..9f27b7f53 100644 --- a/src/libtmux/common.py +++ b/src/libtmux/common.py @@ -11,7 +11,6 @@ import subprocess import sys import typing as t -from distutils.version import LooseVersion from typing import Dict, Generic, KeysView, List, Optional, TypeVar, Union, overload from . import exc @@ -25,6 +24,9 @@ from libtmux.window import Window +from . import exc +from ._compat import LooseVersion, console_to_str, str_from_console + logger = logging.getLogger(__name__) diff --git a/tests/test_common.py b/tests/test_common.py index d09d21e84..00524fb51 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -3,12 +3,12 @@ import re import sys import typing as t -from distutils.version import LooseVersion from typing import Optional import pytest import libtmux +from libtmux._compat import LooseVersion from libtmux.common import ( TMUX_MAX_VERSION, TMUX_MIN_VERSION, @@ -43,7 +43,7 @@ def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert ( - "%s-master" % TMUX_MAX_VERSION == get_version() + LooseVersion("%s-master" % TMUX_MAX_VERSION) == get_version() ), "Is the latest supported version with -master appended" @@ -63,6 +63,7 @@ def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert TMUX_NEXT_VERSION == get_version() + assert LooseVersion("2.9") == get_version() def test_get_version_openbsd(monkeypatch: pytest.MonkeyPatch) -> None: @@ -78,7 +79,7 @@ def mock_tmux_cmd(*args: t.Any, **kwargs: t.Any) -> Hi: assert has_gte_version(TMUX_MIN_VERSION) assert has_gt_version(TMUX_MAX_VERSION), "Greater than the max-supported version" assert ( - "%s-openbsd" % TMUX_MAX_VERSION == get_version() + LooseVersion("%s-openbsd" % TMUX_MAX_VERSION) == get_version() ), "Is the latest supported version with -openbsd appended"