diff --git a/docs/conf.py b/docs/conf.py index d89b26aa..b4da79ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,7 @@ "sphinx.ext.doctest", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", + "sphinx_toolbox.more_autodoc.autotypeddict", ] # General information about the project. diff --git a/docs/markers.rst b/docs/markers.rst index d3b7676d..dc010c72 100644 --- a/docs/markers.rst +++ b/docs/markers.rst @@ -62,8 +62,8 @@ Reference Evaluate the marker given the context of the current Python process. - :param dict environment: A dictionary containing keys and values to - override the detected environment. + :param Environment environment: A dictionary containing keys and values to + override the detected environment. :raises: UndefinedComparison: If the marker uses a comparison on strings which are not valid versions per the :ref:`specification of version specifiers @@ -71,6 +71,19 @@ Reference :raises: UndefinedEnvironmentName: If the marker accesses a value that isn't present inside of the environment dictionary. + :rtype: bool + +.. autotypeddict:: packaging.markers.Environment + + A dictionary that represents a Python environment. + +.. function:: default_environment() + + Returns a dictionary representing the current Python process. This is the + base environment that is used when evaluating markers in + :meth:`Marker.evaluate`. + + :rtype: Environment .. exception:: InvalidMarker diff --git a/docs/requirements.txt b/docs/requirements.txt index a95ae18b..299a951c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ furo +sphinx-toolbox +typing-extensions>=4.1.0; python_version < "3.9" diff --git a/pyproject.toml b/pyproject.toml index 9fa0fb17..0b3783aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ exclude_lines = ["pragma: no cover", "@abc.abstractmethod", "@abc.abstractproper strict = true show_error_codes = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +warn_unused_ignores = true [[tool.mypy.overrides]] module = ["_manylinux"] diff --git a/src/packaging/_compat.py b/src/packaging/_compat.py new file mode 100644 index 00000000..c00b639c --- /dev/null +++ b/src/packaging/_compat.py @@ -0,0 +1,35 @@ +import sys +import typing + +if sys.version_info[:2] >= (3, 8): # pragma: no cover + from typing import Literal +elif typing.TYPE_CHECKING: # pragma: no cover + from typing_extensions import Literal +else: # pragma: no cover + try: + from typing_extensions import Literal + except ImportError: + + class Literal: + def __init_subclass__(*_args, **_kwargs): + pass + + +if sys.version_info[:2] >= (3, 9): # pragma: no cover + from typing import TypedDict +elif typing.TYPE_CHECKING: # pragma: no cover + from typing_extensions import TypedDict +else: # pragma: no cover + try: + from typing_extensions import TypedDict + except ImportError: + + class TypedDict: + def __init_subclass__(*_args, **_kwargs): + pass + + +__all__ = [ + "Literal", + "TypedDict", +] diff --git a/src/packaging/markers.py b/src/packaging/markers.py index 8b98fca7..3c1d62fa 100644 --- a/src/packaging/markers.py +++ b/src/packaging/markers.py @@ -8,6 +8,7 @@ import sys from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from ._compat import TypedDict from ._parser import ( MarkerAtom, MarkerList, @@ -50,6 +51,89 @@ class UndefinedEnvironmentName(ValueError): """ +class Environment(TypedDict, total=False): + implementation_name: str + """The implementation's identifier, e.g. ``'cpython'``.""" + + implementation_version: str + """ + The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or + ``'7.3.13'`` for PyPy3.10 v7.3.13. + """ + + os_name: str + """ + The value of :py:data:`os.name`. The name of the operating system dependent module + imported, e.g. ``'posix'``. + """ + + platform_machine: str + """ + Returns the machine type, e.g. ``'i386'``. + + An empty string if the value cannot be determined. + """ + + platform_release: str + """ + The system's release, e.g. ``'2.2.0'`` or ``'NT'``. + + An empty string if the value cannot be determined. + """ + + platform_system: str + """ + The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``. + + An empty string if the value cannot be determined. + """ + + platform_version: str + """ + The system's release version, e.g. ``'#3 on degas'``. + + An empty string if the value cannot be determined. + """ + + python_full_version: str + """ + The Python version as string ``'major.minor.patchlevel'``. + + Note that unlike the Python :py:data:`sys.version`, this value will always include + the patchlevel (it defaults to 0). + """ + + platform_python_implementation: str + """ + A string identifying the Python implementation. + + Currently, the following implementations are identified: ``'CPython'`` (C + implementation of Python), ``'IronPython'`` (.NET implementation of Python), + ``'Jython'`` (Java implementation of Python), ``'PyPy'`` (Python implementation of + Python). + """ + + python_version: str + """The Python version as string ``'major.minor'``.""" + + sys_platform: str + """ + This string contains a platform identifier that can be used to append + platform-specific components to :py:data:`sys.path`, for instance. + + For Unix systems, except on Linux and AIX, this is the lowercased OS name as + returned by ``uname -s`` with the first part of the version as returned by + ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python + was built. + """ + + extra: Optional[str] + """ + An optional string used by wheels to signal which specifications apply to a given + extra in the wheel ``METADATA`` file. + """ + + def _normalize_extra_values(results: Any) -> Any: """ Normalize extra values. @@ -134,10 +218,11 @@ def _normalize(*values: str, key: str) -> Tuple[str, ...]: return values -def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: +def _evaluate_markers(markers: MarkerList, environment: Environment) -> bool: groups: List[List[bool]] = [[]] for marker in markers: + assert isinstance(marker, (list, tuple, str)) if isinstance(marker, list): @@ -147,12 +232,12 @@ def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: if isinstance(lhs, Variable): environment_key = lhs.value - lhs_value = environment[environment_key] + lhs_value = environment[environment_key] # type: ignore[literal-required] # noqa: E501 rhs_value = rhs.value else: lhs_value = lhs.value environment_key = rhs.value - rhs_value = environment[environment_key] + rhs_value = environment[environment_key] # type: ignore[literal-required] # noqa: E501 lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) groups[-1].append(_eval_op(lhs_value, op, rhs_value)) @@ -172,7 +257,7 @@ def format_full_version(info: "sys._version_info") -> str: return version -def default_environment() -> Dict[str, str]: +def default_environment() -> Environment: iver = format_full_version(sys.implementation.version) implementation_name = sys.implementation.name return { @@ -231,7 +316,7 @@ def __eq__(self, other: Any) -> bool: return str(self) == str(other) - def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: + def evaluate(self, environment: Optional[Environment] = None) -> bool: """Evaluate a marker. Return the boolean from evaluating the given marker against the diff --git a/src/packaging/metadata.py b/src/packaging/metadata.py index fb274930..e471d266 100644 --- a/src/packaging/metadata.py +++ b/src/packaging/metadata.py @@ -3,7 +3,6 @@ import email.message import email.parser import email.policy -import sys import typing from typing import ( Any, @@ -19,25 +18,9 @@ ) from . import requirements, specifiers, utils, version as version_module +from ._compat import Literal, TypedDict T = typing.TypeVar("T") -if sys.version_info[:2] >= (3, 8): # pragma: no cover - from typing import Literal, TypedDict -else: # pragma: no cover - if typing.TYPE_CHECKING: - from typing_extensions import Literal, TypedDict - else: - try: - from typing_extensions import Literal, TypedDict - except ImportError: - - class Literal: - def __init_subclass__(*_args, **_kwargs): - pass - - class TypedDict: - def __init_subclass__(*_args, **_kwargs): - pass try: