Skip to content

Commit e3e6c39

Browse files
authored
Replacement for distutils.version.LooseVersion, fix warning (#351)
Deprecate distutils.version.LooseVersion for our own LegacyVersion Fixes #348, #349 See also: tmux-python/tmuxp#727
2 parents afb218e + 68443d6 commit e3e6c39

File tree

6 files changed

+197
-4
lines changed

6 files changed

+197
-4
lines changed

CHANGES

+21
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ $ pip install --user --upgrade --pre libtmux
1414

1515
<!-- Maintainers and contributors: Insert change notes for the next release above -->
1616

17+
### Breaking changes
18+
19+
- Fix `distutils` warning, vendorize `LegacyVersion` (#351)
20+
21+
Removal of reliancy on `distutils.version.LooseVersion`, which does not
22+
support `tmux(1)` versions like `3.1a`.
23+
24+
Fixes warning:
25+
26+
> DeprecationWarning: distutils Version classes are deprecated. Use
27+
> packaging.version instead.
28+
29+
The temporary workaround, before 0.16.0 (assuming _setup.cfg_):
30+
31+
```ini
32+
[tool:pytest]
33+
filterwarnings =
34+
ignore:.* Use packaging.version.*:DeprecationWarning::
35+
ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::
36+
```
37+
1738
### Features
1839

1940
- `Window.split_window()` and `Session.new_window()` now support an optional

setup.cfg

-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ line_length = 88
1818

1919
[tool:pytest]
2020
filterwarnings =
21-
ignore:.* Use packaging.version.*:DeprecationWarning::
2221
ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::
2322
addopts = --tb=short --no-header --showlocals --doctest-docutils-modules --reruns 2 -p no:doctest
2423
doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE

src/libtmux/_compat.py

+104
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# flake8: NOQA
2+
import functools
23
import sys
34
import types
45
import typing as t
@@ -31,3 +32,106 @@ def str_from_console(s: t.Union[str, bytes]) -> str:
3132
return str(s)
3233
except UnicodeDecodeError:
3334
return str(s, encoding="utf_8") if isinstance(s, bytes) else s
35+
36+
37+
import re
38+
from typing import Iterator, List, Tuple
39+
40+
from packaging.version import Version
41+
42+
###
43+
### Legacy support for LooseVersion / LegacyVersion, e.g. 2.4-openbsd
44+
### https://github.com/pypa/packaging/blob/21.3/packaging/version.py#L106-L115
45+
### License: BSD, Accessed: Jan 14th, 2022
46+
###
47+
48+
LegacyCmpKey = Tuple[int, Tuple[str, ...]]
49+
50+
_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
51+
_legacy_version_replacement_map = {
52+
"pre": "c",
53+
"preview": "c",
54+
"-": "final-",
55+
"rc": "c",
56+
"dev": "@",
57+
}
58+
59+
60+
def _parse_version_parts(s: str) -> Iterator[str]:
61+
for part in _legacy_version_component_re.split(s):
62+
part = _legacy_version_replacement_map.get(part, part)
63+
64+
if not part or part == ".":
65+
continue
66+
67+
if part[:1] in "0123456789":
68+
# pad for numeric comparison
69+
yield part.zfill(8)
70+
else:
71+
yield "*" + part
72+
73+
# ensure that alpha/beta/candidate are before final
74+
yield "*final"
75+
76+
77+
def _legacy_cmpkey(version: str) -> LegacyCmpKey:
78+
# We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
79+
# greater than or equal to 0. This will effectively put the LegacyVersion,
80+
# which uses the defacto standard originally implemented by setuptools,
81+
# as before all PEP 440 versions.
82+
epoch = -1
83+
84+
# This scheme is taken from pkg_resources.parse_version setuptools prior to
85+
# it's adoption of the packaging library.
86+
parts: List[str] = []
87+
for part in _parse_version_parts(version.lower()):
88+
if part.startswith("*"):
89+
# remove "-" before a prerelease tag
90+
if part < "*final":
91+
while parts and parts[-1] == "*final-":
92+
parts.pop()
93+
94+
# remove trailing zeros from each series of numeric parts
95+
while parts and parts[-1] == "00000000":
96+
parts.pop()
97+
98+
parts.append(part)
99+
100+
return epoch, tuple(parts)
101+
102+
103+
@functools.total_ordering
104+
class LegacyVersion:
105+
_key: LegacyCmpKey
106+
107+
def __hash__(self) -> int:
108+
return hash(self._key)
109+
110+
def __init__(self, version: object) -> None:
111+
self._version = str(version)
112+
self._key = _legacy_cmpkey(self._version)
113+
114+
def __str__(self) -> str:
115+
return self._version
116+
117+
def __lt__(self, other: object) -> bool:
118+
if isinstance(other, str):
119+
other = LegacyVersion(other)
120+
if not isinstance(other, LegacyVersion):
121+
return NotImplemented
122+
123+
return self._key < other._key
124+
125+
def __eq__(self, other: object) -> bool:
126+
if isinstance(other, str):
127+
other = LegacyVersion(other)
128+
if not isinstance(other, LegacyVersion):
129+
return NotImplemented
130+
131+
return self._key == other._key
132+
133+
def __repr__(self) -> str:
134+
return "<LegacyVersion({0})>".format(repr(str(self)))
135+
136+
137+
LooseVersion = LegacyVersion

src/libtmux/common.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
import subprocess
1212
import sys
1313
import typing as t
14-
from distutils.version import LooseVersion
1514
from typing import Dict, Generic, KeysView, List, Optional, TypeVar, Union, overload
1615

1716
from . import exc
18-
from ._compat import console_to_str, str_from_console
17+
from ._compat import LooseVersion, console_to_str, str_from_console
1918

2019
if t.TYPE_CHECKING:
2120
from typing_extensions import Literal

tests/test_common.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import re
44
import sys
55
import typing as t
6-
from distutils.version import LooseVersion
76
from typing import Optional
87

98
import pytest
109

1110
import libtmux
11+
from libtmux._compat import LooseVersion
1212
from libtmux.common import (
1313
TMUX_MAX_VERSION,
1414
TMUX_MIN_VERSION,

tests/test_version.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import operator
2+
import typing as t
3+
from contextlib import nullcontext as does_not_raise
4+
5+
import pytest
6+
7+
from libtmux._compat import LooseVersion
8+
9+
if t.TYPE_CHECKING:
10+
from _pytest.python_api import RaisesContext
11+
from typing_extensions import TypeAlias
12+
13+
VersionCompareOp: TypeAlias = t.Callable[
14+
[t.Any, t.Any],
15+
bool,
16+
]
17+
18+
19+
@pytest.mark.parametrize(
20+
"version",
21+
[
22+
"1",
23+
"1.0",
24+
"1.0.0",
25+
"1.0.0b",
26+
"1.0.0b1",
27+
"1.0.0b-openbsd",
28+
"1.0.0-next",
29+
"1.0.0-next.1",
30+
],
31+
)
32+
def test_version(version: str) -> None:
33+
assert LooseVersion(version)
34+
35+
36+
class VersionCompareFixture(t.NamedTuple):
37+
a: object
38+
op: "VersionCompareOp"
39+
b: object
40+
raises: t.Union[t.Type[Exception], bool]
41+
42+
43+
@pytest.mark.parametrize(
44+
VersionCompareFixture._fields,
45+
[
46+
VersionCompareFixture(a="1", op=operator.eq, b="1", raises=False),
47+
VersionCompareFixture(a="1", op=operator.eq, b="1.0", raises=False),
48+
VersionCompareFixture(a="1", op=operator.eq, b="1.0.0", raises=False),
49+
VersionCompareFixture(a="1", op=operator.gt, b="1.0.0a", raises=False),
50+
VersionCompareFixture(a="1", op=operator.gt, b="1.0.0b", raises=False),
51+
VersionCompareFixture(a="1", op=operator.lt, b="1.0.0p1", raises=False),
52+
VersionCompareFixture(a="1", op=operator.lt, b="1.0.0-openbsd", raises=False),
53+
VersionCompareFixture(a="1", op=operator.lt, b="1", raises=AssertionError),
54+
VersionCompareFixture(a="1", op=operator.lt, b="1", raises=AssertionError),
55+
VersionCompareFixture(a="1.0.0c", op=operator.gt, b="1.0.0b", raises=False),
56+
],
57+
)
58+
def test_version_compare(
59+
a: str,
60+
op: "VersionCompareOp",
61+
b: str,
62+
raises: t.Union[t.Type[Exception], bool],
63+
) -> None:
64+
raises_ctx: "RaisesContext[Exception]" = (
65+
pytest.raises(t.cast(t.Type[Exception], raises))
66+
if raises
67+
else t.cast("RaisesContext[Exception]", does_not_raise())
68+
)
69+
with raises_ctx:
70+
assert op(LooseVersion(a), LooseVersion(b))

0 commit comments

Comments
 (0)