diff --git a/src/poetry/core/packages/utils/utils.py b/src/poetry/core/packages/utils/utils.py index 4b4853ca7..f81fb6b93 100644 --- a/src/poetry/core/packages/utils/utils.py +++ b/src/poetry/core/packages/utils/utils.py @@ -179,8 +179,8 @@ def add_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) + assert 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) diff --git a/src/poetry/core/version/markers.py b/src/poetry/core/version/markers.py index 70bd52c56..d50d2c058 100644 --- a/src/poetry/core/version/markers.py +++ b/src/poetry/core/version/markers.py @@ -258,19 +258,21 @@ def value(self) -> str: def intersect(self, other: BaseMarker) -> BaseMarker: if isinstance(other, SingleMarker): - return MultiMarker.of(self, other) + merged = _merge_single_markers(self, other, MultiMarker) + if merged is not None: + return merged + + return MultiMarker(self, other) return other.intersect(self) def union(self, other: BaseMarker) -> BaseMarker: if isinstance(other, SingleMarker): - if self == other: - return self - - if self == other.invert(): - return AnyMarker() + merged = _merge_single_markers(self, other, MarkerUnion) + if merged is not None: + return merged - return MarkerUnion.of(self, other) + return MarkerUnion(self, other) return other.union(self) @@ -337,7 +339,7 @@ def invert(self) -> BaseMarker: max_ = self._constraint.max max_operator = "<=" if self._constraint.include_max else "<" - return MultiMarker.of( + return MultiMarker( SingleMarker(self._name, f"{min_operator} {min_}"), SingleMarker(self._name, f"{max_operator} {max_}"), ).invert() @@ -368,11 +370,14 @@ def _flatten_markers( for marker in markers: if isinstance(marker, flatten_class): - flattened += _flatten_markers( + for _marker in _flatten_markers( marker.markers, # type: ignore[attr-defined] flatten_class, - ) - else: + ): + if _marker not in flattened: + flattened.append(_marker) + + elif marker not in flattened: flattened.append(marker) return flattened @@ -380,12 +385,11 @@ def _flatten_markers( class MultiMarker(BaseMarker): def __init__(self, *markers: BaseMarker) -> None: - self._markers = [] - - flattened_markers = _flatten_markers(markers, MultiMarker) + self._markers = _flatten_markers(markers, MultiMarker) - for m in flattened_markers: - self._markers.append(m) + @property + def markers(self) -> list[BaseMarker]: + return self._markers @classmethod def of(cls, *markers: BaseMarker) -> BaseMarker: @@ -402,36 +406,22 @@ def of(cls, *markers: BaseMarker) -> BaseMarker: if marker.is_any(): continue - if isinstance(marker, SingleMarker): - intersected = False - for i, mark in enumerate(new_markers): - if isinstance(mark, SingleMarker) and ( - mark.name == marker.name - or {mark.name, marker.name} == PYTHON_VERSION_MARKERS - ): - new_marker = _merge_single_markers(mark, marker, cls) - if new_marker is not None: - new_markers[i] = new_marker - intersected = True - - elif isinstance(mark, MarkerUnion): - intersection = mark.intersect(marker) - if isinstance(intersection, SingleMarker): - new_markers[i] = intersection - elif intersection.is_empty(): - return EmptyMarker() - if intersected: - continue - - elif isinstance(marker, MarkerUnion): - for mark in new_markers: - if isinstance(mark, SingleMarker): - intersection = marker.intersect(mark) - if isinstance(intersection, SingleMarker): - marker = intersection - break - elif intersection.is_empty(): - return EmptyMarker() + intersected = False + for i, mark in enumerate(new_markers): + # If we have a SingleMarker then with any luck after intersection + # it'll become another SingleMarker. + if isinstance(mark, SingleMarker): + new_marker = marker.intersect(mark) + if new_marker.is_empty(): + return EmptyMarker() + + if isinstance(new_marker, SingleMarker): + new_markers[i] = new_marker + intersected = True + break + + if intersected: + continue new_markers.append(marker) @@ -446,104 +436,35 @@ def of(cls, *markers: BaseMarker) -> BaseMarker: return MultiMarker(*new_markers) - @property - def markers(self) -> list[BaseMarker]: - return self._markers - def intersect(self, other: BaseMarker) -> BaseMarker: - if other.is_any(): - return self - - if other.is_empty(): - return other - - if isinstance(other, MarkerUnion): - return other.intersect(self) - - new_markers = self._markers + [other] - - multi = MultiMarker.of(*new_markers) - - if isinstance(multi, MultiMarker): - return dnf(multi) - - return multi + return intersection(self, other) def union(self, other: BaseMarker) -> BaseMarker: - if isinstance(other, (SingleMarker, MultiMarker)): - return MarkerUnion.of(self, other) - - return other.union(self) + return union(self, other) def union_simplify(self, other: BaseMarker) -> BaseMarker | None: """ - In contrast to the standard union method, which prefers to return - a MarkerUnion of MultiMarkers, this version prefers to return - a MultiMarker of MarkerUnions. + Finds a couple of easy simplifications for union on MultiMarkers: - The rationale behind this approach is to find additional simplifications. - In order to avoid endless recursions, this method returns None - if it cannot find a simplification. - """ - if isinstance(other, SingleMarker): - new_markers = [] - for marker in self._markers: - union = marker.union(other) - if not union.is_any(): - new_markers.append(union) - - if len(new_markers) == 1: - return new_markers[0] - if other in new_markers and all( - other == m or isinstance(m, MarkerUnion) and other in m.markers - for m in new_markers - ): - return other + - union with any marker that appears as part of the multi is just that + marker - if not any(isinstance(m, MarkerUnion) for m in new_markers): - return self.of(*new_markers) + - union between two multimarkers where one is contained by the other is just + the larger of the two + """ + if other in self._markers: + return other - elif isinstance(other, MultiMarker): - common_markers = [ - marker for marker in self.markers if marker in other.markers - ] + if isinstance(other, MultiMarker): + our_markers = set(self.markers) + their_markers = set(other.markers) - unique_markers = [ - marker for marker in self.markers if marker not in common_markers - ] - if not unique_markers: + if our_markers.issubset(their_markers): return self - other_unique_markers = [ - marker for marker in other.markers if marker not in common_markers - ] - if not other_unique_markers: + if their_markers.issubset(our_markers): return other - if common_markers: - unique_union = self.of(*unique_markers).union( - self.of(*other_unique_markers) - ) - if not isinstance(unique_union, MarkerUnion): - return self.of(*common_markers).intersect(unique_union) - - else: - # Usually this operation just complicates things, but the special case - # where it doesn't allows the collapse of adjacent ranges eg - # - # 'python_version >= "3.6" and python_version < "3.6.2"' union - # 'python_version >= "3.6.2" and python_version < "3.7"' -> - # - # 'python_version >= "3.6" and python_version < "3.7"'. - unions = [ - m1.union(m2) for m2 in other_unique_markers for m1 in unique_markers - ] - conjunction = self.of(*unions) - if not isinstance(conjunction, MultiMarker) or not any( - isinstance(m, MarkerUnion) for m in conjunction.markers - ): - return conjunction - return None def validate(self, environment: dict[str, Any] | None) -> bool: @@ -588,7 +509,7 @@ def only(self, *marker_names: str) -> BaseMarker: def invert(self) -> BaseMarker: markers = [marker.invert() for marker in self._markers] - return MarkerUnion.of(*markers) + return MarkerUnion(*markers) def __eq__(self, other: object) -> bool: if not isinstance(other, MultiMarker): @@ -616,7 +537,7 @@ def __str__(self) -> str: class MarkerUnion(BaseMarker): def __init__(self, *markers: BaseMarker) -> None: - self._markers = list(markers) + self._markers = _flatten_markers(markers, MarkerUnion) @property def markers(self) -> list[BaseMarker]: @@ -631,34 +552,30 @@ def of(cls, *markers: BaseMarker) -> BaseMarker: old_markers = new_markers new_markers = [] for marker in old_markers: - if marker in new_markers or marker.is_empty(): + if marker in new_markers: + continue + + if marker.is_empty(): continue included = False + for i, mark in enumerate(new_markers): + # If we have a SingleMarker then with any luck after union it'll + # become another SingleMarker. + if isinstance(mark, SingleMarker): + new_marker = marker.union(mark) + if new_marker.is_any(): + return AnyMarker() + + if isinstance(new_marker, SingleMarker): + new_markers[i] = new_marker + included = True + break - if isinstance(marker, SingleMarker): - for i, mark in enumerate(new_markers): - if isinstance(mark, SingleMarker) and ( - mark.name == marker.name - or {mark.name, marker.name} == PYTHON_VERSION_MARKERS - ): - new_marker = _merge_single_markers(mark, marker, cls) - if new_marker is not None: - new_markers[i] = new_marker - included = True - break - - elif isinstance(mark, MultiMarker): - union = mark.union_simplify(marker) - if union is not None: - new_markers[i] = union - included = True - break - - elif isinstance(marker, MultiMarker): - included = False - for i, mark in enumerate(new_markers): - union = marker.union_simplify(mark) + # If we have a MultiMarker then we can look for the simplifications + # implemented in union_simplify(). + elif isinstance(mark, MultiMarker): + union = mark.union_simplify(marker) if union is not None: new_markers[i] = union included = True @@ -667,8 +584,9 @@ def of(cls, *markers: BaseMarker) -> BaseMarker: if included: # flatten again because union_simplify may return a union new_markers = _flatten_markers(new_markers, MarkerUnion) - else: - new_markers.append(marker) + continue + + new_markers.append(marker) if any(m.is_any() for m in new_markers): return AnyMarker() @@ -688,39 +606,10 @@ def append(self, marker: BaseMarker) -> None: self._markers.append(marker) def intersect(self, other: BaseMarker) -> BaseMarker: - if other.is_any(): - return self - - if other.is_empty(): - return other - - new_markers = [] - if isinstance(other, (SingleMarker, MultiMarker)): - for marker in self._markers: - intersection = marker.intersect(other) - - if not intersection.is_empty(): - new_markers.append(intersection) - elif isinstance(other, MarkerUnion): - for our_marker in self._markers: - for their_marker in other.markers: - intersection = our_marker.intersect(their_marker) - - if not intersection.is_empty(): - new_markers.append(intersection) - - return MarkerUnion.of(*new_markers) + return intersection(self, other) def union(self, other: BaseMarker) -> BaseMarker: - if other.is_any(): - return other - - if other.is_empty(): - return self - - new_markers = self._markers + [other] - - return MarkerUnion.of(*new_markers) + return union(self, other) def validate(self, environment: dict[str, Any] | None) -> bool: return any(m.validate(environment) for m in self._markers) @@ -762,8 +651,7 @@ def only(self, *marker_names: str) -> BaseMarker: def invert(self) -> BaseMarker: markers = [marker.invert() for marker in self._markers] - - return MultiMarker.of(*markers) + return MultiMarker(*markers) def __eq__(self, other: object) -> bool: if not isinstance(other, MarkerUnion): @@ -804,21 +692,28 @@ def parse_marker(marker: str) -> BaseMarker: return markers -def _compact_markers(tree_elements: Tree, tree_prefix: str = "") -> BaseMarker: +def _compact_markers( + tree_elements: Tree, tree_prefix: str = "", top_level: bool = True +) -> BaseMarker: from lark import Token - groups: list[BaseMarker] = [MultiMarker()] + # groups is a disjunction of conjunctions + # eg [[A, B], [C, D]] represents "(A and B) or (C and D)" + groups: list[list[BaseMarker]] = [[]] + for token in tree_elements: if isinstance(token, Token): if token.type == f"{tree_prefix}BOOL_OP" and token.value == "or": - groups.append(MultiMarker()) + groups.append([]) continue if token.data == "marker": - groups[-1] = MultiMarker.of( - groups[-1], _compact_markers(token.children, tree_prefix=tree_prefix) + sub_marker = _compact_markers( + token.children, tree_prefix=tree_prefix, top_level=False ) + groups[-1].append(sub_marker) + elif token.data == f"{tree_prefix}item": name, op, value = token.children if value.type == f"{tree_prefix}MARKER_NAME": @@ -828,27 +723,38 @@ def _compact_markers(tree_elements: Tree, tree_prefix: str = "") -> BaseMarker: ) value = value[1:-1] - groups[-1] = MultiMarker.of( - groups[-1], SingleMarker(str(name), f"{op}{value}") - ) + sub_marker = SingleMarker(str(name), f"{op}{value}") + groups[-1].append(sub_marker) + elif token.data == f"{tree_prefix}BOOL_OP" and token.children[0] == "or": - groups.append(MultiMarker()) + groups.append([]) - for i, group in enumerate(reversed(groups)): - if group.is_empty(): - del groups[len(groups) - 1 - i] - continue + # Combine the groups. + sub_markers = [MultiMarker(*group) for group in groups] - if isinstance(group, MultiMarker) and len(group.markers) == 1: - groups[len(groups) - 1 - i] = group.markers[0] + # This function calls itself recursively. In the inner calls we don't perform any + # simplification, instead doing it all only when we have the complete marker. + if not top_level: + return MarkerUnion(*sub_markers) - if not groups: - return EmptyMarker() + return union(*sub_markers) - if len(groups) == 1: - return groups[0] - return MarkerUnion.of(*groups) +def cnf(marker: BaseMarker) -> BaseMarker: + """Transforms the marker into CNF (conjunctive normal form).""" + if isinstance(marker, MarkerUnion): + cnf_markers = [cnf(m) for m in marker.markers] + sub_marker_lists = [ + m.markers if isinstance(m, MultiMarker) else [m] for m in cnf_markers + ] + return MultiMarker.of( + *[MarkerUnion.of(*c) for c in itertools.product(*sub_marker_lists)] + ) + + if isinstance(marker, MultiMarker): + return MultiMarker.of(*[cnf(m) for m in marker.markers]) + + return marker def dnf(marker: BaseMarker) -> BaseMarker: @@ -866,6 +772,18 @@ def dnf(marker: BaseMarker) -> BaseMarker: return marker +def intersection(*markers: BaseMarker) -> BaseMarker: + return dnf(MultiMarker(*markers)) + + +def union(*markers: BaseMarker) -> BaseMarker: + conjunction = cnf(MarkerUnion(*markers)) + if not isinstance(conjunction, MultiMarker): + return conjunction + + return dnf(conjunction) + + def _merge_single_markers( marker1: SingleMarker, marker2: SingleMarker, @@ -874,6 +792,9 @@ def _merge_single_markers( if {marker1.name, marker2.name} == PYTHON_VERSION_MARKERS: return _merge_python_version_single_markers(marker1, marker2, merge_class) + if marker1.name != marker2.name: + return None + if merge_class == MultiMarker: merge_method = marker1.constraint.intersect else: diff --git a/tests/packages/test_main.py b/tests/packages/test_main.py index 2dd465b7a..5a880e12a 100644 --- a/tests/packages/test_main.py +++ b/tests/packages/test_main.py @@ -102,9 +102,9 @@ def test_dependency_from_pep_508_complex() -> None: assert dep.python_versions == ">=2.7 !=3.2.*" assert ( str(dep.marker) - == 'python_version >= "2.7" and python_version != "3.2" ' - 'and (sys_platform == "win32" or sys_platform == "darwin") ' - 'and extra == "foo"' + == 'python_version >= "2.7" and python_version != "3.2" and sys_platform ==' + ' "win32" and extra == "foo" or python_version >= "2.7" and python_version' + ' != "3.2" and sys_platform == "darwin" and extra == "foo"' ) @@ -278,11 +278,11 @@ def test_dependency_from_pep_508_with_python_full_version() -> None: assert dep.name == "requests" assert str(dep.constraint) == "2.18.0" assert dep.extras == frozenset() - assert dep.python_versions == ">=2.7 <2.8 || >=3.4 <3.5.4" + assert dep.python_versions == ">=2.7 <2.8 || >=3.4.0 <3.5.4" assert ( str(dep.marker) == 'python_version >= "2.7" and python_version < "2.8" ' - 'or python_full_version >= "3.4" and python_full_version < "3.5.4"' + 'or python_full_version >= "3.4.0" and python_full_version < "3.5.4"' ) diff --git a/tests/packages/utils/test_utils.py b/tests/packages/utils/test_utils.py index a9949752e..1006031de 100644 --- a/tests/packages/utils/test_utils.py +++ b/tests/packages/utils/test_utils.py @@ -25,8 +25,8 @@ { "python_version": [ [("<", "3.6")], - [("<", "3.6"), (">=", "3.3")], [("<", "3.3")], + [("<", "3.6"), (">=", "3.3")], ], "sys_platform": [ [("==", "win32")], diff --git a/tests/version/test_markers.py b/tests/version/test_markers.py index 5e0b216b4..64fd94446 100644 --- a/tests/version/test_markers.py +++ b/tests/version/test_markers.py @@ -11,6 +11,7 @@ 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 cnf from poetry.core.version.markers import dnf from poetry.core.version.markers import parse_marker @@ -519,16 +520,16 @@ def test_multi_marker_union_multi_is_multi( 'python_full_version >= "3.6.2" and python_version < "3.7"', 'python_version >= "3.6" and python_version < "3.7"', ), - # Ranges with same end. Ideally the union would give the lower version first. + # Ranges with same end. ( 'python_version >= "3.6" and python_version < "3.7"', 'python_full_version >= "3.6.2" and python_version < "3.7"', - 'python_version < "3.7" and python_version >= "3.6"', + 'python_version >= "3.6" and python_version < "3.7"', ), ( 'python_version >= "3.6" and python_version <= "3.7"', 'python_full_version >= "3.6.2" and python_version <= "3.7"', - 'python_version <= "3.7" and python_version >= "3.6"', + 'python_version >= "3.6" and python_version <= "3.7"', ), # A range covers an exact marker. ( @@ -573,14 +574,21 @@ def test_version_ranges_collapse_on_union( def test_multi_marker_union_with_union() -> None: - m = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') + m1 = parse_marker('sys_platform == "darwin" and implementation_name == "cpython"') + m2 = parse_marker('python_version >= "3.6" or os_name == "Windows"') - union = m.union(parse_marker('python_version >= "3.6" or os_name == "Windows"')) - assert ( - str(union) - == 'python_version >= "3.6" or os_name == "Windows"' - ' or sys_platform == "darwin" and implementation_name == "cpython"' + # Union isn't _quite_ symmetrical. + expected1 = ( + 'sys_platform == "darwin" and implementation_name == "cpython" or' + ' python_version >= "3.6" or os_name == "Windows"' + ) + assert str(m1.union(m2)) == expected1 + + expected2 = ( + 'python_version >= "3.6" or os_name == "Windows" or' + ' sys_platform == "darwin" and implementation_name == "cpython"' ) + assert str(m2.union(m1)) == expected2 def test_multi_marker_union_with_multi_union_is_single_marker() -> None: @@ -684,17 +692,24 @@ def test_marker_union_intersect_multi_marker() -> None: m1 = parse_marker('sys_platform == "darwin" or python_version < "3.4"') m2 = parse_marker('implementation_name == "cpython" and os_name == "Windows"') - expected = ( + # Intersection isn't _quite_ symmetrical. + expected1 = ( + 'sys_platform == "darwin" and implementation_name == "cpython" and os_name ==' + ' "Windows" or python_version < "3.4" and implementation_name == "cpython" and' + ' os_name == "Windows"' + ) + + intersection = m1.intersect(m2) + assert str(intersection) == expected1 + + expected2 = ( 'implementation_name == "cpython" and os_name == "Windows" and sys_platform' ' == "darwin" or implementation_name == "cpython" and os_name == "Windows"' ' and python_version < "3.4"' ) - intersection = m1.intersect(m2) - assert str(intersection) == expected - intersection = m2.intersect(m1) - assert str(intersection) == expected + assert str(intersection) == expected2 def test_marker_union_union_with_union() -> None: @@ -1117,6 +1132,178 @@ def test_union_should_drop_markers_if_their_complement_is_present( 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"), + ), + ), + ( + "(A+B)(C+D)_", + MultiMarker( + MarkerUnion( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "win32"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + MultiMarker( + MarkerUnion( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "win32"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "AB+AC_A(B+C)", + MarkerUnion( + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("python_version", "<3.9"), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + SingleMarker("sys_platform", "linux"), + ), + ), + MultiMarker( + SingleMarker("python_version", ">=3.7"), + MarkerUnion( + SingleMarker("python_version", "<3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "A+BC_(A+B)(A+C)", + MarkerUnion( + SingleMarker("python_version", "<3.7"), + MultiMarker( + SingleMarker("python_version", ">=3.9"), + SingleMarker("sys_platform", "linux"), + ), + ), + MultiMarker( + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.9"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "(A+B(C+D))(E+F)_(A+B)(A+C+D)(E+F)", + 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"), + ), + ), + MultiMarker( + MarkerUnion( + SingleMarker("python_version", ">=3.9"), + SingleMarker("implementation_name", "cpython"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("python_version", ">=3.8"), + ), + MarkerUnion( + SingleMarker("sys_platform", "win32"), + SingleMarker("sys_platform", "linux"), + ), + ), + ), + ( + "A(B+C)+(D+E)(F+G)_(A+D+E)(B+C+D+E)(A+F+G)(B+C+F+G)", + 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"), + ), + ), + ), + MultiMarker( + MarkerUnion( + SingleMarker("sys_platform", "!=win32"), + SingleMarker("python_version", "<3.8"), + SingleMarker("python_version", ">=3.9"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.8"), + SingleMarker("python_version", ">=3.9"), + ), + MarkerUnion( + SingleMarker("python_version", "<3.7"), + SingleMarker("sys_platform", "!=linux"), + SingleMarker("python_version", ">=3.9"), + ), + ), + ), + ], +) +def test_cnf(scheme: str, marker: BaseMarker, expected: BaseMarker) -> None: + assert cnf(marker) == expected + + @pytest.mark.parametrize( "scheme, marker, expected", [ @@ -1356,6 +1543,16 @@ def test_empty_marker_is_found_in_complex_intersection( assert m2.intersect(m1).is_empty() +def test_empty_marker_is_found_in_complex_parse() -> None: + marker = parse_marker( + '(python_implementation != "pypy" or python_version != "3.6") and ' + '((python_implementation != "pypy" and python_version != "3.6") or' + ' (python_implementation == "pypy" and python_version == "3.6")) and ' + '(python_implementation == "pypy" or python_version == "3.6")' + ) + assert marker.is_empty() + + @pytest.mark.parametrize( "python_version, python_full_version, " "expected_intersection_version, expected_union_version",