Skip to content
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

fix and make common python version normalization #385

Merged
merged 4 commits into from
Jun 2, 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
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