Skip to content

Replacement for distutils.version.LooseVersion, fix warning #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,27 @@ $ pip install --user --upgrade --pre libtmux

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

### Breaking changes

- Fix `distutils` warning, vendorize `LegacyVersion` (#351)

Removal of reliancy on `distutils.version.LooseVersion`, which does not
support `tmux(1)` versions like `3.1a`.

Fixes warning:

> DeprecationWarning: distutils Version classes are deprecated. Use
> packaging.version instead.

The temporary workaround, before 0.16.0 (assuming _setup.cfg_):

```ini
[tool:pytest]
filterwarnings =
ignore:.* Use packaging.version.*:DeprecationWarning::
ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::
```

### Features

- `Window.split_window()` and `Session.new_window()` now support an optional
Expand Down
1 change: 0 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ line_length = 88

[tool:pytest]
filterwarnings =
ignore:.* Use packaging.version.*:DeprecationWarning::
ignore:The frontend.Option(Parser)? class.*:DeprecationWarning::
addopts = --tb=short --no-header --showlocals --doctest-docutils-modules --reruns 2 -p no:doctest
doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE
Expand Down
104 changes: 104 additions & 0 deletions src/libtmux/_compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# flake8: NOQA
import functools
import sys
import types
import typing as t
Expand Down Expand Up @@ -31,3 +32,106 @@ 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


import re
from typing import Iterator, List, Tuple

from packaging.version import Version

###
### 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)


@functools.total_ordering
class LegacyVersion:
_key: LegacyCmpKey

def __hash__(self) -> int:
return hash(self._key)

def __init__(self, version: object) -> None:
self._version = str(version)
self._key = _legacy_cmpkey(self._version)

def __str__(self) -> str:
return self._version

def __lt__(self, other: object) -> bool:
if isinstance(other, str):
other = LegacyVersion(other)
if not isinstance(other, LegacyVersion):
return NotImplemented

return self._key < other._key

def __eq__(self, other: object) -> 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 "<LegacyVersion({0})>".format(repr(str(self)))


LooseVersion = LegacyVersion
3 changes: 1 addition & 2 deletions src/libtmux/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
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
from ._compat import console_to_str, str_from_console
from ._compat import LooseVersion, console_to_str, str_from_console

if t.TYPE_CHECKING:
from typing_extensions import Literal
Expand Down
2 changes: 1 addition & 1 deletion tests/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
70 changes: 70 additions & 0 deletions tests/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import operator
import typing as t
from contextlib import nullcontext as does_not_raise

import pytest

from libtmux._compat import LooseVersion

if t.TYPE_CHECKING:
from _pytest.python_api import RaisesContext
from typing_extensions import TypeAlias

VersionCompareOp: TypeAlias = t.Callable[
[t.Any, t.Any],
bool,
]


@pytest.mark.parametrize(
"version",
[
"1",
"1.0",
"1.0.0",
"1.0.0b",
"1.0.0b1",
"1.0.0b-openbsd",
"1.0.0-next",
"1.0.0-next.1",
],
)
def test_version(version: str) -> None:
assert LooseVersion(version)


class VersionCompareFixture(t.NamedTuple):
a: object
op: "VersionCompareOp"
b: object
raises: t.Union[t.Type[Exception], bool]


@pytest.mark.parametrize(
VersionCompareFixture._fields,
[
VersionCompareFixture(a="1", op=operator.eq, b="1", raises=False),
VersionCompareFixture(a="1", op=operator.eq, b="1.0", raises=False),
VersionCompareFixture(a="1", op=operator.eq, b="1.0.0", raises=False),
VersionCompareFixture(a="1", op=operator.gt, b="1.0.0a", raises=False),
VersionCompareFixture(a="1", op=operator.gt, b="1.0.0b", raises=False),
VersionCompareFixture(a="1", op=operator.lt, b="1.0.0p1", raises=False),
VersionCompareFixture(a="1", op=operator.lt, b="1.0.0-openbsd", raises=False),
VersionCompareFixture(a="1", op=operator.lt, b="1", raises=AssertionError),
VersionCompareFixture(a="1", op=operator.lt, b="1", raises=AssertionError),
VersionCompareFixture(a="1.0.0c", op=operator.gt, b="1.0.0b", raises=False),
],
)
def test_version_compare(
a: str,
op: "VersionCompareOp",
b: str,
raises: t.Union[t.Type[Exception], bool],
) -> None:
raises_ctx: "RaisesContext[Exception]" = (
pytest.raises(t.cast(t.Type[Exception], raises))
if raises
else t.cast("RaisesContext[Exception]", does_not_raise())
)
with raises_ctx:
assert op(LooseVersion(a), LooseVersion(b))