Skip to content

Commit

Permalink
fix and make common python version normalization (#385)
Browse files Browse the repository at this point in the history
Also handle a single digit of precision more carefully at the boundary.
  • Loading branch information
dimbleby committed Jun 2, 2022
1 parent b026aab commit dd96ae7
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 62 deletions.
38 changes: 5 additions & 33 deletions src/poetry/core/packages/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from poetry.core.packages.dependency_group import MAIN_GROUP
from poetry.core.packages.specification import PackageSpecification
from poetry.core.packages.utils.utils import contains_group_without_marker
from poetry.core.packages.utils.utils import normalize_python_version_markers
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version_range_constraint import VersionRangeConstraint
from poetry.core.version.markers import parse_marker
Expand Down Expand Up @@ -192,39 +193,10 @@ def marker(self, marker: str | BaseMarker) -> None:
# Recalculate python versions.
self._python_versions = "*"
if not contains_group_without_marker(markers, "python_version"):
ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==" and "*" not in version:
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
split = v.split(".")
if len(split) in [1, 2]:
split.append("*")
op_ = "" if op == "in" else "!="
else:
op_ = "==" if op == "in" else "!="

versions.append(op_ + ".".join(split))

glue = " || " if op == "in" else ", "
if versions:
ands.append(glue.join(versions))

continue

ands.append(f"{op}{version}")

ors.append(" ".join(ands))

self._python_versions = " || ".join(ors)
python_version_markers = markers["python_version"]
self._python_versions = normalize_python_version_markers(
python_version_markers
)

self._python_constraint = parse_constraint(self._python_versions)

Expand Down
70 changes: 46 additions & 24 deletions src/poetry/core/packages/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
from urllib.request import url2pathname

from poetry.core.pyproject.toml import PyProjectTOML
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_range import VersionRange
from poetry.core.version.markers import dnf


if TYPE_CHECKING:
from poetry.core.packages.constraints import BaseConstraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_constraint import VersionConstraint
from poetry.core.semver.version_union import VersionUnion
from poetry.core.version.markers import BaseMarker
Expand Down Expand Up @@ -206,7 +207,6 @@ def create_nested_marker(
from poetry.core.packages.constraints.constraint import Constraint
from poetry.core.packages.constraints.multi_constraint import MultiConstraint
from poetry.core.packages.constraints.union_constraint import UnionConstraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_union import VersionUnion

if constraint.is_any():
Expand Down Expand Up @@ -286,8 +286,6 @@ def get_python_constraint_from_marker(
marker: BaseMarker,
) -> VersionConstraint:
from poetry.core.semver.empty_constraint import EmptyConstraint
from poetry.core.semver.helpers import parse_constraint
from poetry.core.semver.version import Version
from poetry.core.semver.version_range import VersionRange

python_marker = marker.only("python_version", "python_full_version")
Expand All @@ -304,34 +302,58 @@ def get_python_constraint_from_marker(
# which means that python_version is arbitrary for this group
return VersionRange()

python_version_markers = markers["python_version"]
normalized = normalize_python_version_markers(python_version_markers)
constraint = parse_constraint(normalized)
return constraint


def normalize_python_version_markers( # NOSONAR
disjunction: list[list[tuple[str, str]]],
) -> str:
ors = []
for or_ in markers["python_version"]:
for or_ in disjunction:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
if "*" not in version:
version = "~" + version
op = ""
elif op == "!=":
if "*" not in version:
version += ".*"
if op == "==" and "*" not in version:
version = "~" + version
op = ""

elif op == "!=" and "*" not in version:
version += ".*"

elif op in ("<=", ">"):
# Make adjustments on encountering versions with less than full
# precision.
#
# Per PEP-508:
# python_version <-> '.'.join(platform.python_version_tuple()[:2])
#
# So for two digits of precision we make the following adjustments:
# - `python_version > "x.y"` requires version >= x.(y+1).anything
# - `python_version <= "x.y"` requires version < x.(y+1).anything
#
# Treatment when we see a single digit of precision is less clear: is
# that even a legitimate marker?
#
# Experiment suggests that pip behaviour is essentially to make a
# lexicographical comparison, for example `python_version > "3"` is
# satisfied by version 3.anything, whereas `python_version <= "3"` is
# satisfied only by version 2.anything.
#
# We achieve the above by fiddling with the operator and version in the
# marker.
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
if parsed_version.precision < 3:
if op == "<=":
op = "<"
version = parsed_version.next_major().text
elif op == ">":
op = ">="
version = parsed_version.next_major().text
elif parsed_version.precision == 2:
if op == "<=":
op = "<"
version = parsed_version.next_minor().text
elif op == ">":
op = ">="
version = parsed_version.next_minor().text

if parsed_version.precision == 2:
version = parsed_version.next_minor().text

elif op in ("in", "not in"):
versions = []
for v in re.split("[ ,]+", version):
Expand All @@ -344,8 +366,8 @@ def get_python_constraint_from_marker(

versions.append(op_ + ".".join(split))

glue = " || " if op == "in" else ", "
if versions:
glue = " || " if op == "in" else ", "
ands.append(glue.join(versions))

continue
Expand All @@ -354,4 +376,4 @@ def get_python_constraint_from_marker(

ors.append(" ".join(ands))

return parse_constraint(" || ".join(ors))
return " || ".join(ors)
6 changes: 3 additions & 3 deletions tests/packages/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,10 @@ def test_dependency_from_pep_508_should_not_produce_empty_constraints_for_correc

assert dep.name == "pytest-mypy"
assert str(dep.constraint) == "*"
assert dep.python_versions == "<=3.10 >3"
assert dep.python_versions == "<3.11 >=3"
assert dep.python_constraint.allows(Version.parse("3.6"))
assert dep.python_constraint.allows(Version.parse("3.10"))
assert not dep.python_constraint.allows(Version.parse("3"))
assert dep.python_constraint.allows(Version.parse("3.10.4"))
assert dep.python_constraint.allows(Version.parse("3"))
assert dep.python_constraint.allows(Version.parse("3.0.1"))
assert (
str(dep.marker)
Expand Down
4 changes: 2 additions & 2 deletions tests/packages/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def test_convert_markers(
('python_version != "3.6.* "', "!=3.6.*"),
# <, <=, >, >= precision 1
('python_version < "3"', "<3"),
('python_version <= "3"', "<4"),
('python_version > "3"', ">=4"),
('python_version <= "3"', "<3"),
('python_version > "3"', ">=3"),
('python_version >= "3"', ">=3"),
# <, <=, >, >= precision 2
('python_version < "3.6"', "<3.6"),
Expand Down

0 comments on commit dd96ae7

Please sign in to comment.