updates
This commit is contained in:
@@ -1,28 +1,41 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python version compatibility code and random general utilities."""
|
||||
|
||||
"""Python version compatibility code."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
from inspect import Parameter
|
||||
from inspect import Signature
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from inspect import Parameter
|
||||
from inspect import signature
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Final
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import NoReturn
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
import py
|
||||
|
||||
# fmt: off
|
||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
|
||||
# If `overload` is imported from `compat` instead of from `typing`,
|
||||
# Sphinx doesn't recognize it as `overload` and the API docs for
|
||||
# overloaded functions look good again. But type checkers handle
|
||||
# it fine.
|
||||
# fmt: on
|
||||
if True:
|
||||
from typing import overload as overload
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
from annotationlib import Format
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
|
||||
#: constant to prepare valuing pylib path replacements/lazy proxies later on
|
||||
# intended for removal in pytest 8.0 or 9.0
|
||||
|
||||
@@ -42,16 +55,32 @@ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET: Final = NotSetType.token
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
import importlib.metadata
|
||||
|
||||
importlib_metadata = importlib.metadata
|
||||
else:
|
||||
import importlib_metadata as importlib_metadata # noqa: F401
|
||||
|
||||
|
||||
def _format_args(func: Callable[..., Any]) -> str:
|
||||
return str(signature(func))
|
||||
|
||||
|
||||
def is_generator(func: object) -> bool:
|
||||
genfunc = inspect.isgeneratorfunction(func)
|
||||
return genfunc and not iscoroutinefunction(func)
|
||||
|
||||
|
||||
def iscoroutinefunction(func: object) -> bool:
|
||||
"""Return True if func is a coroutine function (a function defined with async
|
||||
def syntax, and doesn't contain yield), or a function decorated with
|
||||
@asyncio.coroutine.
|
||||
|
||||
Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
|
||||
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
|
||||
importing asyncio directly, which in turns also initializes the "logging"
|
||||
module as a side-effect (see issue #8).
|
||||
"""
|
||||
@@ -64,14 +93,7 @@ def is_async_function(func: object) -> bool:
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def signature(obj: Callable[..., Any]) -> Signature:
|
||||
"""Return signature without evaluating annotations."""
|
||||
if sys.version_info >= (3, 14):
|
||||
return inspect.signature(obj, annotation_format=Format.STRING)
|
||||
return inspect.signature(obj)
|
||||
|
||||
|
||||
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -81,8 +103,8 @@ def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
return f"{relfn}:{lineno + 1}"
|
||||
return f"{fn}:{lineno + 1}"
|
||||
return "%s:%d" % (relfn, lineno + 1)
|
||||
return "%s:%d" % (fn, lineno + 1)
|
||||
|
||||
|
||||
def num_mock_patch_args(function) -> int:
|
||||
@@ -105,9 +127,10 @@ def num_mock_patch_args(function) -> int:
|
||||
|
||||
|
||||
def getfuncargnames(
|
||||
function: Callable[..., object],
|
||||
function: Callable[..., Any],
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: type | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
@@ -118,8 +141,9 @@ def getfuncargnames(
|
||||
* Aren't bound with functools.partial.
|
||||
* Aren't replaced with mocks.
|
||||
|
||||
The cls arguments indicate that the function should be treated as a bound
|
||||
method even though it's not unless the function is a static method.
|
||||
The is_method and cls arguments indicate that the function should
|
||||
be treated as a bound method even though it's not unless, only in
|
||||
the case of cls, the function is a static method.
|
||||
|
||||
The name parameter should be the original name in which the function was collected.
|
||||
"""
|
||||
@@ -133,7 +157,7 @@ def getfuncargnames(
|
||||
# creates a tuple of the names of the parameters that don't have
|
||||
# defaults.
|
||||
try:
|
||||
parameters = signature(function).parameters.values()
|
||||
parameters = signature(function).parameters
|
||||
except (ValueError, TypeError) as e:
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
@@ -144,7 +168,7 @@ def getfuncargnames(
|
||||
|
||||
arg_names = tuple(
|
||||
p.name
|
||||
for p in parameters
|
||||
for p in parameters.values()
|
||||
if (
|
||||
p.kind is Parameter.POSITIONAL_OR_KEYWORD
|
||||
or p.kind is Parameter.KEYWORD_ONLY
|
||||
@@ -155,9 +179,9 @@ def getfuncargnames(
|
||||
name = function.__name__
|
||||
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, and its first parameter
|
||||
# wasn't defined as positional only, remove the first parameter name.
|
||||
if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if is_method or (
|
||||
# Not using `getattr` because we don't want to resolve the staticmethod.
|
||||
# Not using `cls.__dict__` because we want to check the entire MRO.
|
||||
cls
|
||||
@@ -192,13 +216,25 @@ _non_printable_ascii_translate_table.update(
|
||||
)
|
||||
|
||||
|
||||
def _translate_non_printable(s: str) -> str:
|
||||
return s.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
STRING_TYPES = bytes, str
|
||||
|
||||
|
||||
def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||
|
||||
and escapes strings into a sequence of escaped unicode ids, e.g.:
|
||||
and escapes unicode objects into a sequence of escaped unicode
|
||||
ids, e.g.:
|
||||
|
||||
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||
|
||||
@@ -209,22 +245,67 @@ def ascii_escaped(val: bytes | str) -> str:
|
||||
a UTF-8 string.
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
ret = val.decode("ascii", "backslashreplace")
|
||||
ret = _bytes_to_ascii(val)
|
||||
else:
|
||||
ret = val.encode("unicode_escape").decode("ascii")
|
||||
return ret.translate(_non_printable_ascii_translate_table)
|
||||
return _translate_non_printable(ret)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _PytestWrapper:
|
||||
"""Dummy wrapper around a function object for internal use only.
|
||||
|
||||
Used to correctly unwrap the underlying function object when we are
|
||||
creating fixtures, because we wrap the function object ourselves with a
|
||||
decorator to issue warnings when the fixture function is called directly.
|
||||
"""
|
||||
|
||||
obj: Any
|
||||
|
||||
|
||||
def get_real_func(obj):
|
||||
"""Get the real function object of the (possibly) wrapped object by
|
||||
:func:`functools.wraps`, or :func:`functools.partial`."""
|
||||
obj = inspect.unwrap(obj)
|
||||
functools.wraps or functools.partial."""
|
||||
start_obj = obj
|
||||
for i in range(100):
|
||||
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
|
||||
# to trigger a warning if it gets called directly instead of by pytest: we don't
|
||||
# want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
|
||||
new_obj = getattr(obj, "__pytest_wrapped__", None)
|
||||
if isinstance(new_obj, _PytestWrapper):
|
||||
obj = new_obj.obj
|
||||
break
|
||||
new_obj = getattr(obj, "__wrapped__", None)
|
||||
if new_obj is None:
|
||||
break
|
||||
obj = new_obj
|
||||
else:
|
||||
from _pytest._io.saferepr import saferepr
|
||||
|
||||
raise ValueError(
|
||||
("could not find real function of {start}\nstopped at {current}").format(
|
||||
start=saferepr(start_obj), current=saferepr(obj)
|
||||
)
|
||||
)
|
||||
if isinstance(obj, functools.partial):
|
||||
obj = obj.func
|
||||
return obj
|
||||
|
||||
|
||||
def get_real_method(obj, holder):
|
||||
"""Attempt to obtain the real function object that might be wrapping
|
||||
``obj``, while at the same time returning a bound method to ``holder`` if
|
||||
the original object was a bound method."""
|
||||
try:
|
||||
is_method = hasattr(obj, "__func__")
|
||||
obj = get_real_func(obj)
|
||||
except Exception: # pragma: no cover
|
||||
return obj
|
||||
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
|
||||
obj = obj.__get__(holder)
|
||||
return obj
|
||||
|
||||
|
||||
def getimfunc(func):
|
||||
try:
|
||||
return func.__func__
|
||||
@@ -257,6 +338,47 @@ def safe_isclass(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
from typing_extensions import final as final
|
||||
elif sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
|
||||
def final(f):
|
||||
return f
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
|
||||
def __init__(self, func: Callable[[_S], _T]) -> None:
|
||||
self.func = func
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: type[_S] | None = ...
|
||||
) -> cached_property[_S, _T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||
return value
|
||||
|
||||
|
||||
def get_user_id() -> int | None:
|
||||
"""Return the current process's real user id or None if it could not be
|
||||
determined.
|
||||
@@ -278,37 +400,36 @@ def get_user_id() -> int | None:
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import assert_never
|
||||
else:
|
||||
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})"
|
||||
|
||||
|
||||
class CallableBool:
|
||||
"""
|
||||
A bool-like object that can also be called, returning its true/false value.
|
||||
|
||||
Used for backwards compatibility in cases where something was supposed to be a method
|
||||
but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).
|
||||
|
||||
Do not use in new code.
|
||||
"""
|
||||
|
||||
def __init__(self, value: bool) -> None:
|
||||
self._value = value
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self._value
|
||||
|
||||
def __call__(self) -> bool:
|
||||
return self._value
|
||||
|
||||
|
||||
def running_on_ci() -> bool:
|
||||
"""Check if we're currently running on a CI system."""
|
||||
# Only enable CI mode if one of these env variables is defined and non-empty.
|
||||
# Note: review `regendoc` tox env in case this list is changed.
|
||||
env_vars = ["CI", "BUILD_NUMBER"]
|
||||
return any(os.environ.get(var) for var in env_vars)
|
||||
# Perform exhaustiveness checking.
|
||||
#
|
||||
# Consider this example:
|
||||
#
|
||||
# MyUnion = Union[int, str]
|
||||
#
|
||||
# def handle(x: MyUnion) -> int {
|
||||
# if isinstance(x, int):
|
||||
# return 1
|
||||
# elif isinstance(x, str):
|
||||
# return 2
|
||||
# else:
|
||||
# raise Exception('unreachable')
|
||||
#
|
||||
# Now suppose we add a new variant:
|
||||
#
|
||||
# MyUnion = Union[int, str, bytes]
|
||||
#
|
||||
# After doing this, we must remember ourselves to go and update the handle
|
||||
# function to handle the new variant.
|
||||
#
|
||||
# With `assert_never` we can do better:
|
||||
#
|
||||
# // raise Exception('unreachable')
|
||||
# return assert_never(x)
|
||||
#
|
||||
# Now, if we forget to handle the new variant, the type-checker will emit a
|
||||
# compile-time error, instead of the runtime error we would have gotten
|
||||
# previously.
|
||||
#
|
||||
# This also work for Enums (if you use `is` to compare) and Literals.
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})"
|
||||
|
||||
Reference in New Issue
Block a user