diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a98e61c..35e11aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,6 +36,24 @@ jobs: # GIL disabled; see docs/free-threading.md. - "3.14t" experimental: [ false ] + include: + # Python 3.15, which `allow-prereleases` below resolves to the + # newest pre-release the runner image publishes -- 3.15.0rc1 at the + # time of writing, and 3.15.0 itself once it ships in October 2026, + # with no change needed here. + # + # Advisory (`experimental: true` -> `continue-on-error`) while 3.15 + # is a pre-release: the suite passes on 3.15.0rc1 today, but rc2 and + # the final release are still to come, and a regression in CPython + # itself -- or a test dependency that has no 3.15 wheels yet and + # fails to build from source -- must not block merges on a version + # nothing runs in production. Move these two entries up into the + # `python-version` list above (and delete them from `include`) to + # make 3.15 a required check once 3.15.0 final is out. + - python-version: "3.15" + experimental: true + - python-version: "3.15t" + experimental: true steps: - uses: actions/checkout@v4 @@ -49,6 +67,13 @@ jobs: - uses: "actions/setup-python@v5" with: python-version: "${{ matrix.python-version }}" + # Required for the 3.15 rows: without it the version spec only + # matches stable releases and the job fails with "Version 3.15 was + # not found in the local cache". It is safe to set for every row -- + # it widens `3.X` to `~3.X.0-0`, and a pre-release only wins when no + # stable release satisfies the spec, so 3.10-3.14 still resolve to + # their newest stable patch. + allow-prereleases: true cache: "pip" cache-dependency-path: | requirements-docs.txt diff --git a/mode/utils/objects.py b/mode/utils/objects.py index 25a2822..0e7cf79 100644 --- a/mode/utils/objects.py +++ b/mode/utils/objects.py @@ -18,7 +18,7 @@ from contextlib import suppress from decimal import Decimal from functools import total_ordering -from inspect import get_annotations +from inspect import get_annotations, signature from pathlib import Path from typing import ( Any, @@ -34,11 +34,42 @@ ) try: - from typing import _eval_type # type: ignore -except ImportError: + from typing import _eval_type as _typing_eval_type # type: ignore +except ImportError: # pragma: no cover + _typing_eval_type = None + _EVAL_TYPE_WANTS_TYPE_PARAMS = False +else: + # `type_params` (the PEP 695 type parameters in scope) was added to + # `typing._eval_type` in 3.13, where omitting it only emitted a + # DeprecationWarning and defaulted to `()`. On 3.15 it became a required + # positional argument, so calling without it now fails outright with + # "TypeError: _eval_type() missing 1 required positional argument: + # 'type_params'". + # + # The signature is inspected once, here at import. A try/except TypeError + # around the call would be wrong: `_eval_type` legitimately raises + # TypeError for annotations `typing._type_check` rejects, and `eval_type` + # below depends on being able to tell those apart. + _EVAL_TYPE_WANTS_TYPE_PARAMS = ( + "type_params" in signature(_typing_eval_type).parameters + ) + - def _eval_type(t, globalns, localns, recursive_guard=frozenset()): # type: ignore +def _eval_type( + t: Any, + globalns: Optional[dict[str, Any]], + localns: Optional[dict[str, Any]], +) -> Any: + # Nothing resolved through here carries PEP 695 type parameters of its + # own, so `()` is both what 3.13/3.14 already substituted when the + # argument was omitted and what 3.15 requires -- passing it explicitly + # keeps behaviour identical across versions, and silences the 3.13/3.14 + # deprecation warning on the way. + if _typing_eval_type is None: # pragma: no cover return t + if _EVAL_TYPE_WANTS_TYPE_PARAMS: + return _typing_eval_type(t, globalns, localns, ()) + return _typing_eval_type(t, globalns, localns) def _is_class_var(typ: Any) -> bool: diff --git a/pyproject.toml b/pyproject.toml index 0c2a806..ae48c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Free Threading :: 2 - Beta", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy",