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
1 change: 1 addition & 0 deletions docs/changelog/+target-marker-environment.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose the selected interpreter's dependency marker values as ``PythonInfo.marker_environment``.
5 changes: 5 additions & 0 deletions docs/how-to/standalone-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
+platform: str
+sysconfig_vars: dict
+sysconfig_paths: dict
+marker_environment: dict[str, str]
+machine: str
+free_threaded: bool
+debug_build: bool
Expand All @@ -178,6 +179,10 @@ Once you have a :class:`~python_discovery.PythonInfo`, you can inspect everythin
info.debug_build # True if this is a Py_DEBUG build.
info.sysconfig_vars # All sysconfig.get_config_vars() values.
info.sysconfig_paths # All sysconfig.get_paths() values.
info.marker_environment

Use ``marker_environment`` to evaluate :pep:`508` dependency markers for the discovered interpreter. The mapping
contains all standard marker keys, including its implementation version and operating system.

Prefer :attr:`~python_discovery.PythonInfo.system_exe` over the raw
:attr:`~python_discovery.PythonInfo.system_executable` field it reads. The field is typed ``str | None`` because it
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ urls.Tracker = "https://github.com/tox-dev/python-discovery/issues"
test = [
"covdefaults>=2.3",
"coverage>=7.6.1",
"packaging>=23.2",
"pytest>=8.3.5",
"pytest-mock>=3.14.1",
"setuptools>=75.3.4",
Expand Down
1 change: 1 addition & 0 deletions src/python_discovery/_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class PythonInfo: # ruff:ignore[too-many-public-methods]
os: str
free_threaded: bool
debug_build: bool
marker_environment: dict[str, str]
prefix: str | None
base_prefix: str | None
real_prefix: str | None
Expand Down
24 changes: 24 additions & 0 deletions src/python_discovery/_py_info_collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ def _init_identity(self):
self.os = os.name
self.free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
self.debug_build = bool(sysconfig.get_config_var("Py_DEBUG"))
# Vermin checks the whole script against Python 2.7 despite the version gate above.
interpreter = getattr(sys, "implementation") # ruff:ignore[get-attr-with-constant]
interpreter_version = interpreter.version
marker_version = "{}.{}.{}{}".format(
interpreter_version.major,
interpreter_version.minor,
interpreter_version.micro,
"{}{}".format(interpreter_version.releaselevel[0], interpreter_version.serial)
if interpreter_version.releaselevel != "final"
else "",
)
self.marker_environment = {
"implementation_name": interpreter.name,
"implementation_version": marker_version,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"platform_python_implementation": platform.python_implementation(),
"python_version": ".".join(platform.python_version_tuple()[:2]),
"sys_platform": sys.platform,
}

def _init_prefixes(self):
def abs_path(value):
Expand Down
45 changes: 44 additions & 1 deletion tests/py_info/test_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
import sysconfig
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING, NamedTuple
from typing import TYPE_CHECKING, Final, NamedTuple

import pytest
from packaging.markers import default_environment
from setuptools.dist import Distribution

from python_discovery import DiskCache, PythonInfo, PythonSpec
from python_discovery import _cached_py_info as cached_py_info
from python_discovery._py_info import VersionInfo

if TYPE_CHECKING:
from packaging.markers import Environment
from pytest_mock import MockerFixture

IS_PYPY = PythonInfo.current_system().implementation == "PyPy"
Expand Down Expand Up @@ -492,6 +494,47 @@ def test_py_info_json_round_trip() -> None:
assert restored.machine == CURRENT.machine


def test_marker_environment_matches_target_interpreter() -> None:
info: Final[PythonInfo | None] = PythonInfo.from_exe(sys.executable, ignore_cache=True, resolve_to_host=False)
assert info is not None
assert info.marker_environment == default_environment()


def test_marker_environment_uses_target_platform(tmp_path: Path) -> None:
(tmp_path / "sitecustomize.py").write_text(
'import platform\nplatform.machine = lambda: "target-machine"\nplatform.release = lambda: "target-release"\n',
encoding="utf-8",
)
expected: Final[Environment] = default_environment()
assert expected["platform_machine"] != "target-machine"
assert expected["platform_release"] != "target-release"
info: Final[PythonInfo | None] = PythonInfo.from_exe(
sys.executable,
ignore_cache=True,
resolve_to_host=False,
env={**os.environ, "PYTHONPATH": str(tmp_path)},
)
assert info is not None
assert info.marker_environment == {
**expected,
"platform_machine": "target-machine",
"platform_release": "target-release",
}


def test_marker_environment_survives_json_round_trip() -> None:
assert PythonInfo.from_json(CURRENT.to_json()).marker_environment == CURRENT.marker_environment


def test_marker_environment_formats_prerelease(mocker: MockerFixture) -> None:
version: Final[VersionInfo] = VersionInfo(*sys.implementation.version)
mocker.patch.object(sys.implementation, "version", version._replace(releaselevel="candidate", serial=1))

assert PythonInfo().marker_environment["implementation_version"] == (
f"{version.major}.{version.minor}.{version.micro}c1"
)


@pytest.mark.parametrize(
("target_platform", "discovered_platforms", "expected_idx"),
[
Expand Down