Skip to content
Open
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
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ source_pkgs = ["packaging"]
[tool.coverage.report]
show_missing = true
fail_under = 100
exclude_also = ["@(abc.)?abstractmethod", "@(abc.)?abstractproperty", "if (typing.)?TYPE_CHECKING:", "@(typing.)?overload", "def __dir__()"]
exclude_also = [
"@(abc.)?abstractmethod",
"@(abc.)?abstractproperty",
"if (typing.)?TYPE_CHECKING:",
"@(typing.)?overload",
"def __dir__()",
'if __name__ == "main"',
]

[tool.pytest.ini_options]
minversion = "6.2"
Expand Down
54 changes: 54 additions & 0 deletions src/packaging/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from __future__ import annotations

import operator
import re
import sys
import typing
Expand All @@ -26,6 +27,8 @@
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType

if typing.TYPE_CHECKING:
import argparse

from typing_extensions import Self, Unpack

if sys.version_info >= (3, 13): # pragma: no cover
Expand Down Expand Up @@ -929,3 +932,54 @@ def _cmpkey(
)

return epoch, _release, _pre, _post, _dev, _local


_COMPARE_OPERATIONS = {
"lt": operator.lt,
"le": operator.le,
"eq": operator.eq,
"ne": operator.ne,
"ge": operator.ge,
"gt": operator.gt,
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
">=": operator.ge,
">": operator.gt,
}


def _main_compare(args: argparse.Namespace) -> int:
result = _COMPARE_OPERATIONS[args.operator](args.version1, args.version2)
return not result


def main() -> None:
import argparse # noqa: PLC0415

parser = argparse.ArgumentParser(description="Version utilities")
subparsers = parser.add_subparsers(dest="command", required=True)

compare = subparsers.add_parser(
"compare",
help="Compare two semantic versions.",
description="Compare two semantic versions. Return code is 0 or 1.",
)
compare.set_defaults(func=_main_compare)
compare.add_argument("version1", type=Version, help="First version to compare")
compare.add_argument(
"operator",
choices=_COMPARE_OPERATIONS.keys(),
help="Comparison operator",
)
compare.add_argument("version2", type=Version, help="Second version to compare")

args = parser.parse_args()

result = args.func(args)
raise SystemExit(result)


if __name__ == "__main__":
main()
60 changes: 60 additions & 0 deletions tests/test_version_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import annotations

import sys

import pytest

from packaging.version import main


@pytest.mark.parametrize(
("args", "retcode"),
[
("1.2 eq 1.2", 0),
("1.2 eq 1.2.0", 0),
("1.2 eq 1.2dev1", 1),
("1.2 == 1.2", 0),
("1.2 == 1.2.0", 0),
("1.2 == 1.2dev1", 1),
("1.2 ne 1.2.0", 1),
("1.2 ne 1.2dev1", 0),
("1.2 != 1.2.0", 1),
("1.2 != 1.2dev1", 0),
("1.2 lt 1.2.0", 1),
("1.2 lt 1.2dev1", 1),
("1.2 lt 1.3", 0),
("1.2 < 1.2.0", 1),
("1.2 < 1.2dev1", 1),
("1.2 < 1.3", 0),
("1.2 gt 1.2.0", 1),
("1.2 gt 1.2dev1", 0),
("1.2 gt 1.1", 0),
("1.2 > 1.2.0", 1),
("1.2 > 1.2dev1", 0),
("1.2 > 1.1", 0),
("1.2 le 1.2", 0),
("1.2 le 1.3", 0),
("1.2 le 1.1", 1),
("1.2 <= 1.2", 0),
("1.2 <= 1.3", 0),
("1.2 <= 1.1", 1),
("1.2 ge 1.2", 0),
("1.2 ge 1.1", 0),
("1.2 ge 1.3", 1),
("1.2 >= 1.2", 0),
("1.2 >= 1.1", 0),
("1.2 >= 1.3", 1),
("1.2 foo 1.2", 2),
("1.2 == unreal", 2),
],
)
def test_compare(monkeypatch: pytest.MonkeyPatch, args: str, retcode: int) -> None:
monkeypatch.setattr(sys, "argv", ["prog", "compare", *args.split()])
with pytest.raises(SystemExit) as excinfo:
main()

assert excinfo.value.code == retcode
Loading