Skip to content
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
42 changes: 42 additions & 0 deletions src/poetry/core/version/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ def is_empty(self) -> bool:
def validate(self, environment: Mapping[str, Any] | None) -> bool:
raise NotImplementedError

@abstractmethod
def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
"""
Partially evaluate this marker against the (possibly partial) environment.

Similar to `validate`, but instead of collapsing the whole marker tree to
a single `bool`, this returns a simplified `BaseMarker`:

- Single markers whose variable name is present in `environment` are
validated and replaced with `AnyMarker` (if they hold) or `EmptyMarker`
(if they do not).
- Single markers whose variable name is not present in `environment` are
left untouched, so the result can still depend on those undefined
variables.
- Composite markers are simplified as usual.

Whereas `validate` treats missing variables as satisfied (returning
`True`), `apply` preserves them so the caller can decide later.
"""
raise NotImplementedError

@abstractmethod
def without_extras(self) -> BaseMarker:
raise NotImplementedError
Expand Down Expand Up @@ -148,6 +169,9 @@ def is_any(self) -> bool:
def validate(self, environment: Mapping[str, Any] | None) -> bool:
return True

def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
return self

def without_extras(self) -> BaseMarker:
return self

Expand Down Expand Up @@ -194,6 +218,9 @@ def is_empty(self) -> bool:
def validate(self, environment: Mapping[str, Any] | None) -> bool:
return False

def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
return self

def without_extras(self) -> BaseMarker:
return self

Expand Down Expand Up @@ -293,6 +320,11 @@ def validate(self, environment: Mapping[str, Any] | None) -> bool:
constraint = self._parser(environment[self._name])
return self._constraint.allows(constraint) # type: ignore[arg-type]

def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
if self._name not in environment:
return self
return AnyMarker() if self.validate(environment) else EmptyMarker()

def without_extras(self) -> BaseMarker:
return self.exclude("extra")

Expand Down Expand Up @@ -770,6 +802,11 @@ def union_simplify(self, other: BaseMarker) -> BaseMarker | None:
def validate(self, environment: Mapping[str, Any] | None) -> bool:
return all(m.validate(environment) for m in self._markers)

def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
if not environment:
return self
return self.of(*(m.apply(environment) for m in self._markers))

def without_extras(self) -> BaseMarker:
return self.exclude("extra")

Expand Down Expand Up @@ -976,6 +1013,11 @@ def intersect_simplify(self, other: BaseMarker) -> BaseMarker | None:
def validate(self, environment: Mapping[str, Any] | None) -> bool:
return any(m.validate(environment) for m in self._markers)

def apply(self, environment: Mapping[str, Any]) -> BaseMarker:
if not environment:
return self
return self.of(*(m.apply(environment) for m in self._markers))

def without_extras(self) -> BaseMarker:
return self.exclude("extra")

Expand Down
103 changes: 103 additions & 0 deletions tests/version/test_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,109 @@ def test_validate(
assert m.validate(environment) is expected


@pytest.mark.parametrize(
("marker_string", "environment", "expected"),
[
# AnyMarker / EmptyMarker are returned unchanged.
("", {}, ""),
("", {"os_name": "linux"}, ""),
(EMPTY, {}, EMPTY),
(EMPTY, {"os_name": "linux"}, EMPTY),
# Empty environment preserves the marker.
("os_name == 'foo'", {}, "os_name == 'foo'"),
(
"python_version >= '3.8' and sys_platform == 'linux'",
{},
"python_version >= '3.8' and sys_platform == 'linux'",
),
# SingleMarker with name in env collapses to Any/Empty.
("os_name == 'foo'", {"os_name": "foo"}, ""),
("os_name == 'foo'", {"os_name": "bar"}, EMPTY),
("python_version >= '3.8'", {"python_version": "3.10"}, ""),
("python_version >= '3.8'", {"python_version": "3.7"}, EMPTY),
# SingleMarker with name not in env is returned unchanged.
("os_name == 'foo'", {}, "os_name == 'foo'"),
("os_name == 'foo'", {"sys_platform": "linux"}, "os_name == 'foo'"),
# MultiMarker: only one name in env -> residual is the other branch.
(
"python_version >= '3.8' and sys_platform == 'linux'",
{"python_version": "3.10"},
"sys_platform == 'linux'",
),
(
"python_version >= '3.8' and sys_platform == 'linux'",
{"sys_platform": "linux"},
"python_version >= '3.8'",
),
# MultiMarker: one branch is False -> whole thing collapses to Empty.
(
"python_version >= '3.8' and sys_platform == 'linux'",
{"python_version": "3.7"},
EMPTY,
),
# MultiMarker: both names known.
(
"python_version >= '3.8' and sys_platform == 'linux'",
{"python_version": "3.10", "sys_platform": "linux"},
"",
),
(
"python_version >= '3.8' and sys_platform == 'linux'",
{"python_version": "3.10", "sys_platform": "win32"},
EMPTY,
),
# MarkerUnion: one branch True -> collapses to Any.
(
"python_version >= '3.8' or sys_platform == 'linux'",
{"python_version": "3.10"},
"",
),
# MarkerUnion: one branch False -> residual is the other branch.
(
"python_version >= '3.8' or sys_platform == 'linux'",
{"python_version": "3.7"},
"sys_platform == 'linux'",
),
# MarkerUnion: both names known.
(
"python_version >= '3.8' or sys_platform == 'linux'",
{"python_version": "3.7", "sys_platform": "win32"},
EMPTY,
),
# Nested: (A or B) and C, only python_version known.
(
"(python_version >= '3.8' or sys_platform == 'win32') and extra == 'foo'",
{"python_version": "3.10"},
"extra == 'foo'",
),
(
"(python_version >= '3.8' or sys_platform == 'win32') and extra == 'foo'",
{"python_version": "3.7"},
"sys_platform == 'win32' and extra == 'foo'",
),
# extra: name in env is evaluated, including empty-tuple case.
("extra == 'a'", {"extra": "a"}, ""),
("extra == 'a'", {"extra": "b"}, EMPTY),
("extra == 'a'", {"extra": ()}, EMPTY),
("extra != 'a'", {"extra": ()}, ""),
# extra: name not in env is preserved.
("extra == 'a'", {}, "extra == 'a'"),
("extra == 'a'", {"python_version": "3.10"}, "extra == 'a'"),
# AtomicMultiMarker / AtomicMarkerUnion (compact extra forms).
("extra != 'a' and extra != 'b'", {"extra": ("c",)}, ""),
("extra != 'a' and extra != 'b'", {"extra": ("a",)}, EMPTY),
("extra != 'a' and extra != 'b'", {}, "extra != 'a' and extra != 'b'"),
("extra == 'a' or extra == 'b'", {"extra": ("a",)}, ""),
("extra == 'a' or extra == 'b'", {"extra": ("c",)}, EMPTY),
("extra == 'a' or extra == 'b'", {}, "extra == 'a' or extra == 'b'"),
],
)
def test_apply(marker_string: str, environment: dict[str, str], expected: str) -> None:
m = parse_marker(marker_string)

assert m.apply(environment) == parse_marker(expected)


@pytest.mark.parametrize(
"marker, env",
[
Expand Down
Loading