88import os
99import platform
1010import sys
11- from typing import Any , Callable , TypedDict , cast
11+ from typing import AbstractSet , Any , Callable , Literal , TypedDict , Union , cast
1212
1313from ._parser import MarkerAtom , MarkerList , Op , Value , Variable
1414from ._parser import parse_marker as _parse_marker
1717from .utils import canonicalize_name
1818
1919__all__ = [
20+ "EvaluateContext" ,
2021 "InvalidMarker" ,
2122 "Marker" ,
2223 "UndefinedComparison" ,
2324 "UndefinedEnvironmentName" ,
2425 "default_environment" ,
2526]
2627
27- Operator = Callable [[str , str ], bool ]
28+ Operator = Callable [[str , Union [str , AbstractSet [str ]]], bool ]
29+ EvaluateContext = Literal ["metadata" , "lock_file" , "requirement" ]
30+ MARKERS_ALLOWING_SET = {"extras" , "dependency_groups" }
2831
2932
3033class InvalidMarker (ValueError ):
@@ -174,13 +177,14 @@ def _format_marker(
174177}
175178
176179
177- def _eval_op (lhs : str , op : Op , rhs : str ) -> bool :
178- try :
179- spec = Specifier ("" .join ([op .serialize (), rhs ]))
180- except InvalidSpecifier :
181- pass
182- else :
183- return spec .contains (lhs , prereleases = True )
180+ def _eval_op (lhs : str , op : Op , rhs : str | AbstractSet [str ]) -> bool :
181+ if isinstance (rhs , str ):
182+ try :
183+ spec = Specifier ("" .join ([op .serialize (), rhs ]))
184+ except InvalidSpecifier :
185+ pass
186+ else :
187+ return spec .contains (lhs , prereleases = True )
184188
185189 oper : Operator | None = _operators .get (op .serialize ())
186190 if oper is None :
@@ -189,19 +193,29 @@ def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
189193 return oper (lhs , rhs )
190194
191195
192- def _normalize (* values : str , key : str ) -> tuple [str , ...]:
196+ def _normalize (
197+ lhs : str , rhs : str | AbstractSet [str ], key : str
198+ ) -> tuple [str , str | AbstractSet [str ]]:
193199 # PEP 685 – Comparison of extra names for optional distribution dependencies
194200 # https://peps.python.org/pep-0685/
195201 # > When comparing extra names, tools MUST normalize the names being
196202 # > compared using the semantics outlined in PEP 503 for names
197203 if key == "extra" :
198- return tuple (canonicalize_name (v ) for v in values )
204+ assert isinstance (rhs , str ), "extra value must be a string"
205+ return (canonicalize_name (lhs ), canonicalize_name (rhs ))
206+ if key in MARKERS_ALLOWING_SET :
207+ if isinstance (rhs , str ): # pragma: no cover
208+ return (canonicalize_name (lhs ), canonicalize_name (rhs ))
209+ else :
210+ return (canonicalize_name (lhs ), {canonicalize_name (v ) for v in rhs })
199211
200212 # other environment markers don't have such standards
201- return values
213+ return lhs , rhs
202214
203215
204- def _evaluate_markers (markers : MarkerList , environment : dict [str , str ]) -> bool :
216+ def _evaluate_markers (
217+ markers : MarkerList , environment : dict [str , str | AbstractSet [str ]]
218+ ) -> bool :
205219 groups : list [list [bool ]] = [[]]
206220
207221 for marker in markers :
@@ -220,7 +234,7 @@ def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
220234 lhs_value = lhs .value
221235 environment_key = rhs .value
222236 rhs_value = environment [environment_key ]
223-
237+ assert isinstance ( lhs_value , str ), "lhs must be a string"
224238 lhs_value , rhs_value = _normalize (lhs_value , rhs_value , key = environment_key )
225239 groups [- 1 ].append (_eval_op (lhs_value , op , rhs_value ))
226240 else :
@@ -298,34 +312,51 @@ def __eq__(self, other: Any) -> bool:
298312
299313 return str (self ) == str (other )
300314
301- def evaluate (self , environment : dict [str , str ] | None = None ) -> bool :
315+ def evaluate (
316+ self ,
317+ environment : dict [str , str ] | None = None ,
318+ context : EvaluateContext = "metadata" ,
319+ ) -> bool :
302320 """Evaluate a marker.
303321
304322 Return the boolean from evaluating the given marker against the
305323 environment. environment is an optional argument to override all or
306- part of the determined environment.
324+ part of the determined environment. The *context* parameter specifies what
325+ context the markers are being evaluated for, which influences what markers
326+ are considered valid. Acceptable values are "metadata" (for core metadata;
327+ default), "lock_file", and "requirement" (i.e. all other situations).
307328
308329 The environment is determined from the current Python process.
309330 """
310- current_environment = cast ("dict[str, str]" , default_environment ())
311- current_environment ["extra" ] = ""
331+ current_environment = cast (
332+ "dict[str, str | AbstractSet[str]]" , default_environment ()
333+ )
334+ if context == "lock_file" :
335+ current_environment .update (
336+ extras = frozenset (), dependency_groups = frozenset ()
337+ )
338+ elif context == "metadata" :
339+ current_environment ["extra" ] = ""
312340 if environment is not None :
313341 current_environment .update (environment )
314342 # The API used to allow setting extra to None. We need to handle this
315343 # case for backwards compatibility.
316- if current_environment ["extra" ] is None :
344+ if "extra" in current_environment and current_environment ["extra" ] is None :
317345 current_environment ["extra" ] = ""
318346
319347 return _evaluate_markers (
320348 self ._markers , _repair_python_full_version (current_environment )
321349 )
322350
323351
324- def _repair_python_full_version (env : dict [str , str ]) -> dict [str , str ]:
352+ def _repair_python_full_version (
353+ env : dict [str , str | AbstractSet [str ]],
354+ ) -> dict [str , str | AbstractSet [str ]]:
325355 """
326356 Work around platform.python_version() returning something that is not PEP 440
327357 compliant for non-tagged Python builds.
328358 """
329- if env ["python_full_version" ].endswith ("+" ):
330- env ["python_full_version" ] += "local"
359+ python_full_version = cast (str , env ["python_full_version" ])
360+ if python_full_version .endswith ("+" ):
361+ env ["python_full_version" ] = f"{ python_full_version } local"
331362 return env
0 commit comments