diff --git a/pyproject.toml b/pyproject.toml index 5b6101f27..d679064c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] diff --git a/src/poetry/core/packages/dependency.py b/src/poetry/core/packages/dependency.py index d69735355..7033c42aa 100644 --- a/src/poetry/core/packages/dependency.py +++ b/src/poetry/core/packages/dependency.py @@ -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 @@ -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 = [] diff --git a/src/poetry/core/packages/utils/utils.py b/src/poetry/core/packages/utils/utils.py index 5ebae1fab..1ae92b88f 100644 --- a/src/poetry/core/packages/utils/utils.py +++ b/src/poetry/core/packages/utils/utils.py @@ -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 @@ -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") @@ -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( @@ -306,6 +288,11 @@ 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"]: @@ -313,10 +300,12 @@ def get_python_constraint_from_marker( 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: diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index 649610ef8..911a13752 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools import re from typing import TYPE_CHECKING @@ -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 diff --git a/tests/packages/test_dependency.py b/tests/packages/test_dependency.py index 6d3e7544f..63a8900f6 100644 --- a/tests/packages/test_dependency.py +++ b/tests/packages/test_dependency.py @@ -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: diff --git a/tests/packages/utils/test_utils.py b/tests/packages/utils/test_utils.py index 97af9df78..d8bfcbf0c 100644 --- a/tests/packages/utils/test_utils.py +++ b/tests/packages/utils/test_utils.py @@ -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 diff --git a/tests/version/test_markers.py b/tests/version/test_markers.py index 48fda706d..992b6d2fb 100644 --- a/tests/version/test_markers.py +++ b/tests/version/test_markers.py @@ -2,14 +2,23 @@ import os +from typing import TYPE_CHECKING + import pytest +from poetry.core.version.markers import AnyMarker +from poetry.core.version.markers import EmptyMarker from poetry.core.version.markers import MarkerUnion from poetry.core.version.markers import MultiMarker from poetry.core.version.markers import SingleMarker +from poetry.core.version.markers import dnf from poetry.core.version.markers import parse_marker +if TYPE_CHECKING: + from poetry.core.version.markers import BaseMarker + + def test_single_marker() -> None: m = parse_marker('sys_platform == "darwin"') @@ -960,3 +969,193 @@ def test_union_should_drop_markers_if_their_complement_is_present( m = parse_marker(marker) assert parse_marker(expected) == m + + +@pytest.mark.parametrize( + "scheme, marker, expected", + [ + ("empty", EmptyMarker(), EmptyMarker()), + ("any", AnyMarker(), AnyMarker()), + ( + "A_", + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", ">=3.7"), + ), + ( + "AB_", + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + ), + ( + "A+B_", + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.9"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.9"), + ), + ), + ( + "AB+AC_", + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "linux"), + ), + ), + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "A(B+C)_AB+AC", + MultiMarker( + SingleMarker("python_version", ">=3.7"), + MarkerUnion( + SingleMarker("python_version", "<3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "(A+B)(C+D)_AC+AD+BC+BD", + MultiMarker( + MarkerUnion( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "win32"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "linux"), + ), + MultiMarker( + SingleMarker("sys_platform", "win32"), + SingleMarker("python_version", "<3.9"), + ), + ), + ), + ( + "A(B+C)+(D+E)(F+G)_AB+AC+DF+DG+EF+DG", + MarkerUnion( + MultiMarker( + SingleMarker("sys_platform", "win32"), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.9"), + ), + ), + MultiMarker( + MarkerUnion( + SingleMarker("python_version", "<3.8"), + SingleMarker("python_version", ">=3.9"), + ), + MarkerUnion( + SingleMarker("sys_platform", "linux"), + SingleMarker("python_version", ">=3.9"), + ), + ), + ), + MarkerUnion( + MultiMarker( + SingleMarker("sys_platform", "win32"), + SingleMarker("python_version", "<3.7"), + ), + SingleMarker("python_version", ">=3.9"), + MultiMarker( + SingleMarker("python_version", "<3.8"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "(A+B(C+D))(E+F)_AE+AF+BCE+BCF+BDE+BDF", + MultiMarker( + MarkerUnion( + SingleMarker("python_version", ">=3.9"), + MultiMarker( + SingleMarker("implementation_name", "cpython"), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.8"), + ), + ), + ), + MarkerUnion( + SingleMarker("sys_platform", "win32"), + SingleMarker("sys_platform", "linux"), + ), + ), + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.9"), + SingleMarker("sys_platform", "win32"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.9"), + SingleMarker("sys_platform", "linux"), + ), + MultiMarker( + SingleMarker("implementation_name", "cpython"), + SingleMarker("python_version", "<3.7"), + SingleMarker("sys_platform", "win32"), + ), + MultiMarker( + SingleMarker("implementation_name", "cpython"), + SingleMarker("python_version", "<3.7"), + SingleMarker("sys_platform", "linux"), + ), + MultiMarker( + SingleMarker("implementation_name", "cpython"), + SingleMarker("python_version", ">=3.8"), + SingleMarker("sys_platform", "win32"), + ), + MultiMarker( + SingleMarker("implementation_name", "cpython"), + SingleMarker("python_version", ">=3.8"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ], +) +def test_dnf(scheme: str, marker: BaseMarker, expected: BaseMarker) -> None: + assert dnf(marker) == expected