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 get_python_constraint_from_marker #347

Merged
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "poetry-core"
version = "1.1.0-alpha.7"
version = "1.1.0-alpha.8"
description = "Poetry PEP 517 Build Backend"
authors = ["Sébastien Eustace <[email protected]>"]

Expand Down
3 changes: 2 additions & 1 deletion src/poetry/core/packages/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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.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 @@ -180,7 +181,7 @@ def marker(self, marker: str | BaseMarker) -> None:

# Recalculate python versions.
self._python_versions = "*"
if "python_version" in markers:
if not contains_group_without_marker(markers, "python_version"):
ors = []
for or_ in markers["python_version"]:
ands = []
Expand Down
101 changes: 45 additions & 56 deletions src/poetry/core/packages/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from urllib.parse import urlsplit
from urllib.request import url2pathname

from poetry.core.version.markers import dnf


if TYPE_CHECKING:
from poetry.core.packages.constraints import BaseConstraint
Expand All @@ -19,6 +21,8 @@
from poetry.core.semver.version_union import VersionUnion
from poetry.core.version.markers import BaseMarker

ConvertedMarkers = dict[str, list[list[tuple[str, str]]]]


BZ2_EXTENSIONS = (".tar.bz2", ".tbz")
XZ_EXTENSIONS = (".tar.xz", ".txz", ".tlz", ".tar.lz", ".tar.lzma")
Expand Down Expand Up @@ -136,69 +140,47 @@ def splitext(path: str) -> tuple[str, str]:
return base, ext


def group_markers(
markers: list[BaseMarker], or_: bool = False
) -> list[tuple[str, str, str] | list[tuple[str, str, str]]]:
def convert_markers(marker: BaseMarker) -> ConvertedMarkers:
from poetry.core.version.markers import MarkerUnion
from poetry.core.version.markers import MultiMarker
from poetry.core.version.markers import SingleMarker

groups = [[]]

for marker in markers:
if or_:
groups.append([])

if isinstance(marker, (MultiMarker, MarkerUnion)):
groups[-1].append(
group_markers(marker.markers, isinstance(marker, MarkerUnion))
)
elif isinstance(marker, SingleMarker):
lhs, op, rhs = marker.name, marker.operator, marker.value

groups[-1].append((lhs, op, rhs))

return groups


def convert_markers(marker: BaseMarker) -> dict[str, list[list[tuple[str, str]]]]:
groups = group_markers([marker])

requirements = {}
marker = dnf(marker)
conjunctions = marker.markers if isinstance(marker, MarkerUnion) else [marker]
group_count = len(conjunctions)

