Skip to content

Commit

Permalink
Ignore certain exceptions during marker evaluation
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardpeek-crown-public committed Nov 12, 2024
1 parent 9167919 commit 8a82eb2
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 9 deletions.
41 changes: 34 additions & 7 deletions src/packaging/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from __future__ import annotations

import functools
import operator
import os
import platform
Expand All @@ -16,6 +15,7 @@
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
from .version import InvalidVersion

__all__ = [
"InvalidMarker",
Expand Down Expand Up @@ -203,14 +203,13 @@ def _normalize(*values: str, key: str) -> tuple[str, ...]:


def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
# Lazy evaluation to mitigate https://github.com/pypa/packaging/issues/774
groups: list[list[Callable[[], bool]]] = [[]]
groups: list[list[bool | Exception]] = [[]]

for marker in markers:
assert isinstance(marker, (list, tuple, str))

if isinstance(marker, list):
groups[-1].append(functools.partial(_evaluate_markers, marker, environment))
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
lhs, op, rhs = marker

Expand All @@ -224,14 +223,42 @@ def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
rhs_value = environment[environment_key]

lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
groups[-1].append(functools.partial(_eval_op, lhs_value, op, rhs_value))

# Defer handling certain exceptions for cases where the marker expression is
# otherwise well formed and they do not end up affecting the overall result.
op_result: bool | Exception
try:
op_result = _eval_op(lhs_value, op, rhs_value)
# Version comparisons may be overly strict despite being guarded against.
# https://github.com/pypa/packaging/issues/774
except InvalidVersion as e:
op_result = e

groups[-1].append(op_result)
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])

return any(all(expr() for expr in group) for group in groups)

# The below is almost equivalent to `any(all(group) for group in groups)` except
# that exceptions are treated as an indeterminate logic level between true and false
any_result: bool | Exception = False
for group in groups:
all_result: bool | Exception = True
for op_result in group:
# Value precedence for `all()` is: `False`, `Exception()`, `True`
if (all_result is True) or (op_result is False):
all_result = op_result

# Value precedence for `any()` is: `True`, `Exception()`, `False`
if (any_result is False) or (all_result is True):
any_result = all_result

# Raise if the overall result is indeterminate due to a expression that errored out
if isinstance(any_result, Exception):
raise any_result

return any_result

def format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
Expand Down
18 changes: 16 additions & 2 deletions tests/test_markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
default_environment,
format_full_version,
)
from packaging.version import InvalidVersion

VARIABLES = [
"extra",
Expand Down Expand Up @@ -321,12 +322,25 @@ def test_environment_with_extra_none(self):
"sys_platform == 'foo_os' and platform_release >= '4.5.6'",
{"sys_platform": "bar_os", "platform_release": "1.2.3-invalid"},
False,
)
),
(
"platform_release >= '4.5.6' and sys_platform == 'foo_os'",
{"sys_platform": "bar_os", "platform_release": "1.2.3-invalid"},
False,
),
(
"platform_release >= '4.5.6'",
{"platform_release": "1.2.3-invalid"},
InvalidVersion,
),
],
)
def test_evaluates(self, marker_string, environment, expected):
args = [] if environment is None else [environment]
assert Marker(marker_string).evaluate(*args) == expected
try:
assert Marker(marker_string).evaluate(*args) == expected
except Exception as e:
assert isinstance(e, expected)

@pytest.mark.parametrize(
"marker_string",
Expand Down

0 comments on commit 8a82eb2

Please sign in to comment.