def _group(
_groups: list[tuple[str, str, str] | list[tuple[str, str, str]]],
or_: bool = False,
def add_constraint(
marker_name: str, constraint: tuple[str, str], group_index: int
) -> None:
ors = {}
for group in _groups:
if isinstance(group, list):
_group(group, or_=True)
else:
variable, op, value = group
group_name = str(variable)
# python_full_version is equivalent to python_version
# for Poetry so we merge them
if marker_name == "python_full_version":
marker_name = "python_version"
if marker_name not in requirements:
requirements[marker_name] = [[] for _ in range(group_count)]
requirements[marker_name][group_index].append(constraint)

for i, sub_marker in enumerate(conjunctions):
if isinstance(sub_marker, MultiMarker):
for m in sub_marker.markers:
if isinstance(m, SingleMarker):
add_constraint(m.name, (m.operator, m.value), i)
elif isinstance(sub_marker, SingleMarker):
add_constraint(sub_marker.name, (sub_marker.operator, sub_marker.value), i)

for group_name in requirements:
# remove duplicates
seen = []
requirements[group_name] = [
r for r in requirements[group_name] if not (r in seen or seen.append(r))
]

# python_full_version is equivalent to python_version
# for Poetry so we merge them
if group_name == "python_full_version":
group_name = "python_version"

if group_name not in requirements:
requirements[group_name] = []

if group_name not in ors:
ors[group_name] = or_

if ors[group_name] or not requirements[group_name]:
requirements[group_name].append([])

requirements[group_name][-1].append((str(op), str(value)))

ors[group_name] = False
return requirements

_group(groups, or_=True)

return requirements
def contains_group_without_marker(markers: ConvertedMarkers, marker_name: str) -> bool:
return marker_name not in markers or [] in markers[marker_name]


def create_nested_marker(
Expand Down Expand Up @@ -306,17 +288,24 @@ def get_python_constraint_from_marker(
return EmptyConstraint()

markers = convert_markers(marker)
if contains_group_without_marker(markers, "python_version"):
# groups are in disjunctive normal form (DNF),
# an empty group means that python_version does not appear in this group,
# which means that python_version is arbitrary for this group
return VersionRange()

ors = []
for or_ in markers["python_version"]:
ands = []
for op, version in or_:
# Expand python version
if op == "==":
version = "~" + version
op = ""
if "*" not in version:
version = "~" + version
op = ""
elif op == "!=":
version += ".*"
if "*" not in version:
version += ".*"
elif op in ("<=", ">"):
parsed_version = Version.parse(version)
if parsed_version.precision == 1:
Expand Down
16 changes: 16 additions & 0 deletions src/poetry/core/version/markers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import itertools
import re

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -841,3 +842,18 @@ def _compact_markers(tree_elements: Tree, tree_prefix: str = "") -> BaseMarker:
return groups[0]

return MarkerUnion.of(*groups)


def dnf(marker: BaseMarker) -> BaseMarker:
"""Transforms the marker into DNF (disjunctive normal form)."""
if isinstance(marker, MultiMarker):
dnf_markers = [dnf(m) for m in marker.markers]
sub_marker_lists = [
m.markers if isinstance(m, MarkerUnion) else [m] for m in dnf_markers
]
return MarkerUnion.of(
*[MultiMarker.of(*c) for c in itertools.product(*sub_marker_lists)]
)
if isinstance(marker, MarkerUnion):
return MarkerUnion.of(*[dnf(m) for m in marker.markers])
return marker
17 changes: 12 additions & 5 deletions tests/packages/test_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,19 @@ def test_with_constraint() -> None:
assert new.transitive_python_constraint == dependency.transitive_python_constraint


def test_marker_properly_sets_python_constraint() -> None:
@pytest.mark.parametrize(
"marker, expected",
[
('python_version >= "3.6" and python_version < "4.0"', ">=3.6,<4.0"),
('sys_platform == "linux"', "*"),
('python_version >= "3.9" or sys_platform == "linux"', "*"),
('python_version >= "3.9" and sys_platform == "linux"', ">=3.9"),
],
)
def test_marker_properly_sets_python_constraint(marker: str, expected: str) -> None:
dependency = Dependency("foo", "^1.2.3")

dependency.marker = 'python_version >= "3.6" and python_version < "4.0"' # type: ignore[assignment]

assert str(dependency.python_constraint) == ">=3.6,<4.0"
dependency.marker = marker # type: ignore[assignment]
assert str(dependency.python_constraint) == expected


def test_dependency_markers_are_the_same_as_markers() -> None:
Expand Down
133 changes: 108 additions & 25 deletions tests/packages/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,127 @@
from poetry.core.version.markers import parse_marker


def test_convert_markers() -> None:
marker = parse_marker(
'sys_platform == "win32" and python_version < "3.6" or sys_platform == "linux"'
' and python_version < "3.6" and python_version >= "3.3" or sys_platform =='
' "darwin" and python_version < "3.3"'
)
converted = convert_markers(marker)
assert converted["python_version"] == [
[("<", "3.6")],
[("<", "3.6"), (">=", "3.3")],
[("<", "3.3")],
]

marker = parse_marker(
'sys_platform == "win32" and python_version < "3.6" or sys_platform == "win32"'
' and python_version < "3.6" and python_version >= "3.3" or sys_platform =='
' "win32" and python_version < "3.3"'
)
converted = convert_markers(marker)
assert converted["python_version"] == [[("<", "3.6")]]

marker = parse_marker('python_version == "2.7" or python_version == "2.6"')
converted = convert_markers(marker)
assert converted["python_version"] == [[("==", "2.7")], [("==", "2.6")]]
@pytest.mark.parametrize(
"marker, expected",
[
(
'sys_platform == "win32" and python_version < "3.6" or sys_platform =='
' "linux" and python_version < "3.6" and python_version >= "3.3" or'
' sys_platform == "darwin" and python_version < "3.3"',
{
"python_version": [
[("<", "3.6")],
[("<", "3.6"), (">=", "3.3")],
[("<", "3.3")],
],
"sys_platform": [
[("==", "win32")],
[("==", "linux")],
[("==", "darwin")],
],
},
),
(
'sys_platform == "win32" and python_version < "3.6" or sys_platform =='
' "win32" and python_version < "3.6" and python_version >= "3.3" or'
' sys_platform == "win32" and python_version < "3.3"',
{"python_version": [[("<", "3.6")]], "sys_platform": [[("==", "win32")]]},
),
(
'python_version == "2.7" or python_version == "2.6"',
{"python_version": [[("==", "2.7")], [("==", "2.6")]]},
),
(
'(python_version < "2.7" or python_full_version >= "3.0.0") and'
' python_full_version < "3.6.0"',
{"python_version": [[("<", "2.7")], [(">=", "3.0.0"), ("<", "3.6.0")]]},
),
(
'(python_version < "2.7" or python_full_version >= "3.0.0") and'
' extra == "foo"',
{
"extra": [[("==", "foo")]],
"python_version": [[("<", "2.7")], [(">=", "3.0.0")]],
},
),
(
'python_version >= "3.9" or sys_platform == "linux"',
{
"python_version": [[(">=", "3.9")], []],
"sys_platform": [[], [("==", "linux")]],
},
),
(
'python_version >= "3.9" and sys_platform == "linux"',
{
"python_version": [[(">=", "3.9")]],
"sys_platform": [[("==", "linux")]],
},
),
],
)
def test_convert_markers(
marker: str, expected: dict[str, list[list[tuple[str, str]]]]
) -> None:
parsed_marker = parse_marker(marker)
converted = convert_markers(parsed_marker)
assert converted == expected


@pytest.mark.parametrize(
["marker", "constraint"],
[
# ==
('python_version == "3.6"', "~3.6"),
('python_version == "3.6.*"', "==3.6.*"),
('python_version == "3.6.* "', "==3.6.*"),
# !=
('python_version != "3.6"', "!=3.6.*"),
('python_version != "3.6.*"', "!=3.6.*"),
('python_version != "3.6.* "', "!=3.6.*"),
# <, <=, >, >= precision 1
('python_version < "3"', "<3"),
('python_version <= "3"', "<4"),
('python_version > "3"', ">=4"),
('python_version >= "3"', ">=3"),
# <, <=, >, >= precision 2
('python_version < "3.6"', "<3.6"),
('python_version <= "3.6"', "<3.7"),
('python_version > "3.6"', ">=3.7"),
('python_version >= "3.6"', ">=3.6"),
# in, not in
('python_version in "2.7, 3.6"', ">=2.7.0,<2.8.0 || >=3.6.0,<3.7.0"),
('python_version in "2.7, 3.6.2"', ">=2.7.0,<2.8.0 || 3.6.2"),
('python_version not in "2.7, 3.6"', "<2.7.0 || >=2.8.0,<3.6.0 || >=3.7.0"),
('python_version not in "2.7, 3.6.2"', "<2.7.0 || >=2.8.0,<3.6.2 || >3.6.2"),
# and
('python_version >= "3.6" and python_full_version < "4.0"', ">=3.6, <4.0"),
(
'python_full_version >= "3.6.1" and python_full_version < "4.0.0"',
">=3.6.1, <4.0.0",
),
# or
('python_version < "3.6" or python_version >= "3.9"', "<3.6 || >=3.9"),
# and or
(
'python_version >= "3.7" and python_version < "3.8" or python_version >='
' "3.9" and python_version < "3.10"',
">=3.7,<3.8 || >=3.9,<3.10",
),
(
'(python_version < "2.7" or python_full_version >= "3.0.0") and'
' python_full_version < "3.6.0"',
"<2.7 || >=3.0,<3.6",
),
# no python_version
('sys_platform == "linux"', "*"),
# no relevant python_version
('python_version >= "3.9" or sys_platform == "linux"', "*"),
# relevant python_version
('python_version >= "3.9" and sys_platform == "linux"', ">=3.9"),
],
)
def test_get_python_constraint_from_marker(marker: str, constraint: str) -> None:
marker_parsed = parse_marker(marker)
constraint_parsed = parse_constraint(constraint)
assert constraint_parsed == get_python_constraint_from_marker(marker_parsed)
assert get_python_constraint_from_marker(marker_parsed) == constraint_parsed
Loading