Merge pull request #12467 from RonnyPfannschmidt/ronny/new-annotations-try-2

from __future__ import annotations + migrate
This commit is contained in:
Ronny Pfannschmidt 2024-06-21 10:24:56 +02:00 committed by GitHub
commit bbe6b4a218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
244 changed files with 2016 additions and 1805 deletions

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import sys

View File

@ -2,6 +2,8 @@
# 2.7.5 3.3.2
# FilesCompleter 75.1109 69.2116
# FastFilesCompleter 0.7383 1.0760
from __future__ import annotations
import timeit

View File

@ -1,2 +1,5 @@
from __future__ import annotations
for i in range(1000):
exec("def test_func_%d(): pass" % i)

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from unittest import TestCase # noqa: F401

View File

@ -1,3 +1,6 @@
from __future__ import annotations
for i in range(5000):
exec(
f"""

View File

@ -0,0 +1,3 @@
Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import.
-- by :user:`RonnyPfannschmidt`

View File

@ -15,15 +15,19 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
from __future__ import annotations
import os
from pathlib import Path
import shutil
from textwrap import dedent
from typing import TYPE_CHECKING
from _pytest import __version__ as version
from _pytest import __version__ as full_version
version = full_version.split("+")[0]
if TYPE_CHECKING:
import sphinx.application
@ -189,6 +193,7 @@ nitpick_ignore = [
("py:class", "SubRequest"),
("py:class", "TerminalReporter"),
("py:class", "_pytest._code.code.TerminalRepr"),
("py:class", "TerminalRepr"),
("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
("py:class", "_pytest.logging.LogCaptureHandler"),
("py:class", "_pytest.mark.structures.ParameterSet"),
@ -210,13 +215,16 @@ nitpick_ignore = [
("py:class", "_PluggyPlugin"),
# TypeVars
("py:class", "_pytest._code.code.E"),
("py:class", "E"), # due to delayed annotation
("py:class", "_pytest.fixtures.FixtureFunction"),
("py:class", "_pytest.nodes._NodeType"),
("py:class", "_NodeType"), # due to delayed annotation
("py:class", "_pytest.python_api.E"),
("py:class", "_pytest.recwarn.T"),
("py:class", "_pytest.runner.TResult"),
("py:obj", "_pytest.fixtures.FixtureValue"),
("py:obj", "_pytest.stash.T"),
("py:class", "_ScopeName"),
]
@ -455,7 +463,7 @@ intersphinx_mapping = {
}
def setup(app: "sphinx.application.Sphinx") -> None:
def setup(app: sphinx.application.Sphinx) -> None:
app.add_crossref_type(
"fixture",
"fixture",

View File

@ -1 +1,4 @@
from __future__ import annotations
collect_ignore = ["conf.py"]

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest
from pytest import raises

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import os.path
import pytest

View File

@ -1,3 +1,6 @@
from __future__ import annotations
hello = "world"

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import os.path
import shutil

View File

@ -1,3 +1,6 @@
from __future__ import annotations
def setup_module(module):
module.TestStateFullThing.classcount = 0

View File

@ -1 +1,4 @@
from __future__ import annotations
collect_ignore = ["nonpython", "customdirectory"]

View File

@ -1,4 +1,6 @@
# content of conftest.py
from __future__ import annotations
import json
import pytest

View File

@ -1,3 +1,6 @@
# content of test_first.py
from __future__ import annotations
def test_1():
pass

View File

@ -1,3 +1,6 @@
# content of test_second.py
from __future__ import annotations
def test_2():
pass

View File

@ -1,3 +1,6 @@
# content of test_third.py
from __future__ import annotations
def test_3():
pass

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,6 +1,8 @@
"""Module containing a parametrized tests testing cross-python serialization
via the pickle module."""
from __future__ import annotations
import shutil
import subprocess
import textwrap

View File

@ -1,4 +1,6 @@
# content of conftest.py
from __future__ import annotations
import pytest

View File

@ -1,5 +1,6 @@
# run this with $ pytest --collect-only test_collectonly.py
#
from __future__ import annotations
def test_function():

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import json
from pathlib import Path
import sys

View File

@ -98,6 +98,7 @@ lint.select = [
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"FA100", # add future annotations
"I", # isort
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PIE", # flake8-pie
@ -155,6 +156,10 @@ lint.per-file-ignores."src/_pytest/_version.py" = [
lint.per-file-ignores."testing/python/approx.py" = [
"B015",
]
lint.extend-safe-fixes = [
"UP006",
"UP007",
]
lint.isort.combine-as-imports = true
lint.isort.force-single-line = true
lint.isort.force-sort-within-sections = true
@ -164,9 +169,13 @@ lint.isort.known-local-folder = [
]
lint.isort.lines-after-imports = 2
lint.isort.order-by-type = false
lint.isort.required-imports = [
"from __future__ import annotations",
]
# In order to be able to format for 88 char in ruff format
lint.pycodestyle.max-line-length = 120
lint.pydocstyle.convention = "pep257"
lint.pyupgrade.keep-runtime-typing = false
[tool.pylint.main]
# Maximum number of characters on a single line.

View File

@ -9,6 +9,8 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
Requires Python3.6+.
"""
from __future__ import annotations
from pathlib import Path
import re
import sys

View File

@ -14,6 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
`pytest bot <pytestbot@gmail.com>` commit author.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import re

View File

@ -1,6 +1,8 @@
# mypy: disallow-untyped-defs
"""Invoke development tasks."""
from __future__ import annotations
import argparse
import os
from pathlib import Path

View File

@ -1,4 +1,6 @@
# mypy: disallow-untyped-defs
from __future__ import annotations
import datetime
import pathlib
import re

View File

@ -1,3 +1,6 @@
from __future__ import annotations
__all__ = ["__version__", "version_tuple"]
try:

View File

@ -62,13 +62,13 @@ If things do not work right away:
global argcomplete script).
"""
from __future__ import annotations
import argparse
from glob import glob
import os
import sys
from typing import Any
from typing import List
from typing import Optional
class FastFilesCompleter:
@ -77,7 +77,7 @@ class FastFilesCompleter:
def __init__(self, directories: bool = True) -> None:
self.directories = directories
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
def __call__(self, prefix: str, **kwargs: Any) -> list[str]:
# Only called on non option completions.
if os.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.sep)
@ -104,7 +104,7 @@ if os.environ.get("_ARGCOMPLETE"):
import argcomplete.completers
except ImportError:
sys.exit(-1)
filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter()
filescompleter: FastFilesCompleter | None = FastFilesCompleter()
def try_argcomplete(parser: argparse.ArgumentParser) -> None:
argcomplete.autocomplete(parser, always_complete_options=False)

View File

@ -1,5 +1,7 @@
"""Python inspection/code generation API."""
from __future__ import annotations
from .code import Code
from .code import ExceptionInfo
from .code import filter_traceback

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import ast
import dataclasses
import inspect
@ -17,7 +19,6 @@ from types import TracebackType
from typing import Any
from typing import Callable
from typing import ClassVar
from typing import Dict
from typing import Final
from typing import final
from typing import Generic
@ -25,11 +26,9 @@ from typing import Iterable
from typing import List
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import overload
from typing import Pattern
from typing import Sequence
from typing import Set
from typing import SupportsIndex
from typing import Tuple
from typing import Type
@ -57,6 +56,8 @@ if sys.version_info < (3, 11):
TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]]
class Code:
"""Wrapper around Python code objects."""
@ -67,7 +68,7 @@ class Code:
self.raw = obj
@classmethod
def from_function(cls, obj: object) -> "Code":
def from_function(cls, obj: object) -> Code:
return cls(getrawcode(obj))
def __eq__(self, other):
@ -85,7 +86,7 @@ class Code:
return self.raw.co_name
@property
def path(self) -> Union[Path, str]:
def path(self) -> Path | str:
"""Return a path object pointing to source code, or an ``str`` in
case of ``OSError`` / non-existing file."""
if not self.raw.co_filename:
@ -102,17 +103,17 @@ class Code:
return self.raw.co_filename
@property
def fullsource(self) -> Optional["Source"]:
def fullsource(self) -> Source | None:
"""Return a _pytest._code.Source object for the full source file of the code."""
full, _ = findsource(self.raw)
return full
def source(self) -> "Source":
def source(self) -> Source:
"""Return a _pytest._code.Source object for the code object's source only."""
# return source only for that part of code
return Source(self.raw)
def getargs(self, var: bool = False) -> Tuple[str, ...]:
def getargs(self, var: bool = False) -> tuple[str, ...]:
"""Return a tuple with the argument names for the code object.
If 'var' is set True also return the names of the variable and
@ -141,11 +142,11 @@ class Frame:
return self.raw.f_lineno - 1
@property
def f_globals(self) -> Dict[str, Any]:
def f_globals(self) -> dict[str, Any]:
return self.raw.f_globals
@property
def f_locals(self) -> Dict[str, Any]:
def f_locals(self) -> dict[str, Any]:
return self.raw.f_locals
@property
@ -153,7 +154,7 @@ class Frame:
return Code(self.raw.f_code)
@property
def statement(self) -> "Source":
def statement(self) -> Source:
"""Statement this frame is at."""
if self.code.fullsource is None:
return Source("")
@ -197,14 +198,14 @@ class TracebackEntry:
def __init__(
self,
rawentry: TracebackType,
repr_style: Optional['Literal["short", "long"]'] = None,
repr_style: Literal["short", "long"] | None = None,
) -> None:
self._rawentry: Final = rawentry
self._repr_style: Final = repr_style
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
self, repr_style: Literal["short", "long"] | None
) -> TracebackEntry:
return TracebackEntry(self._rawentry, repr_style)
@property
@ -223,19 +224,19 @@ class TracebackEntry:
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
@property
def statement(self) -> "Source":
def statement(self) -> Source:
"""_pytest._code.Source object for the current statement."""
source = self.frame.code.fullsource
assert source is not None
return source.getstatement(self.lineno)
@property
def path(self) -> Union[Path, str]:
def path(self) -> Path | str:
"""Path to the source code."""
return self.frame.code.path
@property
def locals(self) -> Dict[str, Any]:
def locals(self) -> dict[str, Any]:
"""Locals of underlying frame."""
return self.frame.f_locals
@ -243,8 +244,8 @@ class TracebackEntry:
return self.frame.code.firstlineno
def getsource(
self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None
) -> Optional["Source"]:
self, astcache: dict[str | Path, ast.AST] | None = None
) -> Source | None:
"""Return failing source code."""
# we use the passed in astcache to not reparse asttrees
# within exception info printing
@ -270,7 +271,7 @@ class TracebackEntry:
source = property(getsource)
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
"""Return True if the current frame has a var __tracebackhide__
resolving to True.
@ -279,9 +280,7 @@ class TracebackEntry:
Mostly for internal use.
"""
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
False
)
tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
# in normal cases, f_locals and f_globals are dictionaries
# however via `exec(...)` / `eval(...)` they can be other types
@ -326,13 +325,13 @@ class Traceback(List[TracebackEntry]):
def __init__(
self,
tb: Union[TracebackType, Iterable[TracebackEntry]],
tb: TracebackType | Iterable[TracebackEntry],
) -> None:
"""Initialize from given python traceback object and ExceptionInfo."""
if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur
cur_: TracebackType | None = cur
while cur_ is not None:
yield TracebackEntry(cur_)
cur_ = cur_.tb_next
@ -343,11 +342,11 @@ class Traceback(List[TracebackEntry]):
def cut(
self,
path: Optional[Union["os.PathLike[str]", str]] = None,
lineno: Optional[int] = None,
firstlineno: Optional[int] = None,
excludepath: Optional["os.PathLike[str]"] = None,
) -> "Traceback":
path: os.PathLike[str] | str | None = None,
lineno: int | None = None,
firstlineno: int | None = None,
excludepath: os.PathLike[str] | None = None,
) -> Traceback:
"""Return a Traceback instance wrapping part of this Traceback.
By providing any combination of path, lineno and firstlineno, the
@ -378,14 +377,12 @@ class Traceback(List[TracebackEntry]):
return self
@overload
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ...
def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
@overload
def __getitem__(self, key: slice) -> "Traceback": ...
def __getitem__(self, key: slice) -> Traceback: ...
def __getitem__(
self, key: Union["SupportsIndex", slice]
) -> Union[TracebackEntry, "Traceback"]:
def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
if isinstance(key, slice):
return self.__class__(super().__getitem__(key))
else:
@ -393,12 +390,9 @@ class Traceback(List[TracebackEntry]):
def filter(
self,
excinfo_or_fn: Union[
"ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool],
],
excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
/,
) -> "Traceback":
) -> Traceback:
"""Return a Traceback instance with certain items removed.
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
@ -414,10 +408,10 @@ class Traceback(List[TracebackEntry]):
fn = excinfo_or_fn
return Traceback(filter(fn, self))
def recursionindex(self) -> Optional[int]:
def recursionindex(self) -> int | None:
"""Return the index of the frame/TracebackEntry where recursion originates if
appropriate, None if no recursion occurred."""
cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {}
cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
for i, entry in enumerate(self):
# id for the code.raw is needed to work around
# the strange metaprogramming in the decorator lib from pypi
@ -445,15 +439,15 @@ class ExceptionInfo(Generic[E]):
_assert_start_repr: ClassVar = "AssertionError('assert "
_excinfo: Optional[Tuple[Type["E"], "E", TracebackType]]
_excinfo: tuple[type[E], E, TracebackType] | None
_striptext: str
_traceback: Optional[Traceback]
_traceback: Traceback | None
def __init__(
self,
excinfo: Optional[Tuple[Type["E"], "E", TracebackType]],
excinfo: tuple[type[E], E, TracebackType] | None,
striptext: str = "",
traceback: Optional[Traceback] = None,
traceback: Traceback | None = None,
*,
_ispytest: bool = False,
) -> None:
@ -469,8 +463,8 @@ class ExceptionInfo(Generic[E]):
# This is OK to ignore because this class is (conceptually) readonly.
# See https://github.com/python/mypy/issues/7049.
exception: E, # type: ignore[misc]
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
exprinfo: str | None = None,
) -> ExceptionInfo[E]:
"""Return an ExceptionInfo for an existing exception.
The exception must have a non-``None`` ``__traceback__`` attribute,
@ -495,9 +489,9 @@ class ExceptionInfo(Generic[E]):
@classmethod
def from_exc_info(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
exc_info: tuple[type[E], E, TracebackType],
exprinfo: str | None = None,
) -> ExceptionInfo[E]:
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
_striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError):
@ -510,9 +504,7 @@ class ExceptionInfo(Generic[E]):
return cls(exc_info, _striptext, _ispytest=True)
@classmethod
def from_current(
cls, exprinfo: Optional[str] = None
) -> "ExceptionInfo[BaseException]":
def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
"""Return an ExceptionInfo matching the current traceback.
.. warning::
@ -532,17 +524,17 @@ class ExceptionInfo(Generic[E]):
return ExceptionInfo.from_exc_info(exc_info, exprinfo)
@classmethod
def for_later(cls) -> "ExceptionInfo[E]":
def for_later(cls) -> ExceptionInfo[E]:
"""Return an unfilled ExceptionInfo."""
return cls(None, _ispytest=True)
def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None:
def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
"""Fill an unfilled ExceptionInfo created with ``for_later()``."""
assert self._excinfo is None, "ExceptionInfo was already filled"
self._excinfo = exc_info
@property
def type(self) -> Type[E]:
def type(self) -> type[E]:
"""The exception class."""
assert (
self._excinfo is not None
@ -605,16 +597,14 @@ class ExceptionInfo(Generic[E]):
text = text[len(self._striptext) :]
return text
def errisinstance(
self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]]
) -> bool:
def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
"""Return True if the exception is an instance of exc.
Consider using ``isinstance(excinfo.value, exc)`` instead.
"""
return isinstance(self.value, exc)
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
def _getreprcrash(self) -> ReprFileLocation | None:
# Find last non-hidden traceback entry that led to the exception of the
# traceback, or None if all hidden.
for i in range(-1, -len(self.traceback) - 1, -1):
@ -630,14 +620,13 @@ class ExceptionInfo(Generic[E]):
showlocals: bool = False,
style: TracebackStyle = "long",
abspath: bool = False,
tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
tbfilter: bool
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
funcargs: bool = False,
truncate_locals: bool = True,
truncate_args: bool = True,
chain: bool = True,
) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]:
) -> ReprExceptionInfo | ExceptionChainRepr:
"""Return str()able representation of this exception info.
:param bool showlocals:
@ -719,7 +708,7 @@ class ExceptionInfo(Generic[E]):
]
)
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
def match(self, regexp: str | Pattern[str]) -> Literal[True]:
"""Check whether the regular expression `regexp` matches the string
representation of the exception using :func:`python:re.search`.
@ -737,9 +726,9 @@ class ExceptionInfo(Generic[E]):
def _group_contains(
self,
exc_group: BaseExceptionGroup[BaseException],
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
match: Union[str, Pattern[str], None],
target_depth: Optional[int] = None,
expected_exception: EXCEPTION_OR_MORE,
match: str | Pattern[str] | None,
target_depth: int | None = None,
current_depth: int = 1,
) -> bool:
"""Return `True` if a `BaseExceptionGroup` contains a matching exception."""
@ -766,10 +755,10 @@ class ExceptionInfo(Generic[E]):
def group_contains(
self,
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
expected_exception: EXCEPTION_OR_MORE,
*,
match: Union[str, Pattern[str], None] = None,
depth: Optional[int] = None,
match: str | Pattern[str] | None = None,
depth: int | None = None,
) -> bool:
"""Check whether a captured exception group contains a matching exception.
@ -811,16 +800,16 @@ class FormattedExcinfo:
showlocals: bool = False
style: TracebackStyle = "long"
abspath: bool = True
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
funcargs: bool = False
truncate_locals: bool = True
truncate_args: bool = True
chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
astcache: dict[str | Path, ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False
)
def _getindent(self, source: "Source") -> int:
def _getindent(self, source: Source) -> int:
# Figure out indent for the given source.
try:
s = str(source.getstatement(len(source) - 1))
@ -835,13 +824,13 @@ class FormattedExcinfo:
return 0
return 4 + (len(s) - len(s.lstrip()))
def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]:
def _getentrysource(self, entry: TracebackEntry) -> Source | None:
source = entry.getsource(self.astcache)
if source is not None:
source = source.deindent()
return source
def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]:
def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
if self.funcargs:
args = []
for argname, argvalue in entry.frame.getargs(var=True):
@ -855,11 +844,11 @@ class FormattedExcinfo:
def get_source(
self,
source: Optional["Source"],
source: Source | None,
line_index: int = -1,
excinfo: Optional[ExceptionInfo[BaseException]] = None,
excinfo: ExceptionInfo[BaseException] | None = None,
short: bool = False,
) -> List[str]:
) -> list[str]:
"""Return formatted and marked up source lines."""
lines = []
if source is not None and line_index < 0:
@ -888,7 +877,7 @@ class FormattedExcinfo:
excinfo: ExceptionInfo[BaseException],
indent: int = 4,
markall: bool = False,
) -> List[str]:
) -> list[str]:
lines = []
indentstr = " " * indent
# Get the real exception information out.
@ -900,7 +889,7 @@ class FormattedExcinfo:
failindent = indentstr
return lines
def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
if self.showlocals:
lines = []
keys = [loc for loc in locals if loc[0] != "@"]
@ -928,10 +917,10 @@ class FormattedExcinfo:
def repr_traceback_entry(
self,
entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry":
lines: List[str] = []
entry: TracebackEntry | None,
excinfo: ExceptionInfo[BaseException] | None = None,
) -> ReprEntry:
lines: list[str] = []
style = (
entry._repr_style
if entry is not None and entry._repr_style is not None
@ -966,7 +955,7 @@ class FormattedExcinfo:
lines.extend(self.get_exconly(excinfo, indent=4))
return ReprEntry(lines, None, None, None, style)
def _makepath(self, path: Union[Path, str]) -> str:
def _makepath(self, path: Path | str) -> str:
if not self.abspath and isinstance(path, Path):
try:
np = bestrelpath(Path.cwd(), path)
@ -976,7 +965,7 @@ class FormattedExcinfo:
return np
return str(path)
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
traceback = excinfo.traceback
if callable(self.tbfilter):
traceback = self.tbfilter(excinfo)
@ -1007,7 +996,7 @@ class FormattedExcinfo:
def _truncate_recursive_traceback(
self, traceback: Traceback
) -> Tuple[Traceback, Optional[str]]:
) -> tuple[Traceback, str | None]:
"""Truncate the given recursive traceback trying to find the starting
point of the recursion.
@ -1024,7 +1013,7 @@ class FormattedExcinfo:
recursionindex = traceback.recursionindex()
except Exception as e:
max_frames = 10
extraline: Optional[str] = (
extraline: str | None = (
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
" The following exception happened when comparing locals in the stack frame:\n"
f" {type(e).__name__}: {e!s}\n"
@ -1042,16 +1031,12 @@ class FormattedExcinfo:
return traceback, extraline
def repr_excinfo(
self, excinfo: ExceptionInfo[BaseException]
) -> "ExceptionChainRepr":
repr_chain: List[
Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]
] = []
e: Optional[BaseException] = excinfo.value
excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo
def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
e: BaseException | None = excinfo.value
excinfo_: ExceptionInfo[BaseException] | None = excinfo
descr = None
seen: Set[int] = set()
seen: set[int] = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
@ -1060,7 +1045,7 @@ class FormattedExcinfo:
# full support for exception groups added to ExceptionInfo.
# See https://github.com/pytest-dev/pytest/issues/9159
if isinstance(e, BaseExceptionGroup):
reprtraceback: Union[ReprTracebackNative, ReprTraceback] = (
reprtraceback: ReprTracebackNative | ReprTraceback = (
ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
@ -1118,9 +1103,9 @@ class TerminalRepr:
@dataclasses.dataclass(eq=False)
class ExceptionRepr(TerminalRepr):
# Provided by subclasses.
reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
sections: List[Tuple[str, str, str]] = dataclasses.field(
reprtraceback: ReprTraceback
reprcrash: ReprFileLocation | None
sections: list[tuple[str, str, str]] = dataclasses.field(
init=False, default_factory=list
)
@ -1135,13 +1120,11 @@ class ExceptionRepr(TerminalRepr):
@dataclasses.dataclass(eq=False)
class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
def __init__(
self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
) -> None:
# reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain.
@ -1162,8 +1145,8 @@ class ExceptionChainRepr(ExceptionRepr):
@dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
reprtraceback: ReprTraceback
reprcrash: ReprFileLocation | None
def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
@ -1172,8 +1155,8 @@ class ReprExceptionInfo(ExceptionRepr):
@dataclasses.dataclass(eq=False)
class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str]
reprentries: Sequence[ReprEntry | ReprEntryNative]
extraline: str | None
style: TracebackStyle
entrysep: ClassVar = "_ "
@ -1217,9 +1200,9 @@ class ReprEntryNative(TerminalRepr):
@dataclasses.dataclass(eq=False)
class ReprEntry(TerminalRepr):
lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"]
reprlocals: Optional["ReprLocals"]
reprfileloc: Optional["ReprFileLocation"]
reprfuncargs: ReprFuncArgs | None
reprlocals: ReprLocals | None
reprfileloc: ReprFileLocation | None
style: TracebackStyle
def _write_entry_lines(self, tw: TerminalWriter) -> None:
@ -1243,9 +1226,9 @@ class ReprEntry(TerminalRepr):
# such as "> assert 0"
fail_marker = f"{FormattedExcinfo.fail_marker} "
indent_size = len(fail_marker)
indents: List[str] = []
source_lines: List[str] = []
failure_lines: List[str] = []
indents: list[str] = []
source_lines: list[str] = []
failure_lines: list[str] = []
for index, line in enumerate(self.lines):
is_failure_line = line.startswith(fail_marker)
if is_failure_line:
@ -1324,7 +1307,7 @@ class ReprLocals(TerminalRepr):
@dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]]
args: Sequence[tuple[str, object]]
def toterminal(self, tw: TerminalWriter) -> None:
if self.args:
@ -1345,7 +1328,7 @@ class ReprFuncArgs(TerminalRepr):
tw.line("")
def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
def getfslineno(obj: object) -> tuple[str | Path, int]:
"""Return source location (path, lineno) for the given object.
If the source cannot be determined return ("", -1).

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import ast
from bisect import bisect_right
import inspect
@ -7,11 +9,7 @@ import tokenize
import types
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Tuple
from typing import Union
import warnings
@ -23,7 +21,7 @@ class Source:
def __init__(self, obj: object = None) -> None:
if not obj:
self.lines: List[str] = []
self.lines: list[str] = []
elif isinstance(obj, Source):
self.lines = obj.lines
elif isinstance(obj, (tuple, list)):
@ -50,9 +48,9 @@ class Source:
def __getitem__(self, key: int) -> str: ...
@overload
def __getitem__(self, key: slice) -> "Source": ...
def __getitem__(self, key: slice) -> Source: ...
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
def __getitem__(self, key: int | slice) -> str | Source:
if isinstance(key, int):
return self.lines[key]
else:
@ -68,7 +66,7 @@ class Source:
def __len__(self) -> int:
return len(self.lines)
def strip(self) -> "Source":
def strip(self) -> Source:
"""Return new Source object with trailing and leading blank lines removed."""
start, end = 0, len(self)
while start < end and not self.lines[start].strip():
@ -79,20 +77,20 @@ class Source:
source.lines[:] = self.lines[start:end]
return source
def indent(self, indent: str = " " * 4) -> "Source":
def indent(self, indent: str = " " * 4) -> Source:
"""Return a copy of the source object with all lines indented by the
given indent-string."""
newsource = Source()
newsource.lines = [(indent + line) for line in self.lines]
return newsource
def getstatement(self, lineno: int) -> "Source":
def getstatement(self, lineno: int) -> Source:
"""Return Source statement which contains the given linenumber
(counted from 0)."""
start, end = self.getstatementrange(lineno)
return self[start:end]
def getstatementrange(self, lineno: int) -> Tuple[int, int]:
def getstatementrange(self, lineno: int) -> tuple[int, int]:
"""Return (start, end) tuple which spans the minimal statement region
which containing the given lineno."""
if not (0 <= lineno < len(self)):
@ -100,7 +98,7 @@ class Source:
ast, start, end = getstatementrange_ast(lineno, self)
return start, end
def deindent(self) -> "Source":
def deindent(self) -> Source:
"""Return a new Source object deindented."""
newsource = Source()
newsource.lines[:] = deindent(self.lines)
@ -115,7 +113,7 @@ class Source:
#
def findsource(obj) -> Tuple[Optional[Source], int]:
def findsource(obj) -> tuple[Source | None, int]:
try:
sourcelines, lineno = inspect.findsource(obj)
except Exception:
@ -138,14 +136,14 @@ def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
raise TypeError(f"could not get code object for {obj!r}")
def deindent(lines: Iterable[str]) -> List[str]:
def deindent(lines: Iterable[str]) -> list[str]:
return textwrap.dedent("\n".join(lines)).splitlines()
def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]:
def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]:
# Flatten all statements and except handlers into one lineno-list.
# AST's line numbers start indexing at 1.
values: List[int] = []
values: list[int] = []
for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
# The lineno points to the class/def, so need to include the decorators.
@ -154,7 +152,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
values.append(d.lineno - 1)
values.append(x.lineno - 1)
for name in ("finalbody", "orelse"):
val: Optional[List[ast.stmt]] = getattr(x, name, None)
val: list[ast.stmt] | None = getattr(x, name, None)
if val:
# Treat the finally/orelse part as its own statement.
values.append(val[0].lineno - 1 - 1)
@ -172,8 +170,8 @@ def getstatementrange_ast(
lineno: int,
source: Source,
assertion: bool = False,
astnode: Optional[ast.AST] = None,
) -> Tuple[ast.AST, int, int]:
astnode: ast.AST | None = None,
) -> tuple[ast.AST, int, int]:
if astnode is None:
content = str(source)
# See #4260:

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from .terminalwriter import get_terminal_width
from .terminalwriter import TerminalWriter

View File

@ -13,6 +13,8 @@
# tuples with fairly non-descriptive content. This is modeled very much
# after Lisp/Scheme - style pretty-printing of lists. If you find it
# useful, thank small children who sleep at night.
from __future__ import annotations
import collections as _collections
import dataclasses as _dataclasses
from io import StringIO as _StringIO
@ -20,13 +22,8 @@ import re
import types as _types
from typing import Any
from typing import Callable
from typing import Dict
from typing import IO
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
class _safe_key:
@ -64,7 +61,7 @@ class PrettyPrinter:
self,
indent: int = 4,
width: int = 80,
depth: Optional[int] = None,
depth: int | None = None,
) -> None:
"""Handle pretty printing operations onto a stream using a set of
configured parameters.
@ -100,7 +97,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
objid = id(object)
@ -136,7 +133,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
cls_name = object.__class__.__name__
@ -149,9 +146,9 @@ class PrettyPrinter:
self._format_namespace_items(items, stream, indent, allowance, context, level)
stream.write(")")
_dispatch: Dict[
_dispatch: dict[
Callable[..., str],
Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None],
Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None],
] = {}
def _pprint_dict(
@ -160,7 +157,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
write = stream.write
@ -177,7 +174,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not len(object):
@ -196,7 +193,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
stream.write("[")
@ -211,7 +208,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
stream.write("(")
@ -226,7 +223,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not len(object):
@ -252,7 +249,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
write = stream.write
@ -311,7 +308,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
write = stream.write
@ -340,7 +337,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
write = stream.write
@ -358,7 +355,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
stream.write("mappingproxy(")
@ -373,7 +370,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if type(object) is _types.SimpleNamespace:
@ -391,11 +388,11 @@ class PrettyPrinter:
def _format_dict_items(
self,
items: List[Tuple[Any, Any]],
items: list[tuple[Any, Any]],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not items:
@ -415,11 +412,11 @@ class PrettyPrinter:
def _format_namespace_items(
self,
items: List[Tuple[Any, Any]],
items: list[tuple[Any, Any]],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not items:
@ -452,11 +449,11 @@ class PrettyPrinter:
def _format_items(
self,
items: List[Any],
items: list[Any],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not items:
@ -473,7 +470,7 @@ class PrettyPrinter:
write("\n" + " " * indent)
def _repr(self, object: Any, context: Set[int], level: int) -> str:
def _repr(self, object: Any, context: set[int], level: int) -> str:
return self._safe_repr(object, context.copy(), self._depth, level)
def _pprint_default_dict(
@ -482,7 +479,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
rdf = self._repr(object.default_factory, context, level)
@ -498,7 +495,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
stream.write(object.__class__.__name__ + "(")
@ -519,7 +516,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
@ -538,7 +535,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
stream.write(object.__class__.__name__ + "(")
@ -557,7 +554,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
@ -570,7 +567,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
@ -583,7 +580,7 @@ class PrettyPrinter:
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
context: set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
@ -591,7 +588,7 @@ class PrettyPrinter:
_dispatch[_collections.UserString.__repr__] = _pprint_user_string
def _safe_repr(
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
self, object: Any, context: set[int], maxlevels: int | None, level: int
) -> str:
typ = type(object)
if typ in _builtin_scalars:
@ -608,7 +605,7 @@ class PrettyPrinter:
if objid in context:
return _recursion(object)
context.add(objid)
components: List[str] = []
components: list[str] = []
append = components.append
level += 1
for k, v in sorted(object.items(), key=_safe_tuple):

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import pprint
import reprlib
from typing import Optional
def _try_repr_or_str(obj: object) -> str:
@ -38,7 +39,7 @@ class SafeRepr(reprlib.Repr):
information on exceptions raised during the call.
"""
def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
"""
:param maxsize:
If not None, will truncate the resulting repr to that specific size, using ellipsis
@ -97,7 +98,7 @@ DEFAULT_REPR_MAX_SIZE = 240
def saferepr(
obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
) -> str:
"""Return a size-limited safe repr-string for the given object.

View File

@ -1,11 +1,12 @@
"""Helper functions for writing to terminals and files."""
from __future__ import annotations
import os
import shutil
import sys
from typing import final
from typing import Literal
from typing import Optional
from typing import Sequence
from typing import TextIO
from typing import TYPE_CHECKING
@ -71,7 +72,7 @@ class TerminalWriter:
invert=7,
)
def __init__(self, file: Optional[TextIO] = None) -> None:
def __init__(self, file: TextIO | None = None) -> None:
if file is None:
file = sys.stdout
if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
@ -85,7 +86,7 @@ class TerminalWriter:
self._file = file
self.hasmarkup = should_do_markup(file)
self._current_line = ""
self._terminal_width: Optional[int] = None
self._terminal_width: int | None = None
self.code_highlight = True
@property
@ -116,8 +117,8 @@ class TerminalWriter:
def sep(
self,
sepchar: str,
title: Optional[str] = None,
fullwidth: Optional[int] = None,
title: str | None = None,
fullwidth: int | None = None,
**markup: bool,
) -> None:
if fullwidth is None:
@ -200,9 +201,7 @@ class TerminalWriter:
for indent, new_line in zip(indents, new_lines):
self.line(indent + new_line)
def _get_pygments_lexer(
self, lexer: Literal["python", "diff"]
) -> Optional["Lexer"]:
def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None:
try:
if lexer == "python":
from pygments.lexers.python import PythonLexer
@ -217,7 +216,7 @@ class TerminalWriter:
except ModuleNotFoundError:
return None
def _get_pygments_formatter(self) -> Optional["Formatter"]:
def _get_pygments_formatter(self) -> Formatter | None:
try:
import pygments.util
except ModuleNotFoundError:

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from functools import lru_cache
import unicodedata

View File

@ -1,11 +1,11 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions."""
from __future__ import annotations
import sys
from typing import Any
from typing import Generator
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from _pytest.assertion import rewrite
@ -94,7 +94,7 @@ class AssertionState:
def __init__(self, config: Config, mode) -> None:
self.mode = mode
self.trace = config.trace.root.get("assertion")
self.hook: Optional[rewrite.AssertionRewritingHook] = None
self.hook: rewrite.AssertionRewritingHook | None = None
def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
@ -113,7 +113,7 @@ def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
return hook
def pytest_collection(session: "Session") -> None:
def pytest_collection(session: Session) -> None:
# This hook is only called when test modules are collected
# so for example not in the managing process of pytest-xdist
# (which does not collect test modules).
@ -133,7 +133,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:
def callbinrepr(op, left: object, right: object) -> str | None:
"""Call the pytest_assertrepr_compare hook and prepare the result.
This uses the first result from the hook and then ensures the
@ -179,7 +179,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
util._config = None
def pytest_sessionfinish(session: "Session") -> None:
def pytest_sessionfinish(session: Session) -> None:
assertstate = session.config.stash.get(assertstate_key, None)
if assertstate:
if assertstate.hook is not None:
@ -188,5 +188,5 @@ def pytest_sessionfinish(session: "Session") -> None:
def pytest_assertrepr_compare(
config: Config, op: str, left: Any, right: Any
) -> Optional[List[str]]:
) -> list[str] | None:
return util.assertrepr_compare(config=config, op=op, left=left, right=right)

View File

@ -1,5 +1,7 @@
"""Rewrite assertion AST to produce nice error messages."""
from __future__ import annotations
import ast
from collections import defaultdict
import errno
@ -18,17 +20,11 @@ import sys
import tokenize
import types
from typing import Callable
from typing import Dict
from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
@ -73,17 +69,17 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
self.fnpats = config.getini("python_files")
except ValueError:
self.fnpats = ["test_*.py", "*_test.py"]
self.session: Optional[Session] = None
self._rewritten_names: Dict[str, Path] = {}
self._must_rewrite: Set[str] = set()
self.session: Session | None = None
self._rewritten_names: dict[str, Path] = {}
self._must_rewrite: set[str] = set()
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
# which might result in infinite recursion (#3506)
self._writing_pyc = False
self._basenames_to_check_rewrite = {"conftest"}
self._marked_for_rewrite_cache: Dict[str, bool] = {}
self._marked_for_rewrite_cache: dict[str, bool] = {}
self._session_paths_checked = False
def set_session(self, session: Optional[Session]) -> None:
def set_session(self, session: Session | None) -> None:
self.session = session
self._session_paths_checked = False
@ -93,9 +89,9 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
def find_spec(
self,
name: str,
path: Optional[Sequence[Union[str, bytes]]] = None,
target: Optional[types.ModuleType] = None,
) -> Optional[importlib.machinery.ModuleSpec]:
path: Sequence[str | bytes] | None = None,
target: types.ModuleType | None = None,
) -> importlib.machinery.ModuleSpec | None:
if self._writing_pyc:
return None
state = self.config.stash[assertstate_key]
@ -132,7 +128,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
def create_module(
self, spec: importlib.machinery.ModuleSpec
) -> Optional[types.ModuleType]:
) -> types.ModuleType | None:
return None # default behaviour is fine
def exec_module(self, module: types.ModuleType) -> None:
@ -177,7 +173,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
state.trace(f"found cached rewritten pyc for {fn}")
exec(co, module.__dict__)
def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool:
def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
"""A fast way to get out of rewriting modules.
Profiling has shown that the call to PathFinder.find_spec (inside of
@ -216,7 +212,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
state.trace(f"early skip of rewriting module: {name}")
return True
def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool:
def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
# always rewrite conftest files
if os.path.basename(fn) == "conftest.py":
state.trace(f"rewriting conftest file: {fn!r}")
@ -237,7 +233,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return self._is_marked_for_rewrite(name, state)
def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool:
def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
try:
return self._marked_for_rewrite_cache[name]
except KeyError:
@ -278,7 +274,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
stacklevel=5,
)
def get_data(self, pathname: Union[str, bytes]) -> bytes:
def get_data(self, pathname: str | bytes) -> bytes:
"""Optional PEP302 get_data API."""
with open(pathname, "rb") as f:
return f.read()
@ -317,7 +313,7 @@ def _write_pyc_fp(
def _write_pyc(
state: "AssertionState",
state: AssertionState,
co: types.CodeType,
source_stat: os.stat_result,
pyc: Path,
@ -341,7 +337,7 @@ def _write_pyc(
return True
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
"""Read and rewrite *fn* and return the code object."""
stat = os.stat(fn)
source = fn.read_bytes()
@ -354,7 +350,7 @@ def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeT
def _read_pyc(
source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
) -> Optional[types.CodeType]:
) -> types.CodeType | None:
"""Possibly read a pytest pyc containing rewritten code.
Return rewritten code if successful or None if not.
@ -404,8 +400,8 @@ def _read_pyc(
def rewrite_asserts(
mod: ast.Module,
source: bytes,
module_path: Optional[str] = None,
config: Optional[Config] = None,
module_path: str | None = None,
config: Config | None = None,
) -> None:
"""Rewrite the assert statements in mod."""
AssertionRewriter(module_path, config, source).run(mod)
@ -425,7 +421,7 @@ def _saferepr(obj: object) -> str:
return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
"""Get `maxsize` configuration for saferepr based on the given config object."""
if config is None:
verbosity = 0
@ -543,14 +539,14 @@ def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
@functools.lru_cache(maxsize=1)
def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
def _get_assertion_exprs(src: bytes) -> dict[int, str]:
"""Return a mapping from {lineno: "assertion test expression"}."""
ret: Dict[int, str] = {}
ret: dict[int, str] = {}
depth = 0
lines: List[str] = []
assert_lineno: Optional[int] = None
seen_lines: Set[int] = set()
lines: list[str] = []
assert_lineno: int | None = None
seen_lines: set[int] = set()
def _write_and_reset() -> None:
nonlocal depth, lines, assert_lineno, seen_lines
@ -657,7 +653,7 @@ class AssertionRewriter(ast.NodeVisitor):
"""
def __init__(
self, module_path: Optional[str], config: Optional[Config], source: bytes
self, module_path: str | None, config: Config | None, source: bytes
) -> None:
super().__init__()
self.module_path = module_path
@ -670,7 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.enable_assertion_pass_hook = False
self.source = source
self.scope: tuple[ast.AST, ...] = ()
self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = (
self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
defaultdict(dict)
)
@ -737,7 +733,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Collect asserts.
self.scope = (mod,)
nodes: List[Union[ast.AST, Sentinel]] = [mod]
nodes: list[ast.AST | Sentinel] = [mod]
while nodes:
node = nodes.pop()
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
@ -749,7 +745,7 @@ class AssertionRewriter(ast.NodeVisitor):
assert isinstance(node, ast.AST)
for name, field in ast.iter_fields(node):
if isinstance(field, list):
new: List[ast.AST] = []
new: list[ast.AST] = []
for i, child in enumerate(field):
if isinstance(child, ast.Assert):
# Transform assert.
@ -821,7 +817,7 @@ class AssertionRewriter(ast.NodeVisitor):
to format a string of %-formatted values as added by
.explanation_param().
"""
self.explanation_specifiers: Dict[str, ast.expr] = {}
self.explanation_specifiers: dict[str, ast.expr] = {}
self.stack.append(self.explanation_specifiers)
def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
@ -835,7 +831,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
keys: List[Optional[ast.expr]] = [ast.Constant(key) for key in current.keys()]
keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter))
@ -844,13 +840,13 @@ class AssertionRewriter(ast.NodeVisitor):
self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
return ast.Name(name, ast.Load())
def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]:
def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
"""Handle expressions we don't have custom code for."""
assert isinstance(node, ast.expr)
res = self.assign(node)
return res, self.explanation_param(self.display(res))
def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
"""Return the AST statements to replace the ast.Assert instance.
This rewrites the test of an assertion to provide
@ -874,15 +870,15 @@ class AssertionRewriter(ast.NodeVisitor):
lineno=assert_.lineno,
)
self.statements: List[ast.stmt] = []
self.variables: List[str] = []
self.statements: list[ast.stmt] = []
self.variables: list[str] = []
self.variable_counter = itertools.count()
if self.enable_assertion_pass_hook:
self.format_variables: List[str] = []
self.format_variables: list[str] = []
self.stack: List[Dict[str, ast.expr]] = []
self.expl_stmts: List[ast.stmt] = []
self.stack: list[dict[str, ast.expr]] = []
self.expl_stmts: list[ast.stmt] = []
self.push_format_context()
# Rewrite assert into a bunch of statements.
top_condition, explanation = self.visit(assert_.test)
@ -926,13 +922,13 @@ class AssertionRewriter(ast.NodeVisitor):
[*self.expl_stmts, hook_call_pass],
[],
)
statements_pass: List[ast.stmt] = [hook_impl_test]
statements_pass: list[ast.stmt] = [hook_impl_test]
# Test for assertion condition
main_test = ast.If(negation, statements_fail, statements_pass)
self.statements.append(main_test)
if self.format_variables:
variables: List[ast.expr] = [
variables: list[ast.expr] = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
clear_format = ast.Assign(variables, ast.Constant(None))
@ -968,7 +964,7 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_)
return self.statements
def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]:
def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
# This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable.
@ -980,7 +976,7 @@ class AssertionRewriter(ast.NodeVisitor):
expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
@ -990,7 +986,7 @@ class AssertionRewriter(ast.NodeVisitor):
expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
res_var = self.variable()
expl_list = self.assign(ast.List([], ast.Load()))
app = ast.Attribute(expl_list, "append", ast.Load())
@ -1002,7 +998,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Process each operand, short-circuiting if needed.
for i, v in enumerate(boolop.values):
if i:
fail_inner: List[ast.stmt] = []
fail_inner: list[ast.stmt] = []
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
self.expl_stmts = fail_inner
@ -1030,7 +1026,7 @@ class AssertionRewriter(ast.NodeVisitor):
cond: ast.expr = res
if is_or:
cond = ast.UnaryOp(ast.Not(), cond)
inner: List[ast.stmt] = []
inner: list[ast.stmt] = []
self.statements.append(ast.If(cond, inner, []))
self.statements = body = inner
self.statements = save
@ -1039,13 +1035,13 @@ class AssertionRewriter(ast.NodeVisitor):
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]:
def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
pattern = UNARY_MAP[unary.op.__class__]
operand_res, operand_expl = self.visit(unary.operand)
res = self.assign(ast.UnaryOp(unary.op, operand_res))
return res, pattern % (operand_expl,)
def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]:
def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
symbol = BINOP_MAP[binop.op.__class__]
left_expr, left_expl = self.visit(binop.left)
right_expr, right_expl = self.visit(binop.right)
@ -1053,7 +1049,7 @@ class AssertionRewriter(ast.NodeVisitor):
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
return res, explanation
def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]:
def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
new_func, func_expl = self.visit(call.func)
arg_expls = []
new_args = []
@ -1085,13 +1081,13 @@ class AssertionRewriter(ast.NodeVisitor):
outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
return res, outer_expl
def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]:
def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
# A Starred node can appear in a function call.
res, expl = self.visit(starred.value)
new_starred = ast.Starred(res, starred.ctx)
return new_starred, "*" + expl
def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]:
def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
if not isinstance(attr.ctx, ast.Load):
return self.generic_visit(attr)
value, value_expl = self.visit(attr.value)
@ -1101,7 +1097,7 @@ class AssertionRewriter(ast.NodeVisitor):
expl = pat % (res_expl, res_expl, value_expl, attr.attr)
return res, expl
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
self.push_format_context()
# We first check if we have overwritten a variable in the previous assert
if isinstance(
@ -1114,11 +1110,11 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
res_variables = [self.variable() for i in range(len(comp.ops))]
load_names: List[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
expls: List[ast.expr] = []
syms: List[ast.expr] = []
expls: list[ast.expr] = []
syms: list[ast.expr] = []
results = [left_res]
for i, op, next_operand in it:
if (

View File

@ -4,8 +4,7 @@ Current default behaviour is to truncate assertion explanations at
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
"""
from typing import List
from typing import Optional
from __future__ import annotations
from _pytest.assertion import util
from _pytest.config import Config
@ -18,8 +17,8 @@ USAGE_MSG = "use '-vv' to show"
def truncate_if_required(
explanation: List[str], item: Item, max_length: Optional[int] = None
) -> List[str]:
explanation: list[str], item: Item, max_length: int | None = None
) -> list[str]:
"""Truncate this assertion explanation if the given test item is eligible."""
if _should_truncate_item(item):
return _truncate_explanation(explanation)
@ -33,10 +32,10 @@ def _should_truncate_item(item: Item) -> bool:
def _truncate_explanation(
input_lines: List[str],
max_lines: Optional[int] = None,
max_chars: Optional[int] = None,
) -> List[str]:
input_lines: list[str],
max_lines: int | None = None,
max_chars: int | None = None,
) -> list[str]:
"""Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
@ -100,7 +99,7 @@ def _truncate_explanation(
]
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
from __future__ import annotations
import collections.abc
import os
import pprint
@ -8,10 +10,8 @@ from typing import AbstractSet
from typing import Any
from typing import Callable
from typing import Iterable
from typing import List
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Protocol
from typing import Sequence
from unicodedata import normalize
@ -28,14 +28,14 @@ from _pytest.config import Config
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
# DebugInterpreter.
_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None
_reprcompare: Callable[[str, object, object], str | None] | None = None
# Works similarly as _reprcompare attribute. Is populated with the hook call
# when pytest_runtest_setup is called.
_assertion_pass: Optional[Callable[[int, str, str], None]] = None
_assertion_pass: Callable[[int, str, str], None] | None = None
# Config object which is assigned during pytest_runtest_protocol.
_config: Optional[Config] = None
_config: Config | None = None
class _HighlightFunc(Protocol):
@ -58,7 +58,7 @@ def format_explanation(explanation: str) -> str:
return "\n".join(result)
def _split_explanation(explanation: str) -> List[str]:
def _split_explanation(explanation: str) -> list[str]:
r"""Return a list of individual lines in the explanation.
This will return a list of lines split on '\n{', '\n}' and '\n~'.
@ -75,7 +75,7 @@ def _split_explanation(explanation: str) -> List[str]:
return lines
def _format_lines(lines: Sequence[str]) -> List[str]:
def _format_lines(lines: Sequence[str]) -> list[str]:
"""Format the individual lines.
This will replace the '{', '}' and '~' characters of our mini formatting
@ -169,7 +169,7 @@ def has_default_eq(
def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
) -> list[str] | None:
"""Return specialised explanations for some operators/operands."""
verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
@ -239,7 +239,7 @@ def assertrepr_compare(
def _compare_eq_any(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
) -> List[str]:
) -> list[str]:
explanation = []
if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose)
@ -274,7 +274,7 @@ def _compare_eq_any(
return explanation
def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]:
"""Return the explanation for the diff between text.
Unless --verbose is used this will skip leading and trailing
@ -282,7 +282,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
"""
from difflib import ndiff
explanation: List[str] = []
explanation: list[str] = []
if verbose < 1:
i = 0 # just in case left or right has zero length
@ -327,7 +327,7 @@ def _compare_eq_iterable(
right: Iterable[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
if verbose <= 0 and not running_on_ci():
return ["Use -v to get more diff"]
# dynamic import to speedup pytest
@ -356,9 +356,9 @@ def _compare_eq_sequence(
right: Sequence[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
explanation: List[str] = []
explanation: list[str] = []
len_left = len(left)
len_right = len(right)
for i in range(min(len_left, len_right)):
@ -417,7 +417,7 @@ def _compare_eq_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = []
explanation.extend(_set_one_sided_diff("left", left, right, highlighter))
explanation.extend(_set_one_sided_diff("right", right, left, highlighter))
@ -429,7 +429,7 @@ def _compare_gt_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = _compare_gte_set(left, right, highlighter)
if not explanation:
return ["Both sets are equal"]
@ -441,7 +441,7 @@ def _compare_lt_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
explanation = _compare_lte_set(left, right, highlighter)
if not explanation:
return ["Both sets are equal"]
@ -453,7 +453,7 @@ def _compare_gte_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
return _set_one_sided_diff("right", right, left, highlighter)
@ -462,7 +462,7 @@ def _compare_lte_set(
right: AbstractSet[Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
) -> list[str]:
return _set_one_sided_diff("left", left, right, highlighter)
@ -471,7 +471,7 @@ def _set_one_sided_diff(
set1: AbstractSet[Any],
set2: AbstractSet[Any],
highlighter: _HighlightFunc,
) -> List[str]:
) -> list[str]:
explanation = []
diff = set1 - set2
if diff:
@ -486,8 +486,8 @@ def _compare_eq_dict(
right: Mapping[Any, Any],
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
explanation: List[str] = []
) -> list[str]:
explanation: list[str] = []
set_left = set(left)
set_right = set(right)
common = set_left.intersection(set_right)
@ -531,7 +531,7 @@ def _compare_eq_dict(
def _compare_eq_cls(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
) -> List[str]:
) -> list[str]:
if not has_default_eq(left):
return []
if isdatacls(left):
@ -584,7 +584,7 @@ def _compare_eq_cls(
return explanation
def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]:
def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]:
index = text.find(term)
head = text[:index]
tail = text[index + len(term) :]

View File

@ -3,20 +3,17 @@
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
from __future__ import annotations
import dataclasses
import errno
import json
import os
from pathlib import Path
import tempfile
from typing import Dict
from typing import final
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Union
from .pathlib import resolve_from_str
from .pathlib import rm_rf
@ -77,7 +74,7 @@ class Cache:
self._config = config
@classmethod
def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache:
"""Create the Cache instance for a Config.
:meta private:
@ -249,7 +246,7 @@ class Cache:
class LFPluginCollWrapper:
def __init__(self, lfplugin: "LFPlugin") -> None:
def __init__(self, lfplugin: LFPlugin) -> None:
self.lfplugin = lfplugin
self._collected_at_least_one_failure = False
@ -263,7 +260,7 @@ class LFPluginCollWrapper:
lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to prioritize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
def sort_key(node: nodes.Item | nodes.Collector) -> bool:
return node.path in lf_paths
res.result = sorted(
@ -301,13 +298,13 @@ class LFPluginCollWrapper:
class LFPluginCollSkipfiles:
def __init__(self, lfplugin: "LFPlugin") -> None:
def __init__(self, lfplugin: LFPlugin) -> None:
self.lfplugin = lfplugin
@hookimpl
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Optional[CollectReport]:
) -> CollectReport | None:
if isinstance(collector, File):
if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1
@ -326,9 +323,9 @@ class LFPlugin:
active_keys = "lf", "failedfirst"
self.active = any(config.getoption(key) for key in active_keys)
assert config.cache
self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {})
self._previously_failed_count: Optional[int] = None
self._report_status: Optional[str] = None
self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {})
self._previously_failed_count: int | None = None
self._report_status: str | None = None
self._skipped_files = 0 # count skipped files during collection due to --lf
if config.getoption("lf"):
@ -337,7 +334,7 @@ class LFPlugin:
LFPluginCollWrapper(self), "lfplugin-collwrapper"
)
def get_last_failed_paths(self) -> Set[Path]:
def get_last_failed_paths(self) -> set[Path]:
"""Return a set with all Paths of the previously failed nodeids and
their parents."""
rootpath = self.config.rootpath
@ -348,7 +345,7 @@ class LFPlugin:
result.update(path.parents)
return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]:
def pytest_report_collectionfinish(self) -> str | None:
if self.active and self.config.getoption("verbose") >= 0:
return f"run-last-failure: {self._report_status}"
return None
@ -370,7 +367,7 @@ class LFPlugin:
@hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item]
self, config: Config, items: list[nodes.Item]
) -> Generator[None, None, None]:
res = yield
@ -442,13 +439,13 @@ class NFPlugin:
@hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems(
self, items: List[nodes.Item]
self, items: list[nodes.Item]
) -> Generator[None, None, None]:
res = yield
if self.active:
new_items: Dict[str, nodes.Item] = {}
other_items: Dict[str, nodes.Item] = {}
new_items: dict[str, nodes.Item] = {}
other_items: dict[str, nodes.Item] = {}
for item in items:
if item.nodeid not in self.cached_nodeids:
new_items[item.nodeid] = item
@ -464,7 +461,7 @@ class NFPlugin:
return res
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]:
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
def pytest_sessionfinish(self) -> None:
@ -541,7 +538,7 @@ def pytest_addoption(parser: Parser) -> None:
)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.cacheshow and not config.option.help:
from _pytest.main import wrap_session
@ -572,7 +569,7 @@ def cache(request: FixtureRequest) -> Cache:
return request.config.cache
def pytest_report_header(config: Config) -> Optional[str]:
def pytest_report_header(config: Config) -> str | None:
"""Display cachedir with --cache-show and if non-default."""
if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
assert config.cache is not None

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Per-test stdout/stderr capturing mechanism."""
from __future__ import annotations
import abc
import collections
import contextlib
@ -19,15 +21,14 @@ from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Literal
from typing import NamedTuple
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
if TYPE_CHECKING:
from typing_extensions import Self
from _pytest.config import Config
from _pytest.config import hookimpl
@ -213,7 +214,7 @@ class DontReadFromInput(TextIO):
def __next__(self) -> str:
return self.readline()
def readlines(self, hint: Optional[int] = -1) -> List[str]:
def readlines(self, hint: int | None = -1) -> list[str]:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
@ -245,7 +246,7 @@ class DontReadFromInput(TextIO):
def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int:
def truncate(self, size: int | None = None) -> int:
raise UnsupportedOperation("cannot truncate stdin")
def write(self, data: str) -> int:
@ -257,14 +258,14 @@ class DontReadFromInput(TextIO):
def writable(self) -> bool:
return False
def __enter__(self) -> "DontReadFromInput":
def __enter__(self) -> Self:
return self
def __exit__(
self,
type: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
type: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> None:
pass
@ -339,7 +340,7 @@ class NoCapture(CaptureBase[str]):
class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
) -> None:
name = patchsysdict[fd]
self._old: TextIO = getattr(sys, name)
@ -370,7 +371,7 @@ class SysCaptureBase(CaptureBase[AnyStr]):
self.tmpfile,
)
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
assert (
self._state in states
), "cannot {} in state {!r}: expected one of {}".format(
@ -457,7 +458,7 @@ class FDCaptureBase(CaptureBase[AnyStr]):
# Further complications are the need to support suspend() and the
# possibility of FD reuse (e.g. the tmpfile getting the very same
# target FD). The following approach is robust, I believe.
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
os.dup2(self.targetfd_invalid, targetfd)
else:
self.targetfd_invalid = None
@ -487,7 +488,7 @@ class FDCaptureBase(CaptureBase[AnyStr]):
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
)
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
assert (
self._state in states
), "cannot {} in state {!r}: expected one of {}".format(
@ -609,13 +610,13 @@ class MultiCapture(Generic[AnyStr]):
def __init__(
self,
in_: Optional[CaptureBase[AnyStr]],
out: Optional[CaptureBase[AnyStr]],
err: Optional[CaptureBase[AnyStr]],
in_: CaptureBase[AnyStr] | None,
out: CaptureBase[AnyStr] | None,
err: CaptureBase[AnyStr] | None,
) -> None:
self.in_: Optional[CaptureBase[AnyStr]] = in_
self.out: Optional[CaptureBase[AnyStr]] = out
self.err: Optional[CaptureBase[AnyStr]] = err
self.in_: CaptureBase[AnyStr] | None = in_
self.out: CaptureBase[AnyStr] | None = out
self.err: CaptureBase[AnyStr] | None = err
def __repr__(self) -> str:
return (
@ -632,7 +633,7 @@ class MultiCapture(Generic[AnyStr]):
if self.err:
self.err.start()
def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
"""Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
@ -725,8 +726,8 @@ class CaptureManager:
def __init__(self, method: _CaptureMethod) -> None:
self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None
self._global_capturing: MultiCapture[str] | None = None
self._capture_fixture: CaptureFixture[Any] | None = None
def __repr__(self) -> str:
return (
@ -734,7 +735,7 @@ class CaptureManager:
f"_capture_fixture={self._capture_fixture!r}>"
)
def is_capturing(self) -> Union[str, bool]:
def is_capturing(self) -> str | bool:
if self.is_globally_capturing():
return "global"
if self._capture_fixture:
@ -782,7 +783,7 @@ class CaptureManager:
# Fixture Control
def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
if self._capture_fixture:
current_fixture = self._capture_fixture.request.fixturename
requested_fixture = capture_fixture.request.fixturename
@ -897,15 +898,15 @@ class CaptureFixture(Generic[AnyStr]):
def __init__(
self,
captureclass: Type[CaptureBase[AnyStr]],
captureclass: type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.captureclass: type[CaptureBase[AnyStr]] = captureclass
self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None
self._capture: MultiCapture[AnyStr] | None = None
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Command line options, ini-file and conftest.py processing."""
from __future__ import annotations
import argparse
import collections.abc
import copy
@ -11,7 +13,7 @@ import glob
import importlib.metadata
import inspect
import os
from pathlib import Path
import pathlib
import re
import shlex
import sys
@ -21,22 +23,16 @@ from types import FunctionType
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Final
from typing import final
from typing import Generator
from typing import IO
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Sequence
from typing import Set
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import warnings
import pluggy
@ -118,7 +114,7 @@ class ExitCode(enum.IntEnum):
class ConftestImportFailure(Exception):
def __init__(
self,
path: Path,
path: pathlib.Path,
*,
cause: Exception,
) -> None:
@ -141,9 +137,9 @@ def filter_traceback_for_conftest_import_failure(
def main(
args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> Union[int, ExitCode]:
args: list[str] | os.PathLike[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> int | ExitCode:
"""Perform an in-process test run.
:param args:
@ -176,9 +172,7 @@ def main(
return ExitCode.USAGE_ERROR
else:
try:
ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main(
config=config
)
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
try:
return ExitCode(ret)
except ValueError:
@ -286,9 +280,9 @@ builtin_plugins.add("pytester_assertions")
def get_config(
args: Optional[List[str]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> "Config":
args: list[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:
# subsequent calls to main will create a fresh instance
pluginmanager = PytestPluginManager()
config = Config(
@ -296,7 +290,7 @@ def get_config(
invocation_params=Config.InvocationParams(
args=args or (),
plugins=plugins,
dir=Path.cwd(),
dir=pathlib.Path.cwd(),
),
)
@ -310,7 +304,7 @@ def get_config(
return config
def get_plugin_manager() -> "PytestPluginManager":
def get_plugin_manager() -> PytestPluginManager:
"""Obtain a new instance of the
:py:class:`pytest.PytestPluginManager`, with default plugins
already loaded.
@ -322,9 +316,9 @@ def get_plugin_manager() -> "PytestPluginManager":
def _prepareconfig(
args: Optional[Union[List[str], "os.PathLike[str]"]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> "Config":
args: list[str] | os.PathLike[str] | None = None,
plugins: Sequence[str | _PluggyPlugin] | None = None,
) -> Config:
if args is None:
args = sys.argv[1:]
elif isinstance(args, os.PathLike):
@ -353,7 +347,7 @@ def _prepareconfig(
raise
def _get_directory(path: Path) -> Path:
def _get_directory(path: pathlib.Path) -> pathlib.Path:
"""Get the directory of a path - itself if already a directory."""
if path.is_file():
return path.parent
@ -364,14 +358,14 @@ def _get_directory(path: Path) -> Path:
def _get_legacy_hook_marks(
method: Any,
hook_type: str,
opt_names: Tuple[str, ...],
) -> Dict[str, bool]:
opt_names: tuple[str, ...],
) -> dict[str, bool]:
if TYPE_CHECKING:
# abuse typeguard from importlib to avoid massive method type union thats lacking a alias
assert inspect.isroutine(method)
known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])}
must_warn: List[str] = []
opts: Dict[str, bool] = {}
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
must_warn: list[str] = []
opts: dict[str, bool] = {}
for opt_name in opt_names:
opt_attr = getattr(method, opt_name, AttributeError)
if opt_attr is not AttributeError:
@ -410,13 +404,13 @@ class PytestPluginManager(PluginManager):
# -- State related to local conftest plugins.
# All loaded conftest modules.
self._conftest_plugins: Set[types.ModuleType] = set()
self._conftest_plugins: set[types.ModuleType] = set()
# All conftest modules applicable for a directory.
# This includes the directory's own conftest modules as well
# as those of its parent directories.
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
# Cutoff directory above which conftests are no longer discovered.
self._confcutdir: Optional[Path] = None
self._confcutdir: pathlib.Path | None = None
# If set, conftest loading is skipped.
self._noconftest = False
@ -430,7 +424,7 @@ class PytestPluginManager(PluginManager):
# previously we would issue a warning when a plugin was skipped, but
# since we refactored warnings as first citizens of Config, they are
# just stored here to be used later.
self.skipped_plugins: List[Tuple[str, str]] = []
self.skipped_plugins: list[tuple[str, str]] = []
self.add_hookspecs(_pytest.hookspec)
self.register(self)
@ -456,7 +450,7 @@ class PytestPluginManager(PluginManager):
def parse_hookimpl_opts(
self, plugin: _PluggyPlugin, name: str
) -> Optional[HookimplOpts]:
) -> HookimplOpts | None:
""":meta private:"""
# pytest hooks are always prefixed with "pytest_",
# so we avoid accessing possibly non-readable attributes
@ -480,7 +474,7 @@ class PytestPluginManager(PluginManager):
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
)
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
""":meta private:"""
opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None:
@ -493,9 +487,7 @@ class PytestPluginManager(PluginManager):
)
return opts
def register(
self, plugin: _PluggyPlugin, name: Optional[str] = None
) -> Optional[str]:
def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
warnings.warn(
PytestConfigWarning(
@ -522,14 +514,14 @@ class PytestPluginManager(PluginManager):
def getplugin(self, name: str):
# Support deprecated naming because plugins (xdist e.g.) use it.
plugin: Optional[_PluggyPlugin] = self.get_plugin(name)
plugin: _PluggyPlugin | None = self.get_plugin(name)
return plugin
def hasplugin(self, name: str) -> bool:
"""Return whether a plugin with the given name is registered."""
return bool(self.get_plugin(name))
def pytest_configure(self, config: "Config") -> None:
def pytest_configure(self, config: Config) -> None:
""":meta private:"""
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
# we should remove tryfirst/trylast as markers.
@ -552,13 +544,13 @@ class PytestPluginManager(PluginManager):
#
def _set_initial_conftests(
self,
args: Sequence[Union[str, Path]],
args: Sequence[str | pathlib.Path],
pyargs: bool,
noconftest: bool,
rootpath: Path,
confcutdir: Optional[Path],
invocation_dir: Path,
importmode: Union[ImportMode, str],
rootpath: pathlib.Path,
confcutdir: pathlib.Path | None,
invocation_dir: pathlib.Path,
importmode: ImportMode | str,
*,
consider_namespace_packages: bool,
) -> None:
@ -601,7 +593,7 @@ class PytestPluginManager(PluginManager):
consider_namespace_packages=consider_namespace_packages,
)
def _is_in_confcutdir(self, path: Path) -> bool:
def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
"""Whether to consider the given path to load conftests from."""
if self._confcutdir is None:
return True
@ -618,9 +610,9 @@ class PytestPluginManager(PluginManager):
def _try_load_conftest(
self,
anchor: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
anchor: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> None:
@ -643,9 +635,9 @@ class PytestPluginManager(PluginManager):
def _loadconftestmodules(
self,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
path: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> None:
@ -673,15 +665,15 @@ class PytestPluginManager(PluginManager):
clist.append(mod)
self._dirpath2confmods[directory] = clist
def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
directory = self._get_directory(path)
return self._dirpath2confmods.get(directory, ())
def _rget_with_confmod(
self,
name: str,
path: Path,
) -> Tuple[types.ModuleType, Any]:
path: pathlib.Path,
) -> tuple[types.ModuleType, Any]:
modules = self._getconftestmodules(path)
for mod in reversed(modules):
try:
@ -692,9 +684,9 @@ class PytestPluginManager(PluginManager):
def _importconftest(
self,
conftestpath: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
conftestpath: pathlib.Path,
importmode: str | ImportMode,
rootpath: pathlib.Path,
*,
consider_namespace_packages: bool,
) -> types.ModuleType:
@ -746,7 +738,7 @@ class PytestPluginManager(PluginManager):
def _check_non_top_pytest_plugins(
self,
mod: types.ModuleType,
conftestpath: Path,
conftestpath: pathlib.Path,
) -> None:
if (
hasattr(mod, "pytest_plugins")
@ -832,7 +824,7 @@ class PytestPluginManager(PluginManager):
self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
def _import_plugin_specs(
self, spec: Union[None, types.ModuleType, str, Sequence[str]]
self, spec: None | types.ModuleType | str | Sequence[str]
) -> None:
plugins = _get_plugin_specs_as_list(spec)
for import_spec in plugins:
@ -877,8 +869,8 @@ class PytestPluginManager(PluginManager):
def _get_plugin_specs_as_list(
specs: Union[None, types.ModuleType, str, Sequence[str]],
) -> List[str]:
specs: None | types.ModuleType | str | Sequence[str],
) -> list[str]:
"""Parse a plugins specification into a list of plugin names."""
# None means empty.
if specs is None:
@ -999,19 +991,19 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that.
"""
args: Tuple[str, ...]
args: tuple[str, ...]
"""The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
plugins: Sequence[str | _PluggyPlugin] | None
"""Extra plugins, might be `None`."""
dir: Path
"""The directory from which :func:`pytest.main` was invoked."""
dir: pathlib.Path
"""The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path"""
def __init__(
self,
*,
args: Iterable[str],
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
dir: Path,
plugins: Sequence[str | _PluggyPlugin] | None,
dir: pathlib.Path,
) -> None:
object.__setattr__(self, "args", tuple(args))
object.__setattr__(self, "plugins", plugins)
@ -1032,20 +1024,20 @@ class Config:
TESTPATHS = enum.auto()
# Set by cacheprovider plugin.
cache: Optional["Cache"]
cache: Cache
def __init__(
self,
pluginmanager: PytestPluginManager,
*,
invocation_params: Optional[InvocationParams] = None,
invocation_params: InvocationParams | None = None,
) -> None:
from .argparsing import FILE_OR_DIR
from .argparsing import Parser
if invocation_params is None:
invocation_params = self.InvocationParams(
args=(), plugins=None, dir=Path.cwd()
args=(), plugins=None, dir=pathlib.Path.cwd()
)
self.option = argparse.Namespace()
@ -1083,20 +1075,20 @@ class Config:
self.trace = self.pluginmanager.trace.root.get("config")
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self._inicache: Dict[str, Any] = {}
self._inicache: dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {}
self._cleanup: List[Callable[[], None]] = []
self._opt2dest: dict[str, str] = {}
self._cleanup: list[Callable[[], None]] = []
self.pluginmanager.register(self, "pytestconfig")
self._configured = False
self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
)
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
self.args: list[str] = []
@property
def rootpath(self) -> Path:
def rootpath(self) -> pathlib.Path:
"""The path to the :ref:`rootdir <rootdir>`.
:type: pathlib.Path
@ -1106,11 +1098,9 @@ class Config:
return self._rootpath
@property
def inipath(self) -> Optional[Path]:
def inipath(self) -> pathlib.Path | None:
"""The path to the :ref:`configfile <configfiles>`.
:type: Optional[pathlib.Path]
.. versionadded:: 6.1
"""
return self._inipath
@ -1137,15 +1127,15 @@ class Config:
fin()
def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
"terminalreporter"
)
assert terminalreporter is not None
return terminalreporter._tw
def pytest_cmdline_parse(
self, pluginmanager: PytestPluginManager, args: List[str]
) -> "Config":
self, pluginmanager: PytestPluginManager, args: list[str]
) -> Config:
try:
self.parse(args)
except UsageError:
@ -1171,7 +1161,7 @@ class Config:
def notify_exception(
self,
excinfo: ExceptionInfo[BaseException],
option: Optional[argparse.Namespace] = None,
option: argparse.Namespace | None = None,
) -> None:
if option and getattr(option, "fulltrace", False):
style: TracebackStyle = "long"
@ -1194,7 +1184,7 @@ class Config:
return nodeid
@classmethod
def fromdictargs(cls, option_dict, args) -> "Config":
def fromdictargs(cls, option_dict, args) -> Config:
"""Constructor usable for subprocesses."""
config = get_config(args)
config.option.__dict__.update(option_dict)
@ -1203,7 +1193,7 @@ class Config:
config.pluginmanager.consider_pluginarg(x)
return config
def _processopt(self, opt: "Argument") -> None:
def _processopt(self, opt: Argument) -> None:
for name in opt._short_opts + opt._long_opts:
self._opt2dest[name] = opt.dest
@ -1212,7 +1202,7 @@ class Config:
setattr(self.option, opt.dest, opt.default)
@hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
def pytest_load_initial_conftests(self, early_config: Config) -> None:
# We haven't fully parsed the command line arguments yet, so
# early_config.args it not set yet. But we need it for
# discovering the initial conftests. So "pre-run" the logic here.
@ -1304,7 +1294,7 @@ class Config:
for name in _iter_rewritable_modules(package_files):
hook.mark_rewrite(name)
def _validate_args(self, args: List[str], via: str) -> List[str]:
def _validate_args(self, args: list[str], via: str) -> list[str]:
"""Validate known args."""
self._parser._config_source_hint = via # type: ignore
try:
@ -1319,13 +1309,13 @@ class Config:
def _decide_args(
self,
*,
args: List[str],
args: list[str],
pyargs: bool,
testpaths: List[str],
invocation_dir: Path,
rootpath: Path,
testpaths: list[str],
invocation_dir: pathlib.Path,
rootpath: pathlib.Path,
warn: bool,
) -> Tuple[List[str], ArgsSource]:
) -> tuple[list[str], ArgsSource]:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
@ -1361,7 +1351,7 @@ class Config:
result = [str(invocation_dir)]
return result, source
def _preparse(self, args: List[str], addopts: bool = True) -> None:
def _preparse(self, args: list[str], addopts: bool = True) -> None:
if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
if len(env_addopts):
@ -1485,11 +1475,11 @@ class Config:
self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
def _get_unknown_ini_keys(self) -> List[str]:
def _get_unknown_ini_keys(self) -> list[str]:
parser_inicfg = self._parser._inidict
return [name for name in self.inicfg if name not in parser_inicfg]
def parse(self, args: List[str], addopts: bool = True) -> None:
def parse(self, args: list[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object.
assert (
self.args == []
@ -1593,7 +1583,7 @@ class Config:
# Meant for easy monkeypatching by legacypath plugin.
# Can be inlined back (with no cover removed) once legacypath is gone.
def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]):
def _getini_unknown_type(self, name: str, type: str, value: str | list[str]):
msg = f"unknown configuration type: {type}"
raise ValueError(msg, value) # pragma: no cover
@ -1649,24 +1639,26 @@ class Config:
else:
return self._getini_unknown_type(name, type, value)
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
def _getconftest_pathlist(
self, name: str, path: pathlib.Path
) -> list[pathlib.Path] | None:
try:
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
except KeyError:
return None
assert mod.__file__ is not None
modpath = Path(mod.__file__).parent
values: List[Path] = []
modpath = pathlib.Path(mod.__file__).parent
values: list[pathlib.Path] = []
for relroot in relroots:
if isinstance(relroot, os.PathLike):
relroot = Path(relroot)
relroot = pathlib.Path(relroot)
else:
relroot = relroot.replace("/", os.sep)
relroot = absolutepath(modpath / relroot)
values.append(relroot)
return values
def _get_override_ini_value(self, name: str) -> Optional[str]:
def _get_override_ini_value(self, name: str) -> str | None:
value = None
# override_ini is a list of "ini=value" options.
# Always use the last item if multiple values are set for same ini-name,
@ -1721,7 +1713,7 @@ class Config:
VERBOSITY_TEST_CASES: Final = "test_cases"
_VERBOSITY_INI_DEFAULT: Final = "auto"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
def get_verbosity(self, verbosity_type: str | None = None) -> int:
r"""Retrieve the verbosity level for a fine-grained verbosity type.
:param verbosity_type: Verbosity type to get level for. If a level is
@ -1772,7 +1764,7 @@ class Config:
return f"verbosity_{verbosity_type}"
@staticmethod
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
"""Add a output verbosity configuration option for the given output type.
:param parser: Parser for command line arguments and ini-file values.
@ -1828,7 +1820,7 @@ def _assertion_supported() -> bool:
def create_terminal_writer(
config: Config, file: Optional[TextIO] = None
config: Config, file: TextIO | None = None
) -> TerminalWriter:
"""Create a TerminalWriter instance configured according to the options
in the config object.
@ -1872,7 +1864,7 @@ def _strtobool(val: str) -> bool:
@lru_cache(maxsize=50)
def parse_warning_filter(
arg: str, *, escape: bool
) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]:
) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
"""Parse a warnings filter string.
This is copied from warnings._setoption with the following changes:
@ -1918,7 +1910,7 @@ def parse_warning_filter(
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e))) from None
try:
category: Type[Warning] = _resolve_warning_category(category_)
category: type[Warning] = _resolve_warning_category(category_)
except Exception:
exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native")
@ -1941,7 +1933,7 @@ def parse_warning_filter(
return action, message, category, module, lineno
def _resolve_warning_category(category: str) -> Type[Warning]:
def _resolve_warning_category(category: str) -> type[Warning]:
"""
Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
propagate so we can get access to their tracebacks (#9218).

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import argparse
from gettext import gettext
import os
@ -6,16 +8,12 @@ import sys
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import final
from typing import List
from typing import Literal
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
import _pytest._io
from _pytest.config.exceptions import UsageError
@ -41,32 +39,32 @@ class Parser:
there's an error processing the command line arguments.
"""
prog: Optional[str] = None
prog: str | None = None
def __init__(
self,
usage: Optional[str] = None,
processopt: Optional[Callable[["Argument"], None]] = None,
usage: str | None = None,
processopt: Callable[[Argument], None] | None = None,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True)
self._groups: List[OptionGroup] = []
self._groups: list[OptionGroup] = []
self._processopt = processopt
self._usage = usage
self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {}
self._ininames: List[str] = []
self.extra_info: Dict[str, Any] = {}
self._inidict: dict[str, tuple[str, str | None, Any]] = {}
self._ininames: list[str] = []
self.extra_info: dict[str, Any] = {}
def processoption(self, option: "Argument") -> None:
def processoption(self, option: Argument) -> None:
if self._processopt:
if option.dest:
self._processopt(option)
def getgroup(
self, name: str, description: str = "", after: Optional[str] = None
) -> "OptionGroup":
self, name: str, description: str = "", after: str | None = None
) -> OptionGroup:
"""Get (or create) a named option Group.
:param name: Name of the option group.
@ -108,8 +106,8 @@ class Parser:
def parse(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
args: Sequence[str | os.PathLike[str]],
namespace: argparse.Namespace | None = None,
) -> argparse.Namespace:
from _pytest._argcomplete import try_argcomplete
@ -118,7 +116,7 @@ class Parser:
strargs = [os.fspath(x) for x in args]
return self.optparser.parse_args(strargs, namespace=namespace)
def _getparser(self) -> "MyOptionParser":
def _getparser(self) -> MyOptionParser:
from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
@ -139,10 +137,10 @@ class Parser:
def parse_setoption(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
args: Sequence[str | os.PathLike[str]],
option: argparse.Namespace,
namespace: Optional[argparse.Namespace] = None,
) -> List[str]:
namespace: argparse.Namespace | None = None,
) -> list[str]:
parsedoption = self.parse(args, namespace=namespace)
for name, value in parsedoption.__dict__.items():
setattr(option, name, value)
@ -150,8 +148,8 @@ class Parser:
def parse_known_args(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
args: Sequence[str | os.PathLike[str]],
namespace: argparse.Namespace | None = None,
) -> argparse.Namespace:
"""Parse the known arguments at this point.
@ -161,9 +159,9 @@ class Parser:
def parse_known_and_unknown_args(
self,
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> Tuple[argparse.Namespace, List[str]]:
args: Sequence[str | os.PathLike[str]],
namespace: argparse.Namespace | None = None,
) -> tuple[argparse.Namespace, list[str]]:
"""Parse the known arguments at this point, and also return the
remaining unknown arguments.
@ -179,9 +177,8 @@ class Parser:
self,
name: str,
help: str,
type: Optional[
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
] = None,
type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
| None = None,
default: Any = NOT_SET,
) -> None:
"""Register an ini-file option.
@ -224,7 +221,7 @@ class Parser:
def get_ini_default_for_type(
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None,
) -> Any:
"""
Used by addini to get the default value for a given ini-option type, when
@ -244,7 +241,7 @@ class ArgumentError(Exception):
"""Raised if an Argument instance is created with invalid or
inconsistent arguments."""
def __init__(self, msg: str, option: Union["Argument", str]) -> None:
def __init__(self, msg: str, option: Argument | str) -> None:
self.msg = msg
self.option_id = str(option)
@ -267,8 +264,8 @@ class Argument:
def __init__(self, *names: str, **attrs: Any) -> None:
"""Store params in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts: List[str] = []
self._long_opts: List[str] = []
self._short_opts: list[str] = []
self._long_opts: list[str] = []
try:
self.type = attrs["type"]
except KeyError:
@ -279,7 +276,7 @@ class Argument:
except KeyError:
pass
self._set_opt_strings(names)
dest: Optional[str] = attrs.get("dest")
dest: str | None = attrs.get("dest")
if dest:
self.dest = dest
elif self._long_opts:
@ -291,7 +288,7 @@ class Argument:
self.dest = "???" # Needed for the error repr.
raise ArgumentError("need a long or short option", self) from e
def names(self) -> List[str]:
def names(self) -> list[str]:
return self._short_opts + self._long_opts
def attrs(self) -> Mapping[str, Any]:
@ -335,7 +332,7 @@ class Argument:
self._long_opts.append(opt)
def __repr__(self) -> str:
args: List[str] = []
args: list[str] = []
if self._short_opts:
args += ["_short_opts: " + repr(self._short_opts)]
if self._long_opts:
@ -355,14 +352,14 @@ class OptionGroup:
self,
name: str,
description: str = "",
parser: Optional[Parser] = None,
parser: Parser | None = None,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.name = name
self.description = description
self.options: List[Argument] = []
self.options: list[Argument] = []
self.parser = parser
def addoption(self, *opts: str, **attrs: Any) -> None:
@ -391,7 +388,7 @@ class OptionGroup:
option = Argument(*opts, **attrs)
self._addoption_instance(option, shortupper=True)
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
def _addoption_instance(self, option: Argument, shortupper: bool = False) -> None:
if not shortupper:
for opt in option._short_opts:
if opt[0] == "-" and opt[1].islower():
@ -405,8 +402,8 @@ class MyOptionParser(argparse.ArgumentParser):
def __init__(
self,
parser: Parser,
extra_info: Optional[Dict[str, Any]] = None,
prog: Optional[str] = None,
extra_info: dict[str, Any] | None = None,
prog: str | None = None,
) -> None:
self._parser = parser
super().__init__(
@ -433,8 +430,8 @@ class MyOptionParser(argparse.ArgumentParser):
# Type ignored because typeshed has a very complex type in the superclass.
def parse_args( # type: ignore
self,
args: Optional[Sequence[str]] = None,
namespace: Optional[argparse.Namespace] = None,
args: Sequence[str] | None = None,
namespace: argparse.Namespace | None = None,
) -> argparse.Namespace:
"""Allow splitting of positional arguments."""
parsed, unrecognized = self.parse_known_args(args, namespace)
@ -455,7 +452,7 @@ class MyOptionParser(argparse.ArgumentParser):
# disable long --argument abbreviations without breaking short flags.
def _parse_optional(
self, arg_string: str
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
) -> tuple[argparse.Action | None, str, str | None] | None:
if not arg_string:
return None
if arg_string[0] not in self.prefix_chars:
@ -507,7 +504,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
orgstr = super()._format_action_invocation(action)
if orgstr and orgstr[0] != "-": # only optional arguments
return orgstr
res: Optional[str] = getattr(action, "_formatted_action_invocation", None)
res: str | None = getattr(action, "_formatted_action_invocation", None)
if res:
return res
options = orgstr.split(", ")
@ -516,7 +513,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
action._formatted_action_invocation = orgstr # type: ignore
return orgstr
return_list = []
short_long: Dict[str, str] = {}
short_long: dict[str, str] = {}
for option in options:
if len(option) == 2 or option[2] == " ":
continue

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import final

View File

@ -1,13 +1,10 @@
from __future__ import annotations
import os
from pathlib import Path
import sys
from typing import Dict
from typing import Iterable
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
import iniconfig
@ -32,7 +29,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
def load_config_dict_from_file(
filepath: Path,
) -> Optional[Dict[str, Union[str, List[str]]]]:
) -> dict[str, str | list[str]] | None:
"""Load pytest configuration from the given file path, if supported.
Return None if the file does not contain valid pytest configuration.
@ -77,7 +74,7 @@ def load_config_dict_from_file(
# TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
# however we need to convert all scalar values to str for compatibility with the rest
# of the configuration system, which expects strings only.
def make_scalar(v: object) -> Union[str, List[str]]:
def make_scalar(v: object) -> str | list[str]:
return v if isinstance(v, list) else str(v)
return {k: make_scalar(v) for k, v in result.items()}
@ -88,7 +85,7 @@ def load_config_dict_from_file(
def locate_config(
invocation_dir: Path,
args: Iterable[Path],
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
) -> tuple[Path | None, Path | None, dict[str, str | list[str]]]:
"""Search in the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
@ -101,7 +98,7 @@ def locate_config(
args = [x for x in args if not str(x).startswith("-")]
if not args:
args = [invocation_dir]
found_pyproject_toml: Optional[Path] = None
found_pyproject_toml: Path | None = None
for arg in args:
argpath = absolutepath(arg)
for base in (argpath, *argpath.parents):
@ -122,7 +119,7 @@ def get_common_ancestor(
invocation_dir: Path,
paths: Iterable[Path],
) -> Path:
common_ancestor: Optional[Path] = None
common_ancestor: Path | None = None
for path in paths:
if not path.exists():
continue
@ -144,7 +141,7 @@ def get_common_ancestor(
return common_ancestor
def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
def get_dirs_from_args(args: Iterable[str]) -> list[Path]:
def is_option(x: str) -> bool:
return x.startswith("-")
@ -171,11 +168,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
def determine_setup(
*,
inifile: Optional[str],
inifile: str | None,
args: Sequence[str],
rootdir_cmd_arg: Optional[str],
rootdir_cmd_arg: str | None,
invocation_dir: Path,
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
) -> tuple[Path, Path | None, dict[str, str | list[str]]]:
"""Determine the rootdir, inifile and ini configuration values from the
command line arguments.
@ -192,7 +189,7 @@ def determine_setup(
dirs = get_dirs_from_args(args)
if inifile:
inipath_ = absolutepath(inifile)
inipath: Optional[Path] = inipath_
inipath: Path | None = inipath_
inicfg = load_config_dict_from_file(inipath_) or {}
if rootdir_cmd_arg is None:
rootdir = inipath_.parent

View File

@ -2,6 +2,8 @@
# ruff: noqa: T100
"""Interactive debugging with PDB, the Python Debugger."""
from __future__ import annotations
import argparse
import functools
import sys
@ -9,11 +11,6 @@ import types
from typing import Any
from typing import Callable
from typing import Generator
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union
import unittest
from _pytest import outcomes
@ -30,7 +27,7 @@ from _pytest.reports import BaseReport
from _pytest.runner import CallInfo
def _validate_usepdb_cls(value: str) -> Tuple[str, str]:
def _validate_usepdb_cls(value: str) -> tuple[str, str]:
"""Validate syntax of --pdbcls option."""
try:
modname, classname = value.split(":")
@ -95,22 +92,22 @@ def pytest_configure(config: Config) -> None:
class pytestPDB:
"""Pseudo PDB that defers to the real pdb."""
_pluginmanager: Optional[PytestPluginManager] = None
_config: Optional[Config] = None
_saved: List[
Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]]
_pluginmanager: PytestPluginManager | None = None
_config: Config | None = None
_saved: list[
tuple[Callable[..., None], PytestPluginManager | None, Config | None]
] = []
_recursive_debug = 0
_wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None
_wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None
@classmethod
def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]:
def _is_capturing(cls, capman: CaptureManager | None) -> str | bool:
if capman:
return capman.is_capturing()
return False
@classmethod
def _import_pdb_cls(cls, capman: Optional["CaptureManager"]):
def _import_pdb_cls(cls, capman: CaptureManager | None):
if not cls._config:
import pdb
@ -149,7 +146,7 @@ class pytestPDB:
return wrapped_cls
@classmethod
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
import _pytest.config
class PytestPdbWrapper(pdb_cls):
@ -238,7 +235,7 @@ class pytestPDB:
import _pytest.config
if cls._pluginmanager is None:
capman: Optional[CaptureManager] = None
capman: CaptureManager | None = None
else:
capman = cls._pluginmanager.getplugin("capturemanager")
if capman:
@ -281,7 +278,7 @@ class pytestPDB:
class PdbInvoke:
def pytest_exception_interact(
self, node: Node, call: "CallInfo[Any]", report: BaseReport
self, node: Node, call: CallInfo[Any], report: BaseReport
) -> None:
capman = node.config.pluginmanager.getplugin("capturemanager")
if capman:

View File

@ -9,6 +9,8 @@ All constants defined in this module should be either instances of
in case of warnings which need to format their messages.
"""
from __future__ import annotations
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Discover and run doctests in modules and test files."""
from __future__ import annotations
import bdb
from contextlib import contextmanager
import functools
@ -13,17 +15,11 @@ import traceback
import types
from typing import Any
from typing import Callable
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import warnings
from _pytest import outcomes
@ -67,7 +63,7 @@ DOCTEST_REPORT_CHOICES = (
# Lazy definition of runner class
RUNNER_CLASS = None
# Lazy definition of output checker class
CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
CHECKER_CLASS: type[doctest.OutputChecker] | None = None
def pytest_addoption(parser: Parser) -> None:
@ -129,7 +125,7 @@ def pytest_unconfigure() -> None:
def pytest_collect_file(
file_path: Path,
parent: Collector,
) -> Optional[Union["DoctestModule", "DoctestTextfile"]]:
) -> DoctestModule | DoctestTextfile | None:
config = parent.config
if file_path.suffix == ".py":
if config.option.doctestmodules and not any(
@ -161,7 +157,7 @@ def _is_main_py(path: Path) -> bool:
class ReprFailDoctest(TerminalRepr):
def __init__(
self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]]
) -> None:
self.reprlocation_lines = reprlocation_lines
@ -173,12 +169,12 @@ class ReprFailDoctest(TerminalRepr):
class MultipleDoctestFailures(Exception):
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None:
super().__init__()
self.failures = failures
def _init_runner_class() -> Type["doctest.DocTestRunner"]:
def _init_runner_class() -> type[doctest.DocTestRunner]:
import doctest
class PytestDoctestRunner(doctest.DebugRunner):
@ -190,8 +186,8 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]:
def __init__(
self,
checker: Optional["doctest.OutputChecker"] = None,
verbose: Optional[bool] = None,
checker: doctest.OutputChecker | None = None,
verbose: bool | None = None,
optionflags: int = 0,
continue_on_failure: bool = True,
) -> None:
@ -201,8 +197,8 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]:
def report_failure(
self,
out,
test: "doctest.DocTest",
example: "doctest.Example",
test: doctest.DocTest,
example: doctest.Example,
got: str,
) -> None:
failure = doctest.DocTestFailure(test, example, got)
@ -214,9 +210,9 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]:
def report_unexpected_exception(
self,
out,
test: "doctest.DocTest",
example: "doctest.Example",
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
test: doctest.DocTest,
example: doctest.Example,
exc_info: tuple[type[BaseException], BaseException, types.TracebackType],
) -> None:
if isinstance(exc_info[1], OutcomeException):
raise exc_info[1]
@ -232,11 +228,11 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]:
def _get_runner(
checker: Optional["doctest.OutputChecker"] = None,
verbose: Optional[bool] = None,
checker: doctest.OutputChecker | None = None,
verbose: bool | None = None,
optionflags: int = 0,
continue_on_failure: bool = True,
) -> "doctest.DocTestRunner":
) -> doctest.DocTestRunner:
# We need this in order to do a lazy import on doctest
global RUNNER_CLASS
if RUNNER_CLASS is None:
@ -255,9 +251,9 @@ class DoctestItem(Item):
def __init__(
self,
name: str,
parent: "Union[DoctestTextfile, DoctestModule]",
runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest",
parent: DoctestTextfile | DoctestModule,
runner: doctest.DocTestRunner,
dtest: doctest.DocTest,
) -> None:
super().__init__(name, parent)
self.runner = runner
@ -274,18 +270,18 @@ class DoctestItem(Item):
@classmethod
def from_parent( # type: ignore[override]
cls,
parent: "Union[DoctestTextfile, DoctestModule]",
parent: DoctestTextfile | DoctestModule,
*,
name: str,
runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest",
) -> "Self":
runner: doctest.DocTestRunner,
dtest: doctest.DocTest,
) -> Self:
# incompatible signature due to imposed limits on subclass
"""The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self.funcargs: dict[str, object] = {}
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
def setup(self) -> None:
@ -298,7 +294,7 @@ class DoctestItem(Item):
def runtest(self) -> None:
_check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin()
failures: List[doctest.DocTestFailure] = []
failures: list[doctest.DocTestFailure] = []
# Type ignored because we change the type of `out` from what
# doctest expects.
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
@ -320,12 +316,12 @@ class DoctestItem(Item):
def repr_failure( # type: ignore[override]
self,
excinfo: ExceptionInfo[BaseException],
) -> Union[str, TerminalRepr]:
) -> str | TerminalRepr:
import doctest
failures: Optional[
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
] = None
failures: (
Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None
) = None
if isinstance(
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
):
@ -381,11 +377,11 @@ class DoctestItem(Item):
reprlocation_lines.append((reprlocation, lines))
return ReprFailDoctest(reprlocation_lines)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
return self.path, self.dtest.lineno, f"[doctest] {self.name}"
def _get_flag_lookup() -> Dict[str, int]:
def _get_flag_lookup() -> dict[str, int]:
import doctest
return dict(
@ -451,7 +447,7 @@ class DoctestTextfile(Module):
)
def _check_all_skipped(test: "doctest.DocTest") -> None:
def _check_all_skipped(test: doctest.DocTest) -> None:
"""Raise pytest.skip() if all examples in the given DocTest have the SKIP
option set."""
import doctest
@ -477,7 +473,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
real_unwrap = inspect.unwrap
def _mock_aware_unwrap(
func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None
) -> Any:
try:
if stop is None or stop is _is_mocked:
@ -594,7 +590,7 @@ class DoctestModule(Module):
)
def _init_checker_class() -> Type["doctest.OutputChecker"]:
def _init_checker_class() -> type[doctest.OutputChecker]:
import doctest
import re
@ -662,8 +658,8 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]:
return got
offset = 0
for w, g in zip(wants, gots):
fraction: Optional[str] = w.group("fraction")
exponent: Optional[str] = w.group("exponent1")
fraction: str | None = w.group("fraction")
exponent: str | None = w.group("exponent1")
if exponent is None:
exponent = w.group("exponent2")
precision = 0 if fraction is None else len(fraction)
@ -682,7 +678,7 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]:
return LiteralsOutputChecker
def _get_checker() -> "doctest.OutputChecker":
def _get_checker() -> doctest.OutputChecker:
"""Return a doctest.OutputChecker subclass that supports some
additional options:
@ -741,7 +737,7 @@ def _get_report_choice(key: str) -> int:
@fixture(scope="session")
def doctest_namespace() -> Dict[str, Any]:
def doctest_namespace() -> dict[str, Any]:
"""Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import os
import sys
from typing import Generator

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import abc
from collections import defaultdict
from collections import deque
@ -20,7 +22,6 @@ from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import NoReturn
@ -28,7 +29,6 @@ from typing import Optional
from typing import OrderedDict
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
@ -113,18 +113,18 @@ _FixtureCachedResult = Union[
@dataclasses.dataclass(frozen=True)
class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]"
cached_result: _FixtureCachedResult[FixtureValue]
_scope: Scope
def pytest_sessionstart(session: "Session") -> None:
def pytest_sessionstart(session: Session) -> None:
session._fixturemanager = FixtureManager(session)
def get_scope_package(
node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[nodes.Node]:
fixturedef: FixtureDef[object],
) -> nodes.Node | None:
from _pytest.python import Package
for parent in node.iter_parents():
@ -133,7 +133,7 @@ def get_scope_package(
return node.session
def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None:
import _pytest.python
if scope is Scope.Function:
@ -152,7 +152,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
assert_never(scope)
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
"""Return fixturemarker or None if it doesn't exist or raised
exceptions."""
return cast(
@ -171,8 +171,8 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
class FixtureArgKey:
argname: str
param_index: int
scoped_item_path: Optional[Path]
item_cls: Optional[type]
scoped_item_path: Path | None
item_cls: type | None
_V = TypeVar("_V")
@ -212,10 +212,10 @@ def get_parametrized_fixture_argkeys(
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_by_item: Dict[Scope, Dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
items_by_argkey: Dict[
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
items_by_argkey: dict[
Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
] = {}
for scope in HIGH_SCOPES:
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
@ -249,7 +249,7 @@ def reorder_items_atscope(
scoped_items_by_argkey = items_by_argkey[scope]
scoped_argkeys_by_item = argkeys_by_item[scope]
ignore: Set[FixtureArgKey] = set()
ignore: set[FixtureArgKey] = set()
items_deque = deque(items)
items_done: OrderedSet[nodes.Item] = {}
while items_deque:
@ -309,19 +309,19 @@ class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Fixture names that the item requests directly by function parameters.
argnames: Tuple[str, ...]
argnames: tuple[str, ...]
# Fixture names that the item immediately requires. These include
# argnames + fixture names specified via usefixtures and via autouse=True in
# fixture definitions.
initialnames: Tuple[str, ...]
initialnames: tuple[str, ...]
# The transitive closure of the fixture names that the item requires.
# Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
names_closure: List[str]
names_closure: list[str]
# A map from a fixture name in the transitive closure to the FixtureDefs
# matching the name which are applicable to this function.
# There may be multiple overriding fixtures with the same name. The
# sequence is ordered from furthest to closes to the function.
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]]
def prune_dependency_tree(self) -> None:
"""Recompute names_closure from initialnames and name2fixturedefs.
@ -334,7 +334,7 @@ class FuncFixtureInfo:
tree. In this way the dependency tree can get pruned, and the closure
of argnames may get reduced.
"""
closure: Set[str] = set()
closure: set[str] = set()
working_set = set(self.initialnames)
while working_set:
argname = working_set.pop()
@ -360,10 +360,10 @@ class FixtureRequest(abc.ABC):
def __init__(
self,
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
fixture_defs: Dict[str, "FixtureDef[Any]"],
pyfuncitem: Function,
fixturename: str | None,
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]],
fixture_defs: dict[str, FixtureDef[Any]],
*,
_ispytest: bool = False,
) -> None:
@ -390,7 +390,7 @@ class FixtureRequest(abc.ABC):
self.param: Any
@property
def _fixturemanager(self) -> "FixtureManager":
def _fixturemanager(self) -> FixtureManager:
return self._pyfuncitem.session._fixturemanager
@property
@ -406,13 +406,13 @@ class FixtureRequest(abc.ABC):
@abc.abstractmethod
def _check_scope(
self,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
requested_scope: Scope,
) -> None:
raise NotImplementedError()
@property
def fixturenames(self) -> List[str]:
def fixturenames(self) -> list[str]:
"""Names of all active fixtures in this request."""
result = list(self._pyfuncitem.fixturenames)
result.extend(set(self._fixture_defs).difference(result))
@ -477,7 +477,7 @@ class FixtureRequest(abc.ABC):
return node.keywords
@property
def session(self) -> "Session":
def session(self) -> Session:
"""Pytest session object."""
return self._pyfuncitem.session
@ -487,7 +487,7 @@ class FixtureRequest(abc.ABC):
the last test within the requesting test context finished execution."""
raise NotImplementedError()
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
def applymarker(self, marker: str | MarkDecorator) -> None:
"""Apply a marker to a single test function invocation.
This method is useful if you don't want to have a keyword/marker
@ -498,7 +498,7 @@ class FixtureRequest(abc.ABC):
"""
self.node.add_marker(marker)
def raiseerror(self, msg: Optional[str]) -> NoReturn:
def raiseerror(self, msg: str | None) -> NoReturn:
"""Raise a FixtureLookupError exception.
:param msg:
@ -535,7 +535,7 @@ class FixtureRequest(abc.ABC):
)
return fixturedef.cached_result[0]
def _iter_chain(self) -> Iterator["SubRequest"]:
def _iter_chain(self) -> Iterator[SubRequest]:
"""Yield all SubRequests in the chain, from self up.
Note: does *not* yield the TopRequest.
@ -547,7 +547,7 @@ class FixtureRequest(abc.ABC):
def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
) -> FixtureDef[object] | PseudoFixtureDef[object]:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)
@ -618,7 +618,7 @@ class FixtureRequest(abc.ABC):
self._fixture_defs[argname] = fixturedef
return fixturedef
def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None:
def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
"""Check that this request is allowed to execute this fixturedef without
a param."""
funcitem = self._pyfuncitem
@ -651,7 +651,7 @@ class FixtureRequest(abc.ABC):
)
fail(msg, pytrace=False)
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
def _get_fixturestack(self) -> list[FixtureDef[Any]]:
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values
@ -661,7 +661,7 @@ class FixtureRequest(abc.ABC):
class TopRequest(FixtureRequest):
"""The type of the ``request`` fixture in a test function."""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None:
super().__init__(
fixturename=None,
pyfuncitem=pyfuncitem,
@ -676,7 +676,7 @@ class TopRequest(FixtureRequest):
def _check_scope(
self,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
requested_scope: Scope,
) -> None:
# TopRequest always has function scope so always valid.
@ -710,7 +710,7 @@ class SubRequest(FixtureRequest):
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
fixturedef: FixtureDef[object],
*,
_ispytest: bool = False,
) -> None:
@ -740,7 +740,7 @@ class SubRequest(FixtureRequest):
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[nodes.Node] = self._pyfuncitem
node: nodes.Node | None = self._pyfuncitem
elif scope is Scope.Package:
node = get_scope_package(self._pyfuncitem, self._fixturedef)
else:
@ -753,7 +753,7 @@ class SubRequest(FixtureRequest):
def _check_scope(
self,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object],
requested_scope: Scope,
) -> None:
if isinstance(requested_fixturedef, PseudoFixtureDef):
@ -774,7 +774,7 @@ class SubRequest(FixtureRequest):
pytrace=False,
)
def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str:
def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str:
factory = fixturedef.func
path, lineno = getfslineno(factory)
if isinstance(path, Path):
@ -791,15 +791,15 @@ class FixtureLookupError(LookupError):
"""Could not return a requested fixture (missing or invalid)."""
def __init__(
self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None
self, argname: str | None, request: FixtureRequest, msg: str | None = None
) -> None:
self.argname = argname
self.request = request
self.fixturestack = request._get_fixturestack()
self.msg = msg
def formatrepr(self) -> "FixtureLookupErrorRepr":
tblines: List[str] = []
def formatrepr(self) -> FixtureLookupErrorRepr:
tblines: list[str] = []
addline = tblines.append
stack = [self.request._pyfuncitem.obj]
stack.extend(map(lambda x: x.func, self.fixturestack))
@ -847,11 +847,11 @@ class FixtureLookupError(LookupError):
class FixtureLookupErrorRepr(TerminalRepr):
def __init__(
self,
filename: Union[str, "os.PathLike[str]"],
filename: str | os.PathLike[str],
firstlineno: int,
tblines: Sequence[str],
errorstring: str,
argname: Optional[str],
argname: str | None,
) -> None:
self.tblines = tblines
self.errorstring = errorstring
@ -879,7 +879,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
def call_fixture_func(
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
) -> FixtureValue:
if is_generator(fixturefunc):
fixturefunc = cast(
@ -950,14 +950,12 @@ class FixtureDef(Generic[FixtureValue]):
def __init__(
self,
config: Config,
baseid: Optional[str],
baseid: str | None,
argname: str,
func: "_FixtureFunc[FixtureValue]",
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
params: Optional[Sequence[object]],
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
func: _FixtureFunc[FixtureValue],
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None,
params: Sequence[object] | None,
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
*,
_ispytest: bool = False,
) -> None:
@ -1003,8 +1001,8 @@ class FixtureDef(Generic[FixtureValue]):
self.argnames: Final = getfuncargnames(func, name=argname)
# If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters.
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: Final[List[Callable[[], object]]] = []
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
self._finalizers: Final[list[Callable[[], object]]] = []
@property
def scope(self) -> _ScopeName:
@ -1015,7 +1013,7 @@ class FixtureDef(Generic[FixtureValue]):
self._finalizers.append(finalizer)
def finish(self, request: SubRequest) -> None:
exceptions: List[BaseException] = []
exceptions: list[BaseException] = []
while self._finalizers:
fin = self._finalizers.pop()
try:
@ -1099,7 +1097,7 @@ class FixtureDef(Generic[FixtureValue]):
def resolve_fixture_function(
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[FixtureValue]":
) -> _FixtureFunc[FixtureValue]:
"""Get the actual callable that can be called to obtain the fixture
value."""
fixturefunc = fixturedef.func
@ -1147,7 +1145,7 @@ def pytest_fixture_setup(
def wrap_function_to_error_out_if_called_directly(
function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker",
fixture_marker: FixtureFunctionMarker,
) -> FixtureFunction:
"""Wrap the given fixture function so we can raise an error about it being called directly,
instead of used as an argument in a test function."""
@ -1173,13 +1171,11 @@ def wrap_function_to_error_out_if_called_directly(
@final
@dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]]
scope: _ScopeName | Callable[[str, Config], _ScopeName]
params: tuple[object, ...] | None
autouse: bool = False
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None
name: Optional[str] = None
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None
name: str | None = None
_ispytest: dataclasses.InitVar[bool] = False
@ -1217,13 +1213,11 @@ class FixtureFunctionMarker:
def fixture(
fixture_function: FixtureFunction,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
params: Optional[Iterable[object]] = ...,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
params: Iterable[object] | None = ...,
autouse: bool = ...,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = ...,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = ...,
) -> FixtureFunction: ...
@ -1231,27 +1225,23 @@ def fixture(
def fixture(
fixture_function: None = ...,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
params: Optional[Iterable[object]] = ...,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = ...,
params: Iterable[object] | None = ...,
autouse: bool = ...,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = None,
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
name: str | None = None,
) -> FixtureFunctionMarker: ...
def fixture(
fixture_function: Optional[FixtureFunction] = None,
fixture_function: FixtureFunction | None = None,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
params: Optional[Iterable[object]] = None,
scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function",
params: Iterable[object] | None = None,
autouse: bool = False,
ids: Optional[
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]:
ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
name: str | None = None,
) -> FixtureFunctionMarker | FixtureFunction:
"""Decorator to mark a fixture factory function.
This decorator can be used, with or without parameters, to define a
@ -1385,7 +1375,7 @@ def pytest_addoption(parser: Parser) -> None:
)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.showfixtures:
showfixtures(config)
return 0
@ -1395,7 +1385,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
def _get_direct_parametrize_args(node: nodes.Node) -> set[str]:
"""Return all direct parametrization arguments of a node, so we don't
mistake them for fixtures.
@ -1404,7 +1394,7 @@ def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
These things are done later as well when dealing with parametrization
so this could be improved.
"""
parametrize_argnames: Set[str] = set()
parametrize_argnames: set[str] = set()
for marker in node.iter_markers(name="parametrize"):
if not marker.kwargs.get("indirect", False):
p_argnames, _ = ParameterSet._parse_parametrize_args(
@ -1414,7 +1404,7 @@ def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
return parametrize_argnames
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]:
"""De-duplicate the sequence of names while keeping the original order."""
# Ideally we would use a set, but it does not preserve insertion order.
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
@ -1451,17 +1441,17 @@ class FixtureManager:
by a lookup of their FuncFixtureInfo.
"""
def __init__(self, session: "Session") -> None:
def __init__(self, session: Session) -> None:
self.session = session
self.config: Config = session.config
# Maps a fixture name (argname) to all of the FixtureDefs in the test
# suite/plugins defined with this name. Populated by parsefactories().
# TODO: The order of the FixtureDefs list of each arg is significant,
# explain.
self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {}
self._holderobjseen: Final[Set[object]] = set()
self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {}
self._holderobjseen: Final[set[object]] = set()
# A mapping from a nodeid to a list of autouse fixtures it defines.
self._nodeid_autousenames: Final[Dict[str, List[str]]] = {
self._nodeid_autousenames: Final[dict[str, list[str]]] = {
"": self.config.getini("usefixtures"),
}
session.config.pluginmanager.register(self, "funcmanage")
@ -1469,8 +1459,8 @@ class FixtureManager:
def getfixtureinfo(
self,
node: nodes.Item,
func: Optional[Callable[..., object]],
cls: Optional[type],
func: Callable[..., object] | None,
cls: type | None,
) -> FuncFixtureInfo:
"""Calculate the :class:`FuncFixtureInfo` for an item.
@ -1541,9 +1531,9 @@ class FixtureManager:
def getfixtureclosure(
self,
parentnode: nodes.Node,
initialnames: Tuple[str, ...],
initialnames: tuple[str, ...],
ignore_args: AbstractSet[str],
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]:
# Collect the closure of all fixtures, starting with the given
# fixturenames as the initial set. As we have to visit all
# factory definitions anyway, we also return an arg2fixturedefs
@ -1553,7 +1543,7 @@ class FixtureManager:
fixturenames_closure = list(initialnames)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {}
lastlen = -1
while lastlen != len(fixturenames_closure):
lastlen = len(fixturenames_closure)
@ -1580,7 +1570,7 @@ class FixtureManager:
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
return fixturenames_closure, arg2fixturedefs
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
def pytest_generate_tests(self, metafunc: Metafunc) -> None:
"""Generate new tests based on parametrized fixtures used by the given metafunc"""
def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
@ -1625,7 +1615,7 @@ class FixtureManager:
# Try next super fixture, if any.
def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None:
def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None:
# Separate parametrized setups.
items[:] = reorder_items(items)
@ -1633,15 +1623,11 @@ class FixtureManager:
self,
*,
name: str,
func: "_FixtureFunc[object]",
nodeid: Optional[str],
scope: Union[
Scope, _ScopeName, Callable[[str, Config], _ScopeName]
] = "function",
params: Optional[Sequence[object]] = None,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
func: _FixtureFunc[object],
nodeid: str | None,
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function",
params: Sequence[object] | None = None,
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
autouse: bool = False,
) -> None:
"""Register a fixture
@ -1699,14 +1685,14 @@ class FixtureManager:
def parsefactories(
self,
node_or_obj: object,
nodeid: Optional[str],
nodeid: str | None,
) -> None:
raise NotImplementedError()
def parsefactories(
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
node_or_obj: nodes.Node | object,
nodeid: str | NotSetType | None = NOTSET,
) -> None:
"""Collect fixtures from a collection node or object.
@ -1764,7 +1750,7 @@ class FixtureManager:
def getfixturedefs(
self, argname: str, node: nodes.Node
) -> Optional[Sequence[FixtureDef[Any]]]:
) -> Sequence[FixtureDef[Any]] | None:
"""Get FixtureDefs for a fixture name which are applicable
to a given node.
@ -1791,7 +1777,7 @@ class FixtureManager:
yield fixturedef
def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
def show_fixtures_per_test(config: Config) -> int | ExitCode:
from _pytest.main import wrap_session
return wrap_session(config, _show_fixtures_per_test)
@ -1809,7 +1795,7 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
return bestrelpath(invocation_dir, loc)
def _show_fixtures_per_test(config: Config, session: "Session") -> None:
def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config
session.perform_collect()
@ -1842,7 +1828,7 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> None:
def write_item(item: nodes.Item) -> None:
# Not all items have _fixtureinfo attribute.
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None)
if info is None or not info.name2fixturedefs:
# This test item does not use any fixtures.
return
@ -1862,13 +1848,13 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> None:
write_item(session_item)
def showfixtures(config: Config) -> Union[int, ExitCode]:
def showfixtures(config: Config) -> int | ExitCode:
from _pytest.main import wrap_session
return wrap_session(config, _showfixtures_main)
def _showfixtures_main(config: Config, session: "Session") -> None:
def _showfixtures_main(config: Config, session: Session) -> None:
import _pytest.config
session.perform_collect()
@ -1879,7 +1865,7 @@ def _showfixtures_main(config: Config, session: "Session") -> None:
fm = session._fixturemanager
available = []
seen: Set[Tuple[str, str]] = set()
seen: set[tuple[str, str]] = set()
for argname, fixturedefs in fm._arg2fixturedefs.items():
assert fixturedefs is not None

View File

@ -1,13 +1,13 @@
"""Provides a function to report all internal modules for using freezing
tools."""
from __future__ import annotations
import types
from typing import Iterator
from typing import List
from typing import Union
def freeze_includes() -> List[str]:
def freeze_includes() -> list[str]:
"""Return a list of module names used by pytest that should be
included by cx_freeze."""
import _pytest
@ -17,7 +17,7 @@ def freeze_includes() -> List[str]:
def _iter_all_modules(
package: Union[str, types.ModuleType],
package: str | types.ModuleType,
prefix: str = "",
) -> Iterator[str]:
"""Iterate over the names of all modules that can be found in the given

View File

@ -1,13 +1,12 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration."""
from __future__ import annotations
from argparse import Action
import os
import sys
from typing import Generator
from typing import List
from typing import Optional
from typing import Union
from _pytest.config import Config
from _pytest.config import ExitCode
@ -147,7 +146,7 @@ def showversion(config: Config) -> None:
sys.stdout.write(f"pytest {pytest.__version__}\n")
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.version > 0:
showversion(config)
return 0
@ -162,7 +161,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None:
import textwrap
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
@ -239,7 +238,7 @@ def showhelp(config: Config) -> None:
conftest_options = [("pytest_plugins", "list of plugin names to load")]
def getpluginversioninfo(config: Config) -> List[str]:
def getpluginversioninfo(config: Config) -> list[str]:
lines = []
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
@ -251,7 +250,7 @@ def getpluginversioninfo(config: Config) -> List[str]:
return lines
def pytest_report_header(config: Config) -> List[str]:
def pytest_report_header(config: Config) -> list[str]:
lines = []
if config.option.debug or config.option.traceconfig:
lines.append(f"using: pytest-{pytest.__version__}")

View File

@ -3,16 +3,13 @@
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pluggy import HookspecMarker
@ -57,7 +54,7 @@ hookspec = HookspecMarker("pytest")
@hookspec(historic=True)
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
def pytest_addhooks(pluginmanager: PytestPluginManager) -> None:
"""Called at plugin registration time to allow adding new hooks via a call to
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
@ -76,9 +73,9 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
@hookspec(historic=True)
def pytest_plugin_registered(
plugin: "_PluggyPlugin",
plugin: _PluggyPlugin,
plugin_name: str,
manager: "PytestPluginManager",
manager: PytestPluginManager,
) -> None:
"""A new pytest plugin got registered.
@ -100,7 +97,7 @@ def pytest_plugin_registered(
@hookspec(historic=True)
def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None:
def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
"""Register argparse-style options and ini-style config values,
called once at the beginning of a test run.
@ -141,7 +138,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
@hookspec(historic=True)
def pytest_configure(config: "Config") -> None:
def pytest_configure(config: Config) -> None:
"""Allow plugins and conftest files to perform initial configuration.
.. note::
@ -166,8 +163,8 @@ def pytest_configure(config: "Config") -> None:
@hookspec(firstresult=True)
def pytest_cmdline_parse(
pluginmanager: "PytestPluginManager", args: List[str]
) -> Optional["Config"]:
pluginmanager: PytestPluginManager, args: list[str]
) -> Config | None:
"""Return an initialized :class:`~pytest.Config`, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult`.
@ -189,7 +186,7 @@ def pytest_cmdline_parse(
def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str]
early_config: Config, parser: Parser, args: list[str]
) -> None:
"""Called to implement the loading of :ref:`initial conftest files
<pluginorder>` ahead of command line option parsing.
@ -206,7 +203,7 @@ def pytest_load_initial_conftests(
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
def pytest_cmdline_main(config: Config) -> ExitCode | int | None:
"""Called for performing the main command line action.
The default implementation will invoke the configure hooks and
@ -230,7 +227,7 @@ def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
@hookspec(firstresult=True)
def pytest_collection(session: "Session") -> Optional[object]:
def pytest_collection(session: Session) -> object | None:
"""Perform the collection phase for the given session.
Stops at first non-None result, see :ref:`firstresult`.
@ -272,7 +269,7 @@ def pytest_collection(session: "Session") -> Optional[object]:
def pytest_collection_modifyitems(
session: "Session", config: "Config", items: List["Item"]
session: Session, config: Config, items: list[Item]
) -> None:
"""Called after collection has been performed. May filter or re-order
the items in-place.
@ -288,7 +285,7 @@ def pytest_collection_modifyitems(
"""
def pytest_collection_finish(session: "Session") -> None:
def pytest_collection_finish(session: Session) -> None:
"""Called after collection has been performed and modified.
:param session: The pytest session object.
@ -309,8 +306,8 @@ def pytest_collection_finish(session: "Session") -> None:
},
)
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
collection_path: Path, path: LEGACY_PATH, config: Config
) -> bool | None:
"""Return ``True`` to ignore this path for collection.
Return ``None`` to let other plugins ignore the path for collection.
@ -324,6 +321,7 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`.
:param collection_path: The path to analyze.
:type collection_path: pathlib.Path
:param path: The path to analyze (deprecated).
:param config: The pytest config object.
@ -343,7 +341,7 @@ def pytest_ignore_collect(
@hookspec(firstresult=True)
def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Collector]":
def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None:
"""Create a :class:`~pytest.Collector` for the given directory, or None if
not relevant.
@ -357,6 +355,7 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
Stops at first non-None result, see :ref:`firstresult`.
:param path: The path to analyze.
:type path: pathlib.Path
See :ref:`custom directory collectors` for a simple example of use of this
hook.
@ -379,8 +378,8 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
},
)
def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
file_path: Path, path: LEGACY_PATH, parent: Collector
) -> Collector | None:
"""Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
For best results, the returned collector should be a subclass of
@ -389,6 +388,7 @@ def pytest_collect_file(
The new node needs to have the specified ``parent`` as a parent.
:param file_path: The path to analyze.
:type file_path: pathlib.Path
:param path: The path to collect (deprecated).
.. versionchanged:: 7.0.0
@ -407,7 +407,7 @@ def pytest_collect_file(
# logging hooks for collection
def pytest_collectstart(collector: "Collector") -> None:
def pytest_collectstart(collector: Collector) -> None:
"""Collector starts collecting.
:param collector:
@ -422,7 +422,7 @@ def pytest_collectstart(collector: "Collector") -> None:
"""
def pytest_itemcollected(item: "Item") -> None:
def pytest_itemcollected(item: Item) -> None:
"""We just collected a test item.
:param item:
@ -436,7 +436,7 @@ def pytest_itemcollected(item: "Item") -> None:
"""
def pytest_collectreport(report: "CollectReport") -> None:
def pytest_collectreport(report: CollectReport) -> None:
"""Collector finished collecting.
:param report:
@ -451,7 +451,7 @@ def pytest_collectreport(report: "CollectReport") -> None:
"""
def pytest_deselected(items: Sequence["Item"]) -> None:
def pytest_deselected(items: Sequence[Item]) -> None:
"""Called for deselected test items, e.g. by keyword.
May be called multiple times.
@ -467,7 +467,7 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
@hookspec(firstresult=True)
def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]":
def pytest_make_collect_report(collector: Collector) -> CollectReport | None:
"""Perform :func:`collector.collect() <pytest.Collector.collect>` and return
a :class:`~pytest.CollectReport`.
@ -499,8 +499,8 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
},
)
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
module_path: Path, path: LEGACY_PATH, parent
) -> Module | None:
"""Return a :class:`pytest.Module` collector or None for the given path.
This hook will be called for each matching test module path.
@ -510,6 +510,7 @@ def pytest_pycollect_makemodule(
Stops at first non-None result, see :ref:`firstresult`.
:param module_path: The path of the module to collect.
:type module_path: pathlib.Path
:param path: The path of the module to collect (deprecated).
.. versionchanged:: 7.0.0
@ -529,8 +530,8 @@ def pytest_pycollect_makemodule(
@hookspec(firstresult=True)
def pytest_pycollect_makeitem(
collector: Union["Module", "Class"], name: str, obj: object
) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]:
collector: Module | Class, name: str, obj: object
) -> None | Item | Collector | list[Item | Collector]:
"""Return a custom item/collector for a Python object in a module, or None.
Stops at first non-None result, see :ref:`firstresult`.
@ -554,7 +555,7 @@ def pytest_pycollect_makeitem(
@hookspec(firstresult=True)
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
"""Call underlying test function.
Stops at first non-None result, see :ref:`firstresult`.
@ -571,7 +572,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
"""
def pytest_generate_tests(metafunc: "Metafunc") -> None:
def pytest_generate_tests(metafunc: Metafunc) -> None:
"""Generate (multiple) parametrized calls to a test function.
:param metafunc:
@ -587,9 +588,7 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
@hookspec(firstresult=True)
def pytest_make_parametrize_id(
config: "Config", val: object, argname: str
) -> Optional[str]:
def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None:
"""Return a user-friendly string representation of the given ``val``
that will be used by @pytest.mark.parametrize calls, or None if the hook
doesn't know about ``val``.
@ -615,7 +614,7 @@ def pytest_make_parametrize_id(
@hookspec(firstresult=True)
def pytest_runtestloop(session: "Session") -> Optional[object]:
def pytest_runtestloop(session: Session) -> object | None:
"""Perform the main runtest loop (after collection finished).
The default hook implementation performs the runtest protocol for all items
@ -641,9 +640,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
@hookspec(firstresult=True)
def pytest_runtest_protocol(
item: "Item", nextitem: "Optional[Item]"
) -> Optional[object]:
def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> object | None:
"""Perform the runtest protocol for a single test item.
The default runtest protocol is this (see individual hooks for full details):
@ -683,9 +680,7 @@ def pytest_runtest_protocol(
"""
def pytest_runtest_logstart(
nodeid: str, location: Tuple[str, Optional[int], str]
) -> None:
def pytest_runtest_logstart(nodeid: str, location: tuple[str, int | None, str]) -> None:
"""Called at the start of running the runtest protocol for a single item.
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
@ -704,7 +699,7 @@ def pytest_runtest_logstart(
def pytest_runtest_logfinish(
nodeid: str, location: Tuple[str, Optional[int], str]
nodeid: str, location: tuple[str, int | None, str]
) -> None:
"""Called at the end of running the runtest protocol for a single item.
@ -723,7 +718,7 @@ def pytest_runtest_logfinish(
"""
def pytest_runtest_setup(item: "Item") -> None:
def pytest_runtest_setup(item: Item) -> None:
"""Called to perform the setup phase for a test item.
The default implementation runs ``setup()`` on ``item`` and all of its
@ -742,7 +737,7 @@ def pytest_runtest_setup(item: "Item") -> None:
"""
def pytest_runtest_call(item: "Item") -> None:
def pytest_runtest_call(item: Item) -> None:
"""Called to run the test for test item (the call phase).
The default implementation calls ``item.runtest()``.
@ -758,7 +753,7 @@ def pytest_runtest_call(item: "Item") -> None:
"""
def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
"""Called to perform the teardown phase for a test item.
The default implementation runs the finalizers and calls ``teardown()``
@ -783,9 +778,7 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
@hookspec(firstresult=True)
def pytest_runtest_makereport(
item: "Item", call: "CallInfo[None]"
) -> Optional["TestReport"]:
def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport | None:
"""Called to create a :class:`~pytest.TestReport` for each of
the setup, call and teardown runtest phases of a test item.
@ -804,7 +797,7 @@ def pytest_runtest_makereport(
"""
def pytest_runtest_logreport(report: "TestReport") -> None:
def pytest_runtest_logreport(report: TestReport) -> None:
"""Process the :class:`~pytest.TestReport` produced for each
of the setup, call and teardown runtest phases of an item.
@ -820,9 +813,9 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
@hookspec(firstresult=True)
def pytest_report_to_serializable(
config: "Config",
report: Union["CollectReport", "TestReport"],
) -> Optional[Dict[str, Any]]:
config: Config,
report: CollectReport | TestReport,
) -> dict[str, Any] | None:
"""Serialize the given report object into a data structure suitable for
sending over the wire, e.g. converted to JSON.
@ -839,9 +832,9 @@ def pytest_report_to_serializable(
@hookspec(firstresult=True)
def pytest_report_from_serializable(
config: "Config",
data: Dict[str, Any],
) -> Optional[Union["CollectReport", "TestReport"]]:
config: Config,
data: dict[str, Any],
) -> CollectReport | TestReport | None:
"""Restore a report object previously serialized with
:hook:`pytest_report_to_serializable`.
@ -862,8 +855,8 @@ def pytest_report_from_serializable(
@hookspec(firstresult=True)
def pytest_fixture_setup(
fixturedef: "FixtureDef[Any]", request: "SubRequest"
) -> Optional[object]:
fixturedef: FixtureDef[Any], request: SubRequest
) -> object | None:
"""Perform fixture setup execution.
:param fixturedef:
@ -890,7 +883,7 @@ def pytest_fixture_setup(
def pytest_fixture_post_finalizer(
fixturedef: "FixtureDef[Any]", request: "SubRequest"
fixturedef: FixtureDef[Any], request: SubRequest
) -> None:
"""Called after fixture teardown, but before the cache is cleared, so
the fixture result ``fixturedef.cached_result`` is still available (not
@ -915,7 +908,7 @@ def pytest_fixture_post_finalizer(
# -------------------------------------------------------------------------
def pytest_sessionstart(session: "Session") -> None:
def pytest_sessionstart(session: Session) -> None:
"""Called after the ``Session`` object has been created and before performing collection
and entering the run test loop.
@ -929,8 +922,8 @@ def pytest_sessionstart(session: "Session") -> None:
def pytest_sessionfinish(
session: "Session",
exitstatus: Union[int, "ExitCode"],
session: Session,
exitstatus: int | ExitCode,
) -> None:
"""Called after whole test run finished, right before returning the exit status to the system.
@ -944,7 +937,7 @@ def pytest_sessionfinish(
"""
def pytest_unconfigure(config: "Config") -> None:
def pytest_unconfigure(config: Config) -> None:
"""Called before test process is exited.
:param config: The pytest config object.
@ -962,8 +955,8 @@ def pytest_unconfigure(config: "Config") -> None:
def pytest_assertrepr_compare(
config: "Config", op: str, left: object, right: object
) -> Optional[List[str]]:
config: Config, op: str, left: object, right: object
) -> list[str] | None:
"""Return explanation for comparisons in failing assert expressions.
Return None for no custom explanation, otherwise return a list
@ -984,7 +977,7 @@ def pytest_assertrepr_compare(
"""
def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None:
def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None:
"""Called whenever an assertion passes.
.. versionadded:: 5.0
@ -1031,12 +1024,13 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
},
)
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
config: Config, start_path: Path, startdir: LEGACY_PATH
) -> str | list[str]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
:param config: The pytest config object.
:param start_path: The starting dir.
:type start_path: pathlib.Path
:param startdir: The starting dir (deprecated).
.. note::
@ -1066,11 +1060,11 @@ def pytest_report_header( # type:ignore[empty-body]
},
)
def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config",
config: Config,
start_path: Path,
startdir: "LEGACY_PATH",
items: Sequence["Item"],
) -> Union[str, List[str]]:
startdir: LEGACY_PATH,
items: Sequence[Item],
) -> str | list[str]:
"""Return a string or list of strings to be displayed after collection
has finished successfully.
@ -1080,6 +1074,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
:param config: The pytest config object.
:param start_path: The starting dir.
:type start_path: pathlib.Path
:param startdir: The starting dir (deprecated).
:param items: List of pytest items that are going to be executed; this list should not be modified.
@ -1104,8 +1099,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
@hookspec(firstresult=True)
def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config"
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
report: CollectReport | TestReport, config: Config
) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]:
"""Return result-category, shortletter and verbose word for status
reporting.
@ -1136,9 +1131,9 @@ def pytest_report_teststatus( # type:ignore[empty-body]
def pytest_terminal_summary(
terminalreporter: "TerminalReporter",
exitstatus: "ExitCode",
config: "Config",
terminalreporter: TerminalReporter,
exitstatus: ExitCode,
config: Config,
) -> None:
"""Add a section to terminal summary reporting.
@ -1158,10 +1153,10 @@ def pytest_terminal_summary(
@hookspec(historic=True)
def pytest_warning_recorded(
warning_message: "warnings.WarningMessage",
when: "Literal['config', 'collect', 'runtest']",
warning_message: warnings.WarningMessage,
when: Literal["config", "collect", "runtest"],
nodeid: str,
location: Optional[Tuple[str, int, str]],
location: tuple[str, int, str] | None,
) -> None:
"""Process a warning captured by the internal pytest warnings plugin.
@ -1202,8 +1197,8 @@ def pytest_warning_recorded(
def pytest_markeval_namespace( # type:ignore[empty-body]
config: "Config",
) -> Dict[str, Any]:
config: Config,
) -> dict[str, Any]:
"""Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers.
@ -1231,9 +1226,9 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
def pytest_internalerror(
excrepr: "ExceptionRepr",
excinfo: "ExceptionInfo[BaseException]",
) -> Optional[bool]:
excrepr: ExceptionRepr,
excinfo: ExceptionInfo[BaseException],
) -> bool | None:
"""Called for internal errors.
Return True to suppress the fallback handling of printing an
@ -1250,7 +1245,7 @@ def pytest_internalerror(
def pytest_keyboard_interrupt(
excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
excinfo: ExceptionInfo[KeyboardInterrupt | Exit],
) -> None:
"""Called for keyboard interrupt.
@ -1264,9 +1259,9 @@ def pytest_keyboard_interrupt(
def pytest_exception_interact(
node: Union["Item", "Collector"],
call: "CallInfo[Any]",
report: Union["CollectReport", "TestReport"],
node: Item | Collector,
call: CallInfo[Any],
report: CollectReport | TestReport,
) -> None:
"""Called when an exception was raised which can potentially be
interactively handled.
@ -1295,7 +1290,7 @@ def pytest_exception_interact(
"""
def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
def pytest_enter_pdb(config: Config, pdb: pdb.Pdb) -> None:
"""Called upon pdb.set_trace().
Can be used by plugins to take special action just before the python
@ -1311,7 +1306,7 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
"""
def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
def pytest_leave_pdb(config: Config, pdb: pdb.Pdb) -> None:
"""Called when leaving pdb (e.g. with continue after pdb.set_trace()).
Can be used by plugins to take special action just after the python

View File

@ -8,18 +8,15 @@ Output conforms to
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
"""
from __future__ import annotations
from datetime import datetime
import functools
import os
import platform
import re
from typing import Callable
from typing import Dict
from typing import List
from typing import Match
from typing import Optional
from typing import Tuple
from typing import Union
import xml.etree.ElementTree as ET
from _pytest import nodes
@ -89,15 +86,15 @@ families["xunit2"] = families["_base"]
class _NodeReporter:
def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None:
def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None:
self.id = nodeid
self.xml = xml
self.add_stats = self.xml.add_stats
self.family = self.xml.family
self.duration = 0.0
self.properties: List[Tuple[str, str]] = []
self.nodes: List[ET.Element] = []
self.attrs: Dict[str, str] = {}
self.properties: list[tuple[str, str]] = []
self.nodes: list[ET.Element] = []
self.attrs: dict[str, str] = {}
def append(self, node: ET.Element) -> None:
self.xml.add_stats(node.tag)
@ -109,7 +106,7 @@ class _NodeReporter:
def add_attribute(self, name: str, value: object) -> None:
self.attrs[str(name)] = bin_xml_escape(value)
def make_properties_node(self) -> Optional[ET.Element]:
def make_properties_node(self) -> ET.Element | None:
"""Return a Junit node containing custom properties, if any."""
if self.properties:
properties = ET.Element("properties")
@ -124,7 +121,7 @@ class _NodeReporter:
classnames = names[:-1]
if self.xml.prefix:
classnames.insert(0, self.xml.prefix)
attrs: Dict[str, str] = {
attrs: dict[str, str] = {
"classname": ".".join(classnames),
"name": bin_xml_escape(names[-1]),
"file": testreport.location[0],
@ -156,7 +153,7 @@ class _NodeReporter:
testcase.extend(self.nodes)
return testcase
def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None:
def _add_simple(self, tag: str, message: str, data: str | None = None) -> None:
node = ET.Element(tag, message=message)
node.text = bin_xml_escape(data)
self.append(node)
@ -201,7 +198,7 @@ class _NodeReporter:
self._add_simple("skipped", "xfail-marked test passes unexpectedly")
else:
assert report.longrepr is not None
reprcrash: Optional[ReprFileLocation] = getattr(
reprcrash: ReprFileLocation | None = getattr(
report.longrepr, "reprcrash", None
)
if reprcrash is not None:
@ -221,9 +218,7 @@ class _NodeReporter:
def append_error(self, report: TestReport) -> None:
assert report.longrepr is not None
reprcrash: Optional[ReprFileLocation] = getattr(
report.longrepr, "reprcrash", None
)
reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None)
if reprcrash is not None:
reason = reprcrash.message
else:
@ -451,7 +446,7 @@ def pytest_unconfigure(config: Config) -> None:
config.pluginmanager.unregister(xml)
def mangle_test_address(address: str) -> List[str]:
def mangle_test_address(address: str) -> list[str]:
path, possible_open_bracket, params = address.partition("[")
names = path.split("::")
# Convert file path to dotted path.
@ -466,7 +461,7 @@ class LogXML:
def __init__(
self,
logfile,
prefix: Optional[str],
prefix: str | None,
suite_name: str = "pytest",
logging: str = "no",
report_duration: str = "total",
@ -481,17 +476,15 @@ class LogXML:
self.log_passing_tests = log_passing_tests
self.report_duration = report_duration
self.family = family
self.stats: Dict[str, int] = dict.fromkeys(
self.stats: dict[str, int] = dict.fromkeys(
["error", "passed", "failure", "skipped"], 0
)
self.node_reporters: Dict[
Tuple[Union[str, TestReport], object], _NodeReporter
] = {}
self.node_reporters_ordered: List[_NodeReporter] = []
self.global_properties: List[Tuple[str, str]] = []
self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {}
self.node_reporters_ordered: list[_NodeReporter] = []
self.global_properties: list[tuple[str, str]] = []
# List of reports that failed on call but teardown is pending.
self.open_reports: List[TestReport] = []
self.open_reports: list[TestReport] = []
self.cnt_double_fail_tests = 0
# Replaces convenience family with real family.
@ -510,8 +503,8 @@ class LogXML:
if reporter is not None:
reporter.finalize()
def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter:
nodeid: Union[str, TestReport] = getattr(report, "nodeid", report)
def node_reporter(self, report: TestReport | str) -> _NodeReporter:
nodeid: str | TestReport = getattr(report, "nodeid", report)
# Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
@ -691,7 +684,7 @@ class LogXML:
_check_record_param_type("name", name)
self.global_properties.append((name, bin_xml_escape(value)))
def _get_global_properties_node(self) -> Optional[ET.Element]:
def _get_global_properties_node(self) -> ET.Element | None:
"""Return a Junit node containing custom properties, if any."""
if self.global_properties:
properties = ET.Element("properties")

View File

@ -1,16 +1,15 @@
# mypy: allow-untyped-defs
"""Add backward compatibility support for the legacy py path type."""
from __future__ import annotations
import dataclasses
from pathlib import Path
import shlex
import subprocess
from typing import Final
from typing import final
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from iniconfig import SectionWrapper
@ -50,8 +49,8 @@ class Testdir:
__test__ = False
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
TimeoutExpired: "Final" = Pytester.TimeoutExpired
CLOSE_STDIN: Final = Pytester.CLOSE_STDIN
TimeoutExpired: Final = Pytester.TimeoutExpired
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
@ -145,7 +144,7 @@ class Testdir:
"""See :meth:`Pytester.copy_example`."""
return legacy_path(self._pytester.copy_example(name))
def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
def getnode(self, config: Config, arg) -> Item | Collector | None:
"""See :meth:`Pytester.getnode`."""
return self._pytester.getnode(config, arg)
@ -153,7 +152,7 @@ class Testdir:
"""See :meth:`Pytester.getpathnode`."""
return self._pytester.getpathnode(path)
def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
def genitems(self, colitems: list[Item | Collector]) -> list[Item]:
"""See :meth:`Pytester.genitems`."""
return self._pytester.genitems(colitems)
@ -205,9 +204,7 @@ class Testdir:
source, configargs=configargs, withinit=withinit
)
def collect_by_name(
self, modcol: Collector, name: str
) -> Optional[Union[Item, Collector]]:
def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None:
"""See :meth:`Pytester.collect_by_name`."""
return self._pytester.collect_by_name(modcol, name)
@ -238,13 +235,11 @@ class Testdir:
"""See :meth:`Pytester.runpytest_subprocess`."""
return self._pytester.runpytest_subprocess(*args, timeout=timeout)
def spawn_pytest(
self, string: str, expect_timeout: float = 10.0
) -> "pexpect.spawn":
def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn:
"""See :meth:`Pytester.spawn_pytest`."""
return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn:
"""See :meth:`Pytester.spawn`."""
return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
@ -374,7 +369,7 @@ def Config_rootdir(self: Config) -> LEGACY_PATH:
return legacy_path(str(self.rootpath))
def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
def Config_inifile(self: Config) -> LEGACY_PATH | None:
"""The path to the :ref:`configfile <configfiles>`.
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
@ -394,9 +389,7 @@ def Session_startdir(self: Session) -> LEGACY_PATH:
return legacy_path(self.startpath)
def Config__getini_unknown_type(
self, name: str, type: str, value: Union[str, List[str]]
):
def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]):
if type == "pathlist":
# TODO: This assert is probably not valid in all cases.
assert self.inipath is not None

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Access and control log capturing."""
from __future__ import annotations
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
@ -22,12 +24,8 @@ from typing import Generic
from typing import List
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from _pytest import nodes
from _pytest._io import TerminalWriter
@ -68,7 +66,7 @@ class DatetimeFormatter(logging.Formatter):
:func:`time.strftime` in case of microseconds in format string.
"""
def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str:
def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str:
if datefmt and "%f" in datefmt:
ct = self.converter(record.created)
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
@ -100,7 +98,7 @@ class ColoredLevelFormatter(DatetimeFormatter):
super().__init__(*args, **kwargs)
self._terminalwriter = terminalwriter
self._original_fmt = self._style._fmt
self._level_to_fmt_mapping: Dict[int, str] = {}
self._level_to_fmt_mapping: dict[int, str] = {}
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
self.add_color_level(level, *color_opts)
@ -148,12 +146,12 @@ class PercentStyleMultiline(logging.PercentStyle):
formats the message as if each line were logged separately.
"""
def __init__(self, fmt: str, auto_indent: Union[int, str, bool, None]) -> None:
def __init__(self, fmt: str, auto_indent: int | str | bool | None) -> None:
super().__init__(fmt)
self._auto_indent = self._get_auto_indent(auto_indent)
@staticmethod
def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int:
def _get_auto_indent(auto_indent_option: int | str | bool | None) -> int:
"""Determine the current auto indentation setting.
Specify auto indent behavior (on/off/fixed) by passing in
@ -348,7 +346,7 @@ class catching_logs(Generic[_HandlerType]):
__slots__ = ("handler", "level", "orig_level")
def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None:
def __init__(self, handler: _HandlerType, level: int | None = None) -> None:
self.handler = handler
self.level = level
@ -364,9 +362,9 @@ class catching_logs(Generic[_HandlerType]):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
root_logger = logging.getLogger()
if self.level is not None:
@ -380,7 +378,7 @@ class LogCaptureHandler(logging_StreamHandler):
def __init__(self) -> None:
"""Create a new log handler."""
super().__init__(StringIO())
self.records: List[logging.LogRecord] = []
self.records: list[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
"""Keep the log records in a list in addition to the log text."""
@ -411,10 +409,10 @@ class LogCaptureFixture:
def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._item = item
self._initial_handler_level: Optional[int] = None
self._initial_handler_level: int | None = None
# Dict of log name -> log level.
self._initial_logger_levels: Dict[Optional[str], int] = {}
self._initial_disabled_logging_level: Optional[int] = None
self._initial_logger_levels: dict[str | None, int] = {}
self._initial_disabled_logging_level: int | None = None
def _finalize(self) -> None:
"""Finalize the fixture.
@ -439,7 +437,7 @@ class LogCaptureFixture:
def get_records(
self, when: Literal["setup", "call", "teardown"]
) -> List[logging.LogRecord]:
) -> list[logging.LogRecord]:
"""Get the logging records for one of the possible test phases.
:param when:
@ -458,12 +456,12 @@ class LogCaptureFixture:
return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
@property
def records(self) -> List[logging.LogRecord]:
def records(self) -> list[logging.LogRecord]:
"""The list of log records."""
return self.handler.records
@property
def record_tuples(self) -> List[Tuple[str, int, str]]:
def record_tuples(self) -> list[tuple[str, int, str]]:
"""A list of a stripped down version of log records intended
for use in assertion comparison.
@ -474,7 +472,7 @@ class LogCaptureFixture:
return [(r.name, r.levelno, r.getMessage()) for r in self.records]
@property
def messages(self) -> List[str]:
def messages(self) -> list[str]:
"""A list of format-interpolated log messages.
Unlike 'records', which contains the format string and parameters for
@ -497,7 +495,7 @@ class LogCaptureFixture:
self.handler.clear()
def _force_enable_logging(
self, level: Union[int, str], logger_obj: logging.Logger
self, level: int | str, logger_obj: logging.Logger
) -> int:
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
@ -530,7 +528,7 @@ class LogCaptureFixture:
return original_disable_level
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
def set_level(self, level: int | str, logger: str | None = None) -> None:
"""Set the threshold level of a logger for the duration of a test.
Logging messages which are less severe than this level will not be captured.
@ -557,7 +555,7 @@ class LogCaptureFixture:
@contextmanager
def at_level(
self, level: Union[int, str], logger: Optional[str] = None
self, level: int | str, logger: str | None = None
) -> Generator[None, None, None]:
"""Context manager that sets the level for capturing of logs. After
the end of the 'with' statement the level is restored to its original
@ -615,7 +613,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
result._finalize()
def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]:
def get_log_level_for_setting(config: Config, *setting_names: str) -> int | None:
for setting_name in setting_names:
log_level = config.getoption(setting_name)
if log_level is None:
@ -701,9 +699,9 @@ class LoggingPlugin:
assert terminal_reporter is not None
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler: Union[
_LiveLoggingStreamHandler, _LiveLoggingNullHandler
] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
self.log_cli_handler: (
_LiveLoggingStreamHandler | _LiveLoggingNullHandler
) = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
else:
self.log_cli_handler = _LiveLoggingNullHandler()
log_cli_formatter = self._create_formatter(
@ -714,7 +712,7 @@ class LoggingPlugin:
self.log_cli_handler.setFormatter(log_cli_formatter)
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
def _disable_loggers(self, loggers_to_disable: list[str]) -> None:
if not loggers_to_disable:
return
@ -839,7 +837,7 @@ class LoggingPlugin:
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("setup")
empty: Dict[str, List[logging.LogRecord]] = {}
empty: dict[str, list[logging.LogRecord]] = {}
item.stash[caplog_records_key] = empty
yield from self._runtest_for(item, "setup")
@ -902,7 +900,7 @@ class _LiveLoggingStreamHandler(logging_StreamHandler):
def __init__(
self,
terminal_reporter: TerminalReporter,
capture_manager: Optional[CaptureManager],
capture_manager: CaptureManager | None,
) -> None:
super().__init__(stream=terminal_reporter) # type: ignore[arg-type]
self.capture_manager = capture_manager
@ -914,7 +912,7 @@ class _LiveLoggingStreamHandler(logging_StreamHandler):
"""Reset the handler; should be called before the start of each test."""
self._first_record_emitted = False
def set_when(self, when: Optional[str]) -> None:
def set_when(self, when: str | None) -> None:
"""Prepare for the given test phase (setup/call/teardown)."""
self._when = when
self._section_name_shown = False

View File

@ -1,5 +1,7 @@
"""Core implementation of the testing process: init, session, runtest loop."""
from __future__ import annotations
import argparse
import dataclasses
import fnmatch
@ -13,17 +15,12 @@ from typing import AbstractSet
from typing import Callable
from typing import Dict
from typing import final
from typing import FrozenSet
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Literal
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import warnings
import pluggy
@ -271,8 +268,8 @@ def validate_basetemp(path: str) -> str:
def wrap_session(
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
) -> Union[int, ExitCode]:
config: Config, doit: Callable[[Config, Session], int | ExitCode | None]
) -> int | ExitCode:
"""Skeleton command line program."""
session = Session.from_config(config)
session.exitstatus = ExitCode.OK
@ -291,7 +288,7 @@ def wrap_session(
session.exitstatus = ExitCode.TESTS_FAILED
except (KeyboardInterrupt, exit.Exception):
excinfo = _pytest._code.ExceptionInfo.from_current()
exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED
exitstatus: int | ExitCode = ExitCode.INTERRUPTED
if isinstance(excinfo.value, exit.Exception):
if excinfo.value.returncode is not None:
exitstatus = excinfo.value.returncode
@ -329,11 +326,11 @@ def wrap_session(
return session.exitstatus
def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]:
def pytest_cmdline_main(config: Config) -> int | ExitCode:
return wrap_session(config, _main)
def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
def _main(config: Config, session: Session) -> int | ExitCode | None:
"""Default command line protocol for initialization, session,
running tests and reporting."""
config.hook.pytest_collection(session=session)
@ -346,11 +343,11 @@ def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]:
return None
def pytest_collection(session: "Session") -> None:
def pytest_collection(session: Session) -> None:
session.perform_collect()
def pytest_runtestloop(session: "Session") -> bool:
def pytest_runtestloop(session: Session) -> bool:
if session.testsfailed and not session.config.option.continue_on_collection_errors:
raise session.Interrupted(
"%d error%s during collection"
@ -390,7 +387,7 @@ def _in_venv(path: Path) -> bool:
return any(fname.name in activates for fname in bindir.iterdir())
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
if collection_path.name == "__pycache__":
return True
@ -430,11 +427,11 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
def pytest_collect_directory(
path: Path, parent: nodes.Collector
) -> Optional[nodes.Collector]:
) -> nodes.Collector | None:
return Dir.from_parent(parent, path=path)
def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None:
def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None:
deselect_prefixes = tuple(config.getoption("deselect") or [])
if not deselect_prefixes:
return
@ -508,17 +505,18 @@ class Dir(nodes.Directory):
parent: nodes.Collector,
*,
path: Path,
) -> "Self":
) -> Self:
"""The public constructor.
:param parent: The parent collector of this Dir.
:param path: The directory's path.
:type path: pathlib.Path
"""
return super().from_parent(parent=parent, path=path)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
config = self.config
col: Optional[nodes.Collector]
col: nodes.Collector | None
cols: Sequence[nodes.Collector]
ihook = self.ihook
for direntry in scandir(self.path):
@ -552,8 +550,8 @@ class Session(nodes.Collector):
# Set on the session by runner.pytest_sessionstart.
_setupstate: SetupState
# Set on the session by fixtures.pytest_sessionstart.
_fixturemanager: "FixtureManager"
exitstatus: Union[int, ExitCode]
_fixturemanager: FixtureManager
exitstatus: int | ExitCode
def __init__(self, config: Config) -> None:
super().__init__(
@ -567,22 +565,22 @@ class Session(nodes.Collector):
)
self.testsfailed = 0
self.testscollected = 0
self._shouldstop: Union[bool, str] = False
self._shouldfail: Union[bool, str] = False
self._shouldstop: bool | str = False
self._shouldfail: bool | str = False
self.trace = config.trace.root.get("collection")
self._initialpaths: FrozenSet[Path] = frozenset()
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
self._initial_parts: List[CollectionArgument] = []
self._collection_cache: Dict[nodes.Collector, CollectReport] = {}
self.items: List[nodes.Item] = []
self._initialpaths: frozenset[Path] = frozenset()
self._initialpaths_with_parents: frozenset[Path] = frozenset()
self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = []
self._initial_parts: list[CollectionArgument] = []
self._collection_cache: dict[nodes.Collector, CollectReport] = {}
self.items: list[nodes.Item] = []
self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath)
self.config.pluginmanager.register(self, name="session")
@classmethod
def from_config(cls, config: Config) -> "Session":
def from_config(cls, config: Config) -> Session:
session: Session = cls._create(config=config)
return session
@ -596,11 +594,11 @@ class Session(nodes.Collector):
)
@property
def shouldstop(self) -> Union[bool, str]:
def shouldstop(self) -> bool | str:
return self._shouldstop
@shouldstop.setter
def shouldstop(self, value: Union[bool, str]) -> None:
def shouldstop(self, value: bool | str) -> None:
# The runner checks shouldfail and assumes that if it is set we are
# definitely stopping, so prevent unsetting it.
if value is False and self._shouldstop:
@ -614,11 +612,11 @@ class Session(nodes.Collector):
self._shouldstop = value
@property
def shouldfail(self) -> Union[bool, str]:
def shouldfail(self) -> bool | str:
return self._shouldfail
@shouldfail.setter
def shouldfail(self, value: Union[bool, str]) -> None:
def shouldfail(self, value: bool | str) -> None:
# The runner checks shouldfail and assumes that if it is set we are
# definitely stopping, so prevent unsetting it.
if value is False and self._shouldfail:
@ -651,9 +649,7 @@ class Session(nodes.Collector):
raise self.Interrupted(self.shouldstop)
@hookimpl(tryfirst=True)
def pytest_runtest_logreport(
self, report: Union[TestReport, CollectReport]
) -> None:
def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None:
if report.failed and not hasattr(report, "wasxfail"):
self.testsfailed += 1
maxfail = self.config.getvalue("maxfail")
@ -664,7 +660,7 @@ class Session(nodes.Collector):
def isinitpath(
self,
path: Union[str, "os.PathLike[str]"],
path: str | os.PathLike[str],
*,
with_parents: bool = False,
) -> bool:
@ -686,7 +682,7 @@ class Session(nodes.Collector):
else:
return path_ in self._initialpaths
def gethookproxy(self, fspath: "os.PathLike[str]") -> pluggy.HookRelay:
def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay:
# Optimization: Path(Path(...)) is much slower than isinstance.
path = fspath if isinstance(fspath, Path) else Path(fspath)
pm = self.config.pluginmanager
@ -706,7 +702,7 @@ class Session(nodes.Collector):
def _collect_path(
self,
path: Path,
path_cache: Dict[Path, Sequence[nodes.Collector]],
path_cache: dict[Path, Sequence[nodes.Collector]],
) -> Sequence[nodes.Collector]:
"""Create a Collector for the given path.
@ -718,7 +714,7 @@ class Session(nodes.Collector):
if path.is_dir():
ihook = self.gethookproxy(path.parent)
col: Optional[nodes.Collector] = ihook.pytest_collect_directory(
col: nodes.Collector | None = ihook.pytest_collect_directory(
path=path, parent=self
)
cols: Sequence[nodes.Collector] = (col,) if col is not None else ()
@ -736,17 +732,17 @@ class Session(nodes.Collector):
@overload
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
self, args: Sequence[str] | None = ..., genitems: Literal[True] = ...
) -> Sequence[nodes.Item]: ...
@overload
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ...
self, args: Sequence[str] | None = ..., genitems: bool = ...
) -> Sequence[nodes.Item | nodes.Collector]: ...
def perform_collect(
self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
self, args: Sequence[str] | None = None, genitems: bool = True
) -> Sequence[nodes.Item | nodes.Collector]:
"""Perform the collection phase for this session.
This is called by the default :hook:`pytest_collection` hook
@ -772,10 +768,10 @@ class Session(nodes.Collector):
self._initial_parts = []
self._collection_cache = {}
self.items = []
items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items
items: Sequence[nodes.Item | nodes.Collector] = self.items
try:
initialpaths: List[Path] = []
initialpaths_with_parents: List[Path] = []
initialpaths: list[Path] = []
initialpaths_with_parents: list[Path] = []
for arg in args:
collection_argument = resolve_collection_argument(
self.config.invocation_params.dir,
@ -830,7 +826,7 @@ class Session(nodes.Collector):
self,
node: nodes.Collector,
handle_dupes: bool = True,
) -> Tuple[CollectReport, bool]:
) -> tuple[CollectReport, bool]:
if node in self._collection_cache and handle_dupes:
rep = self._collection_cache[node]
return rep, True
@ -839,11 +835,11 @@ class Session(nodes.Collector):
self._collection_cache[node] = rep
return rep, False
def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterator[nodes.Item | nodes.Collector]:
# This is a cache for the root directories of the initial paths.
# We can't use collection_cache for Session because of its special
# role as the bootstrapping collector.
path_cache: Dict[Path, Sequence[nodes.Collector]] = {}
path_cache: dict[Path, Sequence[nodes.Collector]] = {}
pm = self.config.pluginmanager
@ -881,9 +877,9 @@ class Session(nodes.Collector):
# and discarding all nodes which don't match the level's part.
any_matched_in_initial_part = False
notfound_collectors = []
work: List[
Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]]
] = [(self, [*paths, *names])]
work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [
(self, [*paths, *names])
]
while work:
matchnode, matchparts = work.pop()
@ -900,7 +896,7 @@ class Session(nodes.Collector):
# Collect this level of matching.
# Collecting Session (self) is done directly to avoid endless
# recursion to this function.
subnodes: Sequence[Union[nodes.Collector, nodes.Item]]
subnodes: Sequence[nodes.Collector | nodes.Item]
if isinstance(matchnode, Session):
assert isinstance(matchparts[0], Path)
subnodes = matchnode._collect_path(matchparts[0], path_cache)
@ -960,9 +956,7 @@ class Session(nodes.Collector):
self.trace.root.indent -= 1
def genitems(
self, node: Union[nodes.Item, nodes.Collector]
) -> Iterator[nodes.Item]:
def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]:
self.trace("genitems", node)
if isinstance(node, nodes.Item):
node.ihook.pytest_itemcollected(item=node)
@ -982,7 +976,7 @@ class Session(nodes.Collector):
node.ihook.pytest_collectreport(report=rep)
def search_pypath(module_name: str) -> Optional[str]:
def search_pypath(module_name: str) -> str | None:
"""Search sys.path for the given a dotted module name, and return its file
system path if found."""
try:
@ -1006,7 +1000,7 @@ class CollectionArgument:
path: Path
parts: Sequence[str]
module_name: Optional[str]
module_name: str | None
def resolve_collection_argument(

View File

@ -1,12 +1,12 @@
"""Generic mechanism for marking and selecting python functions."""
from __future__ import annotations
import dataclasses
from typing import AbstractSet
from typing import Collection
from typing import List
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from .expression import Expression
from .expression import ParseError
@ -44,8 +44,8 @@ old_mark_config_key = StashKey[Optional[Config]]()
def param(
*values: object,
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
id: Optional[str] = None,
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
id: str | None = None,
) -> ParameterSet:
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
@ -112,7 +112,7 @@ def pytest_addoption(parser: Parser) -> None:
@hookimpl(tryfirst=True)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
import _pytest.config
if config.option.markers:
@ -151,7 +151,7 @@ class KeywordMatcher:
_names: AbstractSet[str]
@classmethod
def from_item(cls, item: "Item") -> "KeywordMatcher":
def from_item(cls, item: Item) -> KeywordMatcher:
mapped_names = set()
# Add the names of the current item and any parent items,
@ -191,7 +191,7 @@ class KeywordMatcher:
return False
def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
def deselect_by_keyword(items: list[Item], config: Config) -> None:
keywordexpr = config.option.keyword.lstrip()
if not keywordexpr:
return
@ -223,7 +223,7 @@ class MarkMatcher:
own_mark_names: AbstractSet[str]
@classmethod
def from_item(cls, item: "Item") -> "MarkMatcher":
def from_item(cls, item: Item) -> MarkMatcher:
mark_names = {mark.name for mark in item.iter_markers()}
return cls(mark_names)
@ -231,14 +231,14 @@ class MarkMatcher:
return name in self.own_mark_names
def deselect_by_mark(items: "List[Item]", config: Config) -> None:
def deselect_by_mark(items: list[Item], config: Config) -> None:
matchexpr = config.option.markexpr
if not matchexpr:
return
expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
remaining: List[Item] = []
deselected: List[Item] = []
remaining: list[Item] = []
deselected: list[Item] = []
for item in items:
if expr.evaluate(MarkMatcher.from_item(item)):
remaining.append(item)
@ -256,7 +256,7 @@ def _parse_expression(expr: str, exc_message: str) -> Expression:
raise UsageError(f"{exc_message}: {expr}: {e}") from None
def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:
def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
deselect_by_keyword(items, config)
deselect_by_mark(items, config)

View File

@ -15,6 +15,8 @@ The semantics are:
- or/and/not evaluate according to the usual boolean semantics.
"""
from __future__ import annotations
import ast
import dataclasses
import enum
@ -24,7 +26,6 @@ from typing import Callable
from typing import Iterator
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Sequence
@ -105,7 +106,7 @@ class Scanner:
)
yield Token(TokenType.EOF, "", pos)
def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]:
def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
if self.current.type is type:
token = self.current
if token.type is not TokenType.EOF:
@ -197,7 +198,7 @@ class Expression:
self.code = code
@classmethod
def compile(self, input: str) -> "Expression":
def compile(self, input: str) -> Expression:
"""Compile a match expression.
:param input: The input expression - one line.

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import collections.abc
import dataclasses
import inspect
@ -8,16 +10,11 @@ from typing import Collection
from typing import final
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import NamedTuple
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
@ -48,7 +45,7 @@ def istestfunc(func) -> bool:
def get_empty_parameterset_mark(
config: Config, argnames: Sequence[str], func
) -> "MarkDecorator":
) -> MarkDecorator:
from ..nodes import Collector
fs, lineno = getfslineno(func)
@ -76,17 +73,17 @@ def get_empty_parameterset_mark(
class ParameterSet(NamedTuple):
values: Sequence[Union[object, NotSetType]]
marks: Collection[Union["MarkDecorator", "Mark"]]
id: Optional[str]
values: Sequence[object | NotSetType]
marks: Collection[MarkDecorator | Mark]
id: str | None
@classmethod
def param(
cls,
*values: object,
marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
id: Optional[str] = None,
) -> "ParameterSet":
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
id: str | None = None,
) -> ParameterSet:
if isinstance(marks, MarkDecorator):
marks = (marks,)
else:
@ -101,9 +98,9 @@ class ParameterSet(NamedTuple):
@classmethod
def extract_from(
cls,
parameterset: Union["ParameterSet", Sequence[object], object],
parameterset: ParameterSet | Sequence[object] | object,
force_tuple: bool = False,
) -> "ParameterSet":
) -> ParameterSet:
"""Extract from an object or objects.
:param parameterset:
@ -128,11 +125,11 @@ class ParameterSet(NamedTuple):
@staticmethod
def _parse_parametrize_args(
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
*args,
**kwargs,
) -> Tuple[Sequence[str], bool]:
) -> tuple[Sequence[str], bool]:
if isinstance(argnames, str):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
@ -142,9 +139,9 @@ class ParameterSet(NamedTuple):
@staticmethod
def _parse_parametrize_parameters(
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
force_tuple: bool,
) -> List["ParameterSet"]:
) -> list[ParameterSet]:
return [
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
]
@ -152,12 +149,12 @@ class ParameterSet(NamedTuple):
@classmethod
def _for_parametrize(
cls,
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
func,
config: Config,
nodeid: str,
) -> Tuple[Sequence[str], List["ParameterSet"]]:
) -> tuple[Sequence[str], list[ParameterSet]]:
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
del argvalues
@ -200,24 +197,24 @@ class Mark:
#: Name of the mark.
name: str
#: Positional arguments of the mark decorator.
args: Tuple[Any, ...]
args: tuple[Any, ...]
#: Keyword arguments of the mark decorator.
kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks.
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
_param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
#: Resolved/generated ids with parametrize Marks.
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
_param_ids_generated: Sequence[str] | None = dataclasses.field(
default=None, repr=False
)
def __init__(
self,
name: str,
args: Tuple[Any, ...],
args: tuple[Any, ...],
kwargs: Mapping[str, Any],
param_ids_from: Optional["Mark"] = None,
param_ids_generated: Optional[Sequence[str]] = None,
param_ids_from: Mark | None = None,
param_ids_generated: Sequence[str] | None = None,
*,
_ispytest: bool = False,
) -> None:
@ -233,7 +230,7 @@ class Mark:
def _has_param_ids(self) -> bool:
return "ids" in self.kwargs or len(self.args) >= 4
def combined_with(self, other: "Mark") -> "Mark":
def combined_with(self, other: Mark) -> Mark:
"""Return a new Mark which is a combination of this
Mark and another Mark.
@ -245,7 +242,7 @@ class Mark:
assert self.name == other.name
# Remember source of ids with parametrize Marks.
param_ids_from: Optional[Mark] = None
param_ids_from: Mark | None = None
if self.name == "parametrize":
if other._has_param_ids():
param_ids_from = other
@ -316,7 +313,7 @@ class MarkDecorator:
return self.mark.name
@property
def args(self) -> Tuple[Any, ...]:
def args(self) -> tuple[Any, ...]:
"""Alias for mark.args."""
return self.mark.args
@ -330,7 +327,7 @@ class MarkDecorator:
""":meta private:"""
return self.name # for backward-compat (2.4.1 had this attr)
def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
"""Return a MarkDecorator with extra arguments added.
Unlike calling the MarkDecorator, with_args() can be used even
@ -347,7 +344,7 @@ class MarkDecorator:
pass
@overload
def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
pass
def __call__(self, *args: object, **kwargs: object):
@ -362,10 +359,10 @@ class MarkDecorator:
def get_unpacked_marks(
obj: Union[object, type],
obj: object | type,
*,
consider_mro: bool = True,
) -> List[Mark]:
) -> list[Mark]:
"""Obtain the unpacked marks that are stored on an object.
If obj is a class and consider_mro is true, return marks applied to
@ -395,7 +392,7 @@ def get_unpacked_marks(
def normalize_mark_list(
mark_list: Iterable[Union[Mark, MarkDecorator]],
mark_list: Iterable[Mark | MarkDecorator],
) -> Iterable[Mark]:
"""
Normalize an iterable of Mark or MarkDecorator objects into a list of marks
@ -437,13 +434,13 @@ if TYPE_CHECKING:
def __call__(self, arg: Markable) -> Markable: ...
@overload
def __call__(self, reason: str = ...) -> "MarkDecorator": ...
def __call__(self, reason: str = ...) -> MarkDecorator: ...
class _SkipifMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self,
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
condition: str | bool = ...,
*conditions: str | bool,
reason: str = ...,
) -> MarkDecorator: ...
@ -454,30 +451,25 @@ if TYPE_CHECKING:
@overload
def __call__(
self,
condition: Union[str, bool] = False,
*conditions: Union[str, bool],
condition: str | bool = False,
*conditions: str | bool,
reason: str = ...,
run: bool = ...,
raises: Union[
None, Type[BaseException], Tuple[Type[BaseException], ...]
] = ...,
raises: None | type[BaseException] | tuple[type[BaseException], ...] = ...,
strict: bool = ...,
) -> MarkDecorator: ...
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self,
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
*,
indirect: Union[bool, Sequence[str]] = ...,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = ...,
scope: Optional[_ScopeName] = ...,
indirect: bool | Sequence[str] = ...,
ids: Iterable[None | str | float | int | bool]
| Callable[[Any], object | None]
| None = ...,
scope: _ScopeName | None = ...,
) -> MarkDecorator: ...
class _UsefixturesMarkDecorator(MarkDecorator):
@ -517,8 +509,8 @@ class MarkGenerator:
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._config: Optional[Config] = None
self._markers: Set[str] = set()
self._config: Config | None = None
self._markers: set[str] = set()
def __getattr__(self, name: str) -> MarkDecorator:
"""Generate a new :class:`MarkDecorator` with the given name."""
@ -569,7 +561,7 @@ MARK_GEN = MarkGenerator(_ispytest=True)
class NodeKeywords(MutableMapping[str, Any]):
__slots__ = ("node", "parent", "_markers")
def __init__(self, node: "Node") -> None:
def __init__(self, node: Node) -> None:
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
@ -597,7 +589,7 @@ class NodeKeywords(MutableMapping[str, Any]):
def update( # type: ignore[override]
self,
other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
**kwds: Any,
) -> None:
self._markers.update(other)

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Monkeypatching and mocking functionality."""
from __future__ import annotations
from contextlib import contextmanager
import os
import re
@ -8,14 +10,10 @@ import sys
from typing import Any
from typing import final
from typing import Generator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import overload
from typing import Tuple
from typing import TypeVar
from typing import Union
import warnings
from _pytest.fixtures import fixture
@ -30,7 +28,7 @@ V = TypeVar("V")
@fixture
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
def monkeypatch() -> Generator[MonkeyPatch, None, None]:
"""A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@ -97,7 +95,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
return obj
def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]:
def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]:
if not isinstance(import_path, str) or "." not in import_path:
raise TypeError(f"must be absolute import path string, not {import_path!r}")
module, attr = import_path.rsplit(".", 1)
@ -130,14 +128,14 @@ class MonkeyPatch:
"""
def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
self._setattr: list[tuple[object, str, object]] = []
self._setitem: list[tuple[Mapping[Any, Any], object, object]] = []
self._cwd: str | None = None
self._savesyspath: list[str] | None = None
@classmethod
@contextmanager
def context(cls) -> Generator["MonkeyPatch", None, None]:
def context(cls) -> Generator[MonkeyPatch, None, None]:
"""Context manager that returns a new :class:`MonkeyPatch` object
which undoes any patching done inside the ``with`` block upon exit.
@ -182,8 +180,8 @@ class MonkeyPatch:
def setattr(
self,
target: Union[str, object],
name: Union[object, str],
target: str | object,
name: object | str,
value: object = notset,
raising: bool = True,
) -> None:
@ -254,8 +252,8 @@ class MonkeyPatch:
def delattr(
self,
target: Union[object, str],
name: Union[str, Notset] = notset,
target: object | str,
name: str | Notset = notset,
raising: bool = True,
) -> None:
"""Delete attribute ``name`` from ``target``.
@ -310,7 +308,7 @@ class MonkeyPatch:
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dic[name] # type: ignore[attr-defined]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
def setenv(self, name: str, value: str, prepend: str | None = None) -> None:
"""Set environment variable ``name`` to ``value``.
If ``prepend`` is a character, read the current environment variable
@ -363,7 +361,7 @@ class MonkeyPatch:
invalidate_caches()
def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None:
def chdir(self, path: str | os.PathLike[str]) -> None:
"""Change the current working directory to the specified path.
:param path:

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import abc
from functools import cached_property
from inspect import signature
@ -10,17 +12,11 @@ from typing import Callable
from typing import cast
from typing import Iterable
from typing import Iterator
from typing import List
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import Set
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
import pluggy
@ -62,9 +58,9 @@ _T = TypeVar("_T")
def _imply_path(
node_type: Type["Node"],
path: Optional[Path],
fspath: Optional[LEGACY_PATH],
node_type: type[Node],
path: Path | None,
fspath: LEGACY_PATH | None,
) -> Path:
if fspath is not None:
warnings.warn(
@ -109,7 +105,7 @@ class NodeMeta(abc.ABCMeta):
).format(name=f"{cls.__module__}.{cls.__name__}")
fail(msg, pytrace=False)
def _create(cls: Type[_T], *k, **kw) -> _T:
def _create(cls: type[_T], *k, **kw) -> _T:
try:
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
except TypeError:
@ -160,12 +156,12 @@ class Node(abc.ABC, metaclass=NodeMeta):
def __init__(
self,
name: str,
parent: "Optional[Node]" = None,
config: Optional[Config] = None,
session: "Optional[Session]" = None,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
nodeid: Optional[str] = None,
parent: Node | None = None,
config: Config | None = None,
session: Session | None = None,
fspath: LEGACY_PATH | None = None,
path: Path | None = None,
nodeid: str | None = None,
) -> None:
#: A unique name within the scope of the parent node.
self.name: str = name
@ -199,10 +195,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
#: The marker objects belonging to this node.
self.own_markers: List[Mark] = []
self.own_markers: list[Mark] = []
#: Allow adding of extra keywords to use for matching.
self.extra_keyword_matches: Set[str] = set()
self.extra_keyword_matches: set[str] = set()
if nodeid is not None:
assert "::()" not in nodeid
@ -219,7 +215,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
self._store = self.stash
@classmethod
def from_parent(cls, parent: "Node", **kw) -> "Self":
def from_parent(cls, parent: Node, **kw) -> Self:
"""Public constructor for Nodes.
This indirection got introduced in order to enable removing
@ -295,31 +291,29 @@ class Node(abc.ABC, metaclass=NodeMeta):
def teardown(self) -> None:
pass
def iter_parents(self) -> Iterator["Node"]:
def iter_parents(self) -> Iterator[Node]:
"""Iterate over all parent collectors starting from and including self
up to the root of the collection tree.
.. versionadded:: 8.1
"""
parent: Optional[Node] = self
parent: Node | None = self
while parent is not None:
yield parent
parent = parent.parent
def listchain(self) -> List["Node"]:
def listchain(self) -> list[Node]:
"""Return a list of all parent collectors starting from the root of the
collection tree down to and including self."""
chain = []
item: Optional[Node] = self
item: Node | None = self
while item is not None:
chain.append(item)
item = item.parent
chain.reverse()
return chain
def add_marker(
self, marker: Union[str, MarkDecorator], append: bool = True
) -> None:
def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
"""Dynamically add a marker object to the node.
:param marker:
@ -341,7 +335,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
else:
self.own_markers.insert(0, marker_.mark)
def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]:
def iter_markers(self, name: str | None = None) -> Iterator[Mark]:
"""Iterate over all markers of the node.
:param name: If given, filter the results by the name attribute.
@ -350,8 +344,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
return (x[1] for x in self.iter_markers_with_node(name=name))
def iter_markers_with_node(
self, name: Optional[str] = None
) -> Iterator[Tuple["Node", Mark]]:
self, name: str | None = None
) -> Iterator[tuple[Node, Mark]]:
"""Iterate over all markers of the node.
:param name: If given, filter the results by the name attribute.
@ -363,14 +357,12 @@ class Node(abc.ABC, metaclass=NodeMeta):
yield node, mark
@overload
def get_closest_marker(self, name: str) -> Optional[Mark]: ...
def get_closest_marker(self, name: str) -> Mark | None: ...
@overload
def get_closest_marker(self, name: str, default: Mark) -> Mark: ...
def get_closest_marker(
self, name: str, default: Optional[Mark] = None
) -> Optional[Mark]:
def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None:
"""Return the first marker matching the name, from closest (for
example function) to farther level (for example module level).
@ -379,14 +371,14 @@ class Node(abc.ABC, metaclass=NodeMeta):
"""
return next(self.iter_markers(name=name), default)
def listextrakeywords(self) -> Set[str]:
def listextrakeywords(self) -> set[str]:
"""Return a set of all extra keywords in self and any parents."""
extra_keywords: Set[str] = set()
extra_keywords: set[str] = set()
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return extra_keywords
def listnames(self) -> List[str]:
def listnames(self) -> list[str]:
return [x.name for x in self.listchain()]
def addfinalizer(self, fin: Callable[[], object]) -> None:
@ -398,7 +390,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
"""
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
def getparent(self, cls: type[_NodeType]) -> _NodeType | None:
"""Get the closest parent node (including self) which is an instance of
the given class.
@ -416,7 +408,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
def _repr_failure_py(
self,
excinfo: ExceptionInfo[BaseException],
style: "Optional[TracebackStyle]" = None,
style: TracebackStyle | None = None,
) -> TerminalRepr:
from _pytest.fixtures import FixtureLookupError
@ -428,7 +420,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback]
if self.config.getoption("fulltrace", False):
style = "long"
tbfilter = False
@ -474,8 +466,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
def repr_failure(
self,
excinfo: ExceptionInfo[BaseException],
style: "Optional[TracebackStyle]" = None,
) -> Union[str, TerminalRepr]:
style: TracebackStyle | None = None,
) -> str | TerminalRepr:
"""Return a representation of a collection or test failure.
.. seealso:: :ref:`non-python tests`
@ -485,7 +477,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
return self._repr_failure_py(excinfo, style)
def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]:
def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]:
"""Try to extract the actual location from a node, depending on available attributes:
* "location": a pair (path, lineno)
@ -495,7 +487,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
# See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
location: tuple[str, int | None, str] | None = getattr(node, "location", None)
if location is not None:
return location[:2]
obj = getattr(node, "obj", None)
@ -515,14 +507,14 @@ class Collector(Node, abc.ABC):
"""An error during collection, contains a custom message."""
@abc.abstractmethod
def collect(self) -> Iterable[Union["Item", "Collector"]]:
def collect(self) -> Iterable[Item | Collector]:
"""Collect children (items and collectors) for this collector."""
raise NotImplementedError("abstract")
# TODO: This omits the style= parameter which breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
self, excinfo: ExceptionInfo[BaseException]
) -> Union[str, TerminalRepr]:
) -> str | TerminalRepr:
"""Return a representation of a collection failure.
:param excinfo: Exception information for the failure.
@ -551,7 +543,7 @@ class Collector(Node, abc.ABC):
return excinfo.traceback
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
def _check_initialpaths_for_relpath(session: Session, path: Path) -> str | None:
for initial_path in session._initialpaths:
if commonpath(path, initial_path) == initial_path:
rel = str(path.relative_to(initial_path))
@ -564,14 +556,14 @@ class FSCollector(Collector, abc.ABC):
def __init__(
self,
fspath: Optional[LEGACY_PATH] = None,
path_or_parent: Optional[Union[Path, Node]] = None,
path: Optional[Path] = None,
name: Optional[str] = None,
parent: Optional[Node] = None,
config: Optional[Config] = None,
session: Optional["Session"] = None,
nodeid: Optional[str] = None,
fspath: LEGACY_PATH | None = None,
path_or_parent: Path | Node | None = None,
path: Path | None = None,
name: str | None = None,
parent: Node | None = None,
config: Config | None = None,
session: Session | None = None,
nodeid: str | None = None,
) -> None:
if path_or_parent:
if isinstance(path_or_parent, Node):
@ -621,10 +613,10 @@ class FSCollector(Collector, abc.ABC):
cls,
parent,
*,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
fspath: LEGACY_PATH | None = None,
path: Path | None = None,
**kw,
) -> "Self":
) -> Self:
"""The public constructor."""
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
@ -666,9 +658,9 @@ class Item(Node, abc.ABC):
self,
name,
parent=None,
config: Optional[Config] = None,
session: Optional["Session"] = None,
nodeid: Optional[str] = None,
config: Config | None = None,
session: Session | None = None,
nodeid: str | None = None,
**kw,
) -> None:
# The first two arguments are intentionally passed positionally,
@ -683,11 +675,11 @@ class Item(Node, abc.ABC):
nodeid=nodeid,
**kw,
)
self._report_sections: List[Tuple[str, str, str]] = []
self._report_sections: list[tuple[str, str, str]] = []
#: A list of tuples (name, value) that holds user defined properties
#: for this test.
self.user_properties: List[Tuple[str, object]] = []
self.user_properties: list[tuple[str, object]] = []
self._check_item_and_collector_diamond_inheritance()
@ -747,7 +739,7 @@ class Item(Node, abc.ABC):
if content:
self._report_sections.append((when, key, content))
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
"""Get location information for this item for test reports.
Returns a tuple with three elements:
@ -761,7 +753,7 @@ class Item(Node, abc.ABC):
return self.path, None, ""
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
def location(self) -> tuple[str, int | None, str]:
"""
Returns a tuple of ``(relfspath, lineno, testname)`` for this item
where ``relfspath`` is file path relative to ``config.rootpath``

View File

@ -1,12 +1,13 @@
"""Exception classes and constants handling test outcomes as well as
functions creating them."""
from __future__ import annotations
import sys
from typing import Any
from typing import Callable
from typing import cast
from typing import NoReturn
from typing import Optional
from typing import Protocol
from typing import Type
from typing import TypeVar
@ -18,7 +19,7 @@ class OutcomeException(BaseException):
"""OutcomeException and its subclass instances indicate and contain info
about test and collection outcomes."""
def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None:
def __init__(self, msg: str | None = None, pytrace: bool = True) -> None:
if msg is not None and not isinstance(msg, str):
error_msg = ( # type: ignore[unreachable]
"{} expected string as 'msg' parameter, got '{}' instead.\n"
@ -47,7 +48,7 @@ class Skipped(OutcomeException):
def __init__(
self,
msg: Optional[str] = None,
msg: str | None = None,
pytrace: bool = True,
allow_module_level: bool = False,
*,
@ -70,7 +71,7 @@ class Exit(Exception):
"""Raised for immediate program exits (no tracebacks/summaries)."""
def __init__(
self, msg: str = "unknown reason", returncode: Optional[int] = None
self, msg: str = "unknown reason", returncode: int | None = None
) -> None:
self.msg = msg
self.returncode = returncode
@ -104,7 +105,7 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
@_with_exception(Exit)
def exit(
reason: str = "",
returncode: Optional[int] = None,
returncode: int | None = None,
) -> NoReturn:
"""Exit testing process.
@ -207,10 +208,10 @@ def xfail(reason: str = "") -> NoReturn:
def importorskip(
modname: str,
minversion: Optional[str] = None,
reason: Optional[str] = None,
minversion: str | None = None,
reason: str | None = None,
*,
exc_type: Optional[Type[ImportError]] = None,
exc_type: type[ImportError] | None = None,
) -> Any:
"""Import and return the requested module ``modname``, or skip the
current test if the module cannot be imported.
@ -267,8 +268,8 @@ def importorskip(
else:
warn_on_import_error = False
skipped: Optional[Skipped] = None
warning: Optional[Warning] = None
skipped: Skipped | None = None
warning: Warning | None = None
with warnings.catch_warnings():
# Make sure to ignore ImportWarnings that might happen because

View File

@ -1,10 +1,11 @@
# mypy: allow-untyped-defs
"""Submit failure or test session information to a pastebin service."""
from __future__ import annotations
from io import StringIO
import tempfile
from typing import IO
from typing import Union
from _pytest.config import Config
from _pytest.config import create_terminal_writer
@ -68,7 +69,7 @@ def pytest_unconfigure(config: Config) -> None:
tr.write_line(f"pastebin session-log: {pastebinurl}\n")
def create_new_paste(contents: Union[str, bytes]) -> str:
def create_new_paste(contents: str | bytes) -> str:
"""Create a new paste using the bpaste.net service.
:contents: Paste contents string.

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import atexit
import contextlib
from enum import Enum
@ -24,16 +26,9 @@ import types
from types import ModuleType
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import uuid
import warnings
@ -71,12 +66,10 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
def on_rm_rf_error(
func: Optional[Callable[..., Any]],
func: Callable[..., Any] | None,
path: str,
excinfo: Union[
BaseException,
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
],
excinfo: BaseException
| tuple[type[BaseException], BaseException, types.TracebackType | None],
*,
start_path: Path,
) -> bool:
@ -172,7 +165,7 @@ def rm_rf(path: Path) -> None:
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]:
"""Find all elements in root that begin with the prefix, case-insensitive."""
l_prefix = prefix.lower()
for x in os.scandir(root):
@ -180,7 +173,7 @@ def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
yield x
def extract_suffixes(iter: Iterable["os.DirEntry[str]"], prefix: str) -> Iterator[str]:
def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]:
"""Return the parts of the paths following the prefix.
:param iter: Iterator over path names.
@ -204,9 +197,7 @@ def parse_num(maybe_num: str) -> int:
return -1
def _force_symlink(
root: Path, target: Union[str, PurePath], link_to: Union[str, Path]
) -> None:
def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None:
"""Helper to create the current symlink.
It's full of race conditions that are reasonably OK to ignore
@ -420,7 +411,7 @@ def resolve_from_str(input: str, rootpath: Path) -> Path:
return rootpath.joinpath(input)
def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool:
def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool:
"""A port of FNMatcher from py.path.common which works with PurePath() instances.
The difference between this algorithm and PurePath.match() is that the
@ -456,14 +447,14 @@ def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool:
return fnmatch.fnmatch(name, pattern)
def parts(s: str) -> Set[str]:
def parts(s: str) -> set[str]:
parts = s.split(sep)
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
def symlink_or_skip(
src: Union["os.PathLike[str]", str],
dst: Union["os.PathLike[str]", str],
src: os.PathLike[str] | str,
dst: os.PathLike[str] | str,
**kwargs: Any,
) -> None:
"""Make a symlink, or skip the test in case symlinks are not supported."""
@ -491,9 +482,9 @@ class ImportPathMismatchError(ImportError):
def import_path(
path: Union[str, "os.PathLike[str]"],
path: str | os.PathLike[str],
*,
mode: Union[str, ImportMode] = ImportMode.prepend,
mode: str | ImportMode = ImportMode.prepend,
root: Path,
consider_namespace_packages: bool,
) -> ModuleType:
@ -618,7 +609,7 @@ def import_path(
def _import_module_using_spec(
module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool
) -> Optional[ModuleType]:
) -> ModuleType | None:
"""
Tries to import a module by its canonical name, path to the .py file, and its
parent location.
@ -641,7 +632,7 @@ def _import_module_using_spec(
# Attempt to import the parent module, seems is our responsibility:
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
parent_module_name, _, name = module_name.rpartition(".")
parent_module: Optional[ModuleType] = None
parent_module: ModuleType | None = None
if parent_module_name:
parent_module = sys.modules.get(parent_module_name)
if parent_module is None:
@ -680,9 +671,7 @@ def _import_module_using_spec(
return None
def spec_matches_module_path(
module_spec: Optional[ModuleSpec], module_path: Path
) -> bool:
def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool:
"""Return true if the given ModuleSpec can be used to import the given module path."""
if module_spec is None or module_spec.origin is None:
return False
@ -734,7 +723,7 @@ def module_name_from_path(path: Path, root: Path) -> str:
return ".".join(path_parts)
def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> None:
def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None:
"""
Used by ``import_path`` to create intermediate modules when using mode=importlib.
@ -772,7 +761,7 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
module_name = ".".join(module_parts)
def resolve_package_path(path: Path) -> Optional[Path]:
def resolve_package_path(path: Path) -> Path | None:
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
@ -791,7 +780,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
def resolve_pkg_root_and_module_name(
path: Path, *, consider_namespace_packages: bool = False
) -> Tuple[Path, str]:
) -> tuple[Path, str]:
"""
Return the path to the directory of the root package that contains the
given Python file, and its module name:
@ -812,7 +801,7 @@ def resolve_pkg_root_and_module_name(
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
"""
pkg_root: Optional[Path] = None
pkg_root: Path | None = None
pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
@ -859,7 +848,7 @@ def is_importable(module_name: str, module_path: Path) -> bool:
return spec_matches_module_path(spec, module_path)
def compute_module_name(root: Path, module_path: Path) -> Optional[str]:
def compute_module_name(root: Path, module_path: Path) -> str | None:
"""Compute a module name based on a path and a root anchor."""
try:
path_without_suffix = module_path.with_suffix("")
@ -884,9 +873,9 @@ class CouldNotResolvePathError(Exception):
def scandir(
path: Union[str, "os.PathLike[str]"],
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
) -> List["os.DirEntry[str]"]:
path: str | os.PathLike[str],
sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name,
) -> list[os.DirEntry[str]]:
"""Scan a directory recursively, in breadth-first order.
The returned entries are sorted according to the given key.
@ -909,8 +898,8 @@ def scandir(
def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]:
path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool]
) -> Iterator[os.DirEntry[str]]:
"""Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
@ -924,7 +913,7 @@ def visit(
yield from visit(entry.path, recurse)
def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path:
def absolutepath(path: str | os.PathLike[str]) -> Path:
"""Convert a path to an absolute path using os.path.abspath.
Prefer this over Path.resolve() (see #6523).
@ -933,7 +922,7 @@ def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path:
return Path(os.path.abspath(path))
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
def commonpath(path1: Path, path2: Path) -> Path | None:
"""Return the common part shared with the other path, or None if there is
no common part.

View File

@ -4,6 +4,8 @@
PYTEST_DONT_REWRITE
"""
from __future__ import annotations
import collections.abc
import contextlib
from fnmatch import fnmatch
@ -21,22 +23,16 @@ import sys
import traceback
from typing import Any
from typing import Callable
from typing import Dict
from typing import Final
from typing import final
from typing import Generator
from typing import IO
from typing import Iterable
from typing import List
from typing import Literal
from typing import Optional
from typing import overload
from typing import Sequence
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
from weakref import WeakKeyDictionary
from iniconfig import IniConfig
@ -123,7 +119,7 @@ def pytest_configure(config: Config) -> None:
class LsofFdLeakChecker:
def get_open_files(self) -> List[Tuple[str, str]]:
def get_open_files(self) -> list[tuple[str, str]]:
if sys.version_info >= (3, 11):
# New in Python 3.11, ignores utf-8 mode
encoding = locale.getencoding()
@ -199,7 +195,7 @@ class LsofFdLeakChecker:
@fixture
def _pytest(request: FixtureRequest) -> "PytestArg":
def _pytest(request: FixtureRequest) -> PytestArg:
"""Return a helper which offers a gethookrecorder(hook) method which
returns a HookRecorder instance which helps to make assertions about called
hooks."""
@ -210,13 +206,13 @@ class PytestArg:
def __init__(self, request: FixtureRequest) -> None:
self._request = request
def gethookrecorder(self, hook) -> "HookRecorder":
def gethookrecorder(self, hook) -> HookRecorder:
hookrecorder = HookRecorder(hook._pm)
self._request.addfinalizer(hookrecorder.finish_recording)
return hookrecorder
def get_public_names(values: Iterable[str]) -> List[str]:
def get_public_names(values: Iterable[str]) -> list[str]:
"""Only return names from iterator values without a leading underscore."""
return [x for x in values if x[0] != "_"]
@ -265,8 +261,8 @@ class HookRecorder:
check_ispytest(_ispytest)
self._pluginmanager = pluginmanager
self.calls: List[RecordedHookCall] = []
self.ret: Optional[Union[int, ExitCode]] = None
self.calls: list[RecordedHookCall] = []
self.ret: int | ExitCode | None = None
def before(hook_name: str, hook_impls, kwargs) -> None:
self.calls.append(RecordedHookCall(hook_name, kwargs))
@ -279,13 +275,13 @@ class HookRecorder:
def finish_recording(self) -> None:
self._undo_wrapping()
def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]:
def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]:
"""Get all recorded calls to hooks with the given names (or name)."""
if isinstance(names, str):
names = names.split()
return [call for call in self.calls if call._name in names]
def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None:
def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None:
__tracebackhide__ = True
i = 0
entries = list(entries)
@ -327,42 +323,42 @@ class HookRecorder:
@overload
def getreports(
self,
names: "Literal['pytest_collectreport']",
names: Literal["pytest_collectreport"],
) -> Sequence[CollectReport]: ...
@overload
def getreports(
self,
names: "Literal['pytest_runtest_logreport']",
names: Literal["pytest_runtest_logreport"],
) -> Sequence[TestReport]: ...
@overload
def getreports(
self,
names: Union[str, Iterable[str]] = (
names: str | Iterable[str] = (
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]: ...
) -> Sequence[CollectReport | TestReport]: ...
def getreports(
self,
names: Union[str, Iterable[str]] = (
names: str | Iterable[str] = (
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]:
) -> Sequence[CollectReport | TestReport]:
return [x.report for x in self.getcalls(names)]
def matchreport(
self,
inamepart: str = "",
names: Union[str, Iterable[str]] = (
names: str | Iterable[str] = (
"pytest_runtest_logreport",
"pytest_collectreport",
),
when: Optional[str] = None,
) -> Union[CollectReport, TestReport]:
when: str | None = None,
) -> CollectReport | TestReport:
"""Return a testreport whose dotted import path matches."""
values = []
for rep in self.getreports(names=names):
@ -387,31 +383,31 @@ class HookRecorder:
@overload
def getfailures(
self,
names: "Literal['pytest_collectreport']",
names: Literal["pytest_collectreport"],
) -> Sequence[CollectReport]: ...
@overload
def getfailures(
self,
names: "Literal['pytest_runtest_logreport']",
names: Literal["pytest_runtest_logreport"],
) -> Sequence[TestReport]: ...
@overload
def getfailures(
self,
names: Union[str, Iterable[str]] = (
names: str | Iterable[str] = (
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]: ...
) -> Sequence[CollectReport | TestReport]: ...
def getfailures(
self,
names: Union[str, Iterable[str]] = (
names: str | Iterable[str] = (
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]:
) -> Sequence[CollectReport | TestReport]:
return [rep for rep in self.getreports(names) if rep.failed]
def getfailedcollections(self) -> Sequence[CollectReport]:
@ -419,10 +415,10 @@ class HookRecorder:
def listoutcomes(
self,
) -> Tuple[
) -> tuple[
Sequence[TestReport],
Sequence[Union[CollectReport, TestReport]],
Sequence[Union[CollectReport, TestReport]],
Sequence[CollectReport | TestReport],
Sequence[CollectReport | TestReport],
]:
passed = []
skipped = []
@ -441,7 +437,7 @@ class HookRecorder:
failed.append(rep)
return passed, skipped, failed
def countoutcomes(self) -> List[int]:
def countoutcomes(self) -> list[int]:
return [len(x) for x in self.listoutcomes()]
def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
@ -461,14 +457,14 @@ class HookRecorder:
@fixture
def linecomp() -> "LineComp":
def linecomp() -> LineComp:
"""A :class: `LineComp` instance for checking that an input linearly
contains a sequence of strings."""
return LineComp()
@fixture(name="LineMatcher")
def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]:
"""A reference to the :class: `LineMatcher`.
This is instantiable with a list of lines (without their trailing newlines).
@ -480,7 +476,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
@fixture
def pytester(
request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
) -> "Pytester":
) -> Pytester:
"""
Facilities to write tests/configuration files, execute pytest in isolation, and match
against expected output, perfect for black-box testing of pytest plugins.
@ -524,13 +520,13 @@ class RunResult:
def __init__(
self,
ret: Union[int, ExitCode],
outlines: List[str],
errlines: List[str],
ret: int | ExitCode,
outlines: list[str],
errlines: list[str],
duration: float,
) -> None:
try:
self.ret: Union[int, ExitCode] = ExitCode(ret)
self.ret: int | ExitCode = ExitCode(ret)
"""The return value."""
except ValueError:
self.ret = ret
@ -555,7 +551,7 @@ class RunResult:
% (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration)
)
def parseoutcomes(self) -> Dict[str, int]:
def parseoutcomes(self) -> dict[str, int]:
"""Return a dictionary of outcome noun -> count from parsing the terminal
output that the test process produced.
@ -568,7 +564,7 @@ class RunResult:
return self.parse_summary_nouns(self.outlines)
@classmethod
def parse_summary_nouns(cls, lines) -> Dict[str, int]:
def parse_summary_nouns(cls, lines) -> dict[str, int]:
"""Extract the nouns from a pytest terminal summary line.
It always returns the plural noun for consistency::
@ -599,8 +595,8 @@ class RunResult:
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
warnings: Optional[int] = None,
deselected: Optional[int] = None,
warnings: int | None = None,
deselected: int | None = None,
) -> None:
"""
Assert that the specified outcomes appear with the respective
@ -626,7 +622,7 @@ class RunResult:
class SysModulesSnapshot:
def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None:
def __init__(self, preserve: Callable[[str], bool] | None = None) -> None:
self.__preserve = preserve
self.__saved = dict(sys.modules)
@ -659,7 +655,7 @@ class Pytester:
__test__ = False
CLOSE_STDIN: "Final" = NOTSET
CLOSE_STDIN: Final = NOTSET
class TimeoutExpired(Exception):
pass
@ -674,9 +670,9 @@ class Pytester:
) -> None:
check_ispytest(_ispytest)
self._request = request
self._mod_collections: WeakKeyDictionary[
Collector, List[Union[Item, Collector]]
] = WeakKeyDictionary()
self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = (
WeakKeyDictionary()
)
if request.function:
name: str = request.function.__name__
else:
@ -687,7 +683,7 @@ class Pytester:
#: :py:meth:`runpytest`. Initially this is an empty list but plugins can
#: be added to the list. The type of items to add to the list depends on
#: the method using them so refer to them for details.
self.plugins: List[Union[str, _PluggyPlugin]] = []
self.plugins: list[str | _PluggyPlugin] = []
self._sys_path_snapshot = SysPathsSnapshot()
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
self._request.addfinalizer(self._finalize)
@ -755,8 +751,8 @@ class Pytester:
def _makefile(
self,
ext: str,
lines: Sequence[Union[Any, bytes]],
files: Dict[str, str],
lines: Sequence[Any | bytes],
files: dict[str, str],
encoding: str = "utf-8",
) -> Path:
items = list(files.items())
@ -769,7 +765,7 @@ class Pytester:
f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
)
def to_text(s: Union[Any, bytes]) -> str:
def to_text(s: Any | bytes) -> str:
return s.decode(encoding) if isinstance(s, bytes) else str(s)
if lines:
@ -892,9 +888,7 @@ class Pytester:
"""
return self._makefile(".txt", args, kwargs)
def syspathinsert(
self, path: Optional[Union[str, "os.PathLike[str]"]] = None
) -> None:
def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None:
"""Prepend a directory to sys.path, defaults to :attr:`path`.
This is undone automatically when this object dies at the end of each
@ -908,19 +902,20 @@ class Pytester:
self._monkeypatch.syspath_prepend(str(path))
def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
def mkdir(self, name: str | os.PathLike[str]) -> Path:
"""Create a new (sub)directory.
:param name:
The name of the directory, relative to the pytester path.
:returns:
The created directory.
:rtype: pathlib.Path
"""
p = self.path / name
p.mkdir()
return p
def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path:
def mkpydir(self, name: str | os.PathLike[str]) -> Path:
"""Create a new python package.
This creates a (sub)directory with an empty ``__init__.py`` file so it
@ -931,13 +926,14 @@ class Pytester:
p.joinpath("__init__.py").touch()
return p
def copy_example(self, name: Optional[str] = None) -> Path:
def copy_example(self, name: str | None = None) -> Path:
"""Copy file from project's directory into the testdir.
:param name:
The name of the file to copy.
:return:
Path to the copied directory (inside ``self.path``).
:rtype: pathlib.Path
"""
example_dir_ = self._request.config.getini("pytester_example_dir")
if example_dir_ is None:
@ -976,9 +972,7 @@ class Pytester:
f'example "{example_path}" is not found as a file or directory'
)
def getnode(
self, config: Config, arg: Union[str, "os.PathLike[str]"]
) -> Union[Collector, Item]:
def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item:
"""Get the collection node of a file.
:param config:
@ -997,9 +991,7 @@ class Pytester:
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
return res
def getpathnode(
self, path: Union[str, "os.PathLike[str]"]
) -> Union[Collector, Item]:
def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item:
"""Return the collection node of a file.
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
@ -1019,7 +1011,7 @@ class Pytester:
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
return res
def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]:
def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]:
"""Generate all test items from a collection node.
This recurses into the collection node and returns a list of all the
@ -1031,7 +1023,7 @@ class Pytester:
The collected items.
"""
session = colitems[0].session
result: List[Item] = []
result: list[Item] = []
for colitem in colitems:
result.extend(session.genitems(colitem))
return result
@ -1065,7 +1057,7 @@ class Pytester:
values = [*list(cmdlineargs), p]
return self.inline_run(*values)
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]:
"""Run ``pytest.main(['--collect-only'])`` in-process.
Runs the :py:func:`pytest.main` function to run all of pytest inside
@ -1078,7 +1070,7 @@ class Pytester:
def inline_run(
self,
*args: Union[str, "os.PathLike[str]"],
*args: str | os.PathLike[str],
plugins=(),
no_reraise_ctrlc: bool = False,
) -> HookRecorder:
@ -1148,7 +1140,7 @@ class Pytester:
finalizer()
def runpytest_inprocess(
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
self, *args: str | os.PathLike[str], **kwargs: Any
) -> RunResult:
"""Return result of running pytest in-process, providing a similar
interface to what self.runpytest() provides."""
@ -1191,9 +1183,7 @@ class Pytester:
res.reprec = reprec # type: ignore
return res
def runpytest(
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
) -> RunResult:
def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult:
"""Run pytest inline or in a subprocess, depending on the command line
option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
new_args = self._ensure_basetemp(args)
@ -1204,8 +1194,8 @@ class Pytester:
raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
def _ensure_basetemp(
self, args: Sequence[Union[str, "os.PathLike[str]"]]
) -> List[Union[str, "os.PathLike[str]"]]:
self, args: Sequence[str | os.PathLike[str]]
) -> list[str | os.PathLike[str]]:
new_args = list(args)
for x in new_args:
if str(x).startswith("--basetemp"):
@ -1216,7 +1206,7 @@ class Pytester:
)
return new_args
def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
"""Return a new pytest :class:`pytest.Config` instance from given
commandline args.
@ -1240,7 +1230,7 @@ class Pytester:
self._request.addfinalizer(config._ensure_unconfigure)
return config
def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
def parseconfigure(self, *args: str | os.PathLike[str]) -> Config:
"""Return a new pytest configured Config instance.
Returns a new :py:class:`pytest.Config` instance like
@ -1252,7 +1242,7 @@ class Pytester:
return config
def getitem(
self, source: Union[str, "os.PathLike[str]"], funcname: str = "test_func"
self, source: str | os.PathLike[str], funcname: str = "test_func"
) -> Item:
"""Return the test item for a test function.
@ -1273,7 +1263,7 @@ class Pytester:
return item
assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}"
def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]:
def getitems(self, source: str | os.PathLike[str]) -> list[Item]:
"""Return all test items collected from the module.
Writes the source to a Python file and runs pytest's collection on
@ -1284,7 +1274,7 @@ class Pytester:
def getmodulecol(
self,
source: Union[str, "os.PathLike[str]"],
source: str | os.PathLike[str],
configargs=(),
*,
withinit: bool = False,
@ -1316,9 +1306,7 @@ class Pytester:
self.config = config = self.parseconfigure(path, *configargs)
return self.getnode(config, path)
def collect_by_name(
self, modcol: Collector, name: str
) -> Optional[Union[Item, Collector]]:
def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None:
"""Return the collection node for name from the module collection.
Searches a module collection node for a collection node matching the
@ -1336,10 +1324,10 @@ class Pytester:
def popen(
self,
cmdargs: Sequence[Union[str, "os.PathLike[str]"]],
stdout: Union[int, TextIO] = subprocess.PIPE,
stderr: Union[int, TextIO] = subprocess.PIPE,
stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
cmdargs: Sequence[str | os.PathLike[str]],
stdout: int | TextIO = subprocess.PIPE,
stderr: int | TextIO = subprocess.PIPE,
stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
**kw,
):
"""Invoke :py:class:`subprocess.Popen`.
@ -1374,9 +1362,9 @@ class Pytester:
def run(
self,
*cmdargs: Union[str, "os.PathLike[str]"],
timeout: Optional[float] = None,
stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN,
*cmdargs: str | os.PathLike[str],
timeout: float | None = None,
stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
) -> RunResult:
"""Run a command with arguments.
@ -1404,8 +1392,10 @@ class Pytester:
- Otherwise, it is passed through to :py:class:`subprocess.Popen`.
For further information in this case, consult the document of the
``stdin`` parameter in :py:class:`subprocess.Popen`.
:type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int
:returns:
The result.
"""
__tracebackhide__ = True
@ -1462,10 +1452,10 @@ class Pytester:
except UnicodeEncodeError:
print(f"couldn't print to {fp} because of encoding")
def _getpytestargs(self) -> Tuple[str, ...]:
def _getpytestargs(self) -> tuple[str, ...]:
return sys.executable, "-mpytest"
def runpython(self, script: "os.PathLike[str]") -> RunResult:
def runpython(self, script: os.PathLike[str]) -> RunResult:
"""Run a python script using sys.executable as interpreter."""
return self.run(sys.executable, script)
@ -1474,7 +1464,7 @@ class Pytester:
return self.run(sys.executable, "-c", command)
def runpytest_subprocess(
self, *args: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None
self, *args: str | os.PathLike[str], timeout: float | None = None
) -> RunResult:
"""Run pytest as a subprocess with given arguments.
@ -1501,9 +1491,7 @@ class Pytester:
args = self._getpytestargs() + args
return self.run(*args, timeout=timeout)
def spawn_pytest(
self, string: str, expect_timeout: float = 10.0
) -> "pexpect.spawn":
def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn:
"""Run pytest using pexpect.
This makes sure to use the right pytest and sets up the temporary
@ -1517,7 +1505,7 @@ class Pytester:
cmd = f"{invoke} --basetemp={basetemp} {string}"
return self.spawn(cmd, expect_timeout=expect_timeout)
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn:
"""Run a command using pexpect.
The pexpect child is returned.
@ -1562,9 +1550,9 @@ class LineMatcher:
``text.splitlines()``.
"""
def __init__(self, lines: List[str]) -> None:
def __init__(self, lines: list[str]) -> None:
self.lines = lines
self._log_output: List[str] = []
self._log_output: list[str] = []
def __str__(self) -> str:
"""Return the entire original text.
@ -1574,7 +1562,7 @@ class LineMatcher:
"""
return "\n".join(self.lines)
def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]:
def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]:
if isinstance(lines2, str):
lines2 = Source(lines2)
if isinstance(lines2, Source):

View File

@ -4,21 +4,19 @@
# contain them itself, since it is imported by the `pytest` module,
# hence cannot be subject to assertion rewriting, which requires a
# module to not be already imported.
from typing import Dict
from typing import Optional
from __future__ import annotations
from typing import Sequence
from typing import Tuple
from typing import Union
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
def assertoutcome(
outcomes: Tuple[
outcomes: tuple[
Sequence[TestReport],
Sequence[Union[CollectReport, TestReport]],
Sequence[Union[CollectReport, TestReport]],
Sequence[CollectReport | TestReport],
Sequence[CollectReport | TestReport],
],
passed: int = 0,
skipped: int = 0,
@ -37,15 +35,15 @@ def assertoutcome(
def assert_outcomes(
outcomes: Dict[str, int],
outcomes: dict[str, int],
passed: int = 0,
skipped: int = 0,
failed: int = 0,
errors: int = 0,
xpassed: int = 0,
xfailed: int = 0,
warnings: Optional[int] = None,
deselected: Optional[int] = None,
warnings: int | None = None,
deselected: int | None = None,
) -> None:
"""Assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run."""

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Python test discovery, setup and run of test functions."""
from __future__ import annotations
import abc
from collections import Counter
from collections import defaultdict
@ -20,16 +22,11 @@ from typing import final
from typing import Generator
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Literal
from typing import Mapping
from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import warnings
import _pytest
@ -113,7 +110,7 @@ def pytest_addoption(parser: Parser) -> None:
)
def pytest_generate_tests(metafunc: "Metafunc") -> None:
def pytest_generate_tests(metafunc: Metafunc) -> None:
for marker in metafunc.definition.iter_markers(name="parametrize"):
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
@ -153,7 +150,7 @@ def async_warn_and_skip(nodeid: str) -> None:
@hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
testfunction = pyfuncitem.obj
if is_async_function(testfunction):
async_warn_and_skip(pyfuncitem.nodeid)
@ -174,7 +171,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
def pytest_collect_directory(
path: Path, parent: nodes.Collector
) -> Optional[nodes.Collector]:
) -> nodes.Collector | None:
pkginit = path / "__init__.py"
try:
has_pkginit = pkginit.is_file()
@ -186,7 +183,7 @@ def pytest_collect_directory(
return None
def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]:
def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Module | None:
if file_path.suffix == ".py":
if not parent.session.isinitpath(file_path):
if not path_matches_patterns(
@ -206,14 +203,14 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
return any(fnmatch_ex(pattern, path) for pattern in patterns)
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
def pytest_pycollect_makemodule(module_path: Path, parent) -> Module:
return Module.from_parent(parent, path=module_path)
@hookimpl(trylast=True)
def pytest_pycollect_makeitem(
collector: Union["Module", "Class"], name: str, obj: object
) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]:
collector: Module | Class, name: str, obj: object
) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]:
assert isinstance(collector, (Class, Module)), type(collector)
# Nothing was collected elsewhere, let's do it here.
if safe_isclass(obj):
@ -320,7 +317,7 @@ class PyobjMixin(nodes.Node):
parts.reverse()
return ".".join(parts)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
# XXX caching?
path, lineno = getfslineno(self.obj)
modpath = self.getmodpath()
@ -394,7 +391,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
return True
return False
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
if not getattr(self.obj, "__test__", True):
return []
@ -406,11 +403,11 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
# In each class, nodes should be definition ordered.
# __dict__ is definition ordered.
seen: Set[str] = set()
dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = []
seen: set[str] = set()
dict_values: list[list[nodes.Item | nodes.Collector]] = []
ihook = self.ihook
for dic in dicts:
values: List[Union[nodes.Item, nodes.Collector]] = []
values: list[nodes.Item | nodes.Collector] = []
# Note: seems like the dict can change during iteration -
# be careful not to remove the list() without consideration.
for name, obj in list(dic.items()):
@ -437,7 +434,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
result.extend(values)
return result
def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
def _genfunctions(self, name: str, funcobj) -> Iterator[Function]:
modulecol = self.getparent(Module)
assert modulecol is not None
module = modulecol.obj
@ -548,7 +545,7 @@ class Module(nodes.File, PyCollector):
def _getobj(self):
return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
self._register_setup_module_fixture()
self._register_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
@ -642,13 +639,13 @@ class Package(nodes.Directory):
def __init__(
self,
fspath: Optional[LEGACY_PATH],
fspath: LEGACY_PATH | None,
parent: nodes.Collector,
# NOTE: following args are unused:
config=None,
session=None,
nodeid=None,
path: Optional[Path] = None,
path: Path | None = None,
) -> None:
# NOTE: Could be just the following, but kept as-is for compat.
# super().__init__(self, fspath, parent=parent)
@ -680,13 +677,13 @@ class Package(nodes.Directory):
func = partial(_call_with_optional_argument, teardown_module, init_mod)
self.addfinalizer(func)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
# Always collect __init__.py first.
def sort_key(entry: "os.DirEntry[str]") -> object:
def sort_key(entry: os.DirEntry[str]) -> object:
return (entry.name != "__init__.py", entry.name)
config = self.config
col: Optional[nodes.Collector]
col: nodes.Collector | None
cols: Sequence[nodes.Collector]
ihook = self.ihook
for direntry in scandir(self.path, sort_key):
@ -720,12 +717,12 @@ def _call_with_optional_argument(func, arg) -> None:
func()
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None:
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
"""
for name in names:
meth: Optional[object] = getattr(obj, name, None)
meth: object | None = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
return meth
return None
@ -735,14 +732,14 @@ class Class(PyCollector):
"""Collector for test methods (and nested classes) in a Python class."""
@classmethod
def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override]
def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override]
"""The public constructor."""
return super().from_parent(name=name, parent=parent, **kw)
def newinstance(self):
return self.obj()
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
if not safe_getattr(self.obj, "__test__", True):
return []
if hasinit(self.obj):
@ -872,21 +869,21 @@ class IdMaker:
parametersets: Sequence[ParameterSet]
# Optionally, a user-provided callable to make IDs for parameters in a
# ParameterSet.
idfn: Optional[Callable[[Any], Optional[object]]]
idfn: Callable[[Any], object | None] | None
# Optionally, explicit IDs for ParameterSets by index.
ids: Optional[Sequence[Optional[object]]]
ids: Sequence[object | None] | None
# Optionally, the pytest config.
# Used for controlling ASCII escaping, and for calling the
# :hook:`pytest_make_parametrize_id` hook.
config: Optional[Config]
config: Config | None
# Optionally, the ID of the node being parametrized.
# Used only for clearer error messages.
nodeid: Optional[str]
nodeid: str | None
# Optionally, the ID of the function being parametrized.
# Used only for clearer error messages.
func_name: Optional[str]
func_name: str | None
def make_unique_parameterset_ids(self) -> List[str]:
def make_unique_parameterset_ids(self) -> list[str]:
"""Make a unique identifier for each ParameterSet, that may be used to
identify the parametrization in a node ID.
@ -903,7 +900,7 @@ class IdMaker:
# Record the number of occurrences of each ID.
id_counts = Counter(resolved_ids)
# Map the ID to its next suffix.
id_suffixes: Dict[str, int] = defaultdict(int)
id_suffixes: dict[str, int] = defaultdict(int)
# Suffix non-unique IDs to make them unique.
for index, id in enumerate(resolved_ids):
if id_counts[id] > 1:
@ -950,9 +947,7 @@ class IdMaker:
return idval
return self._idval_from_argname(argname, idx)
def _idval_from_function(
self, val: object, argname: str, idx: int
) -> Optional[str]:
def _idval_from_function(self, val: object, argname: str, idx: int) -> str | None:
"""Try to make an ID for a parameter in a ParameterSet using the
user-provided id callable, if given."""
if self.idfn is None:
@ -968,17 +963,17 @@ class IdMaker:
return None
return self._idval_from_value(id)
def _idval_from_hook(self, val: object, argname: str) -> Optional[str]:
def _idval_from_hook(self, val: object, argname: str) -> str | None:
"""Try to make an ID for a parameter in a ParameterSet by calling the
:hook:`pytest_make_parametrize_id` hook."""
if self.config:
id: Optional[str] = self.config.hook.pytest_make_parametrize_id(
id: str | None = self.config.hook.pytest_make_parametrize_id(
config=self.config, val=val, argname=argname
)
return id
return None
def _idval_from_value(self, val: object) -> Optional[str]:
def _idval_from_value(self, val: object) -> str | None:
"""Try to make an ID for a parameter in a ParameterSet from its value,
if the value type is supported."""
if isinstance(val, (str, bytes)):
@ -1036,15 +1031,15 @@ class CallSpec2:
# arg name -> arg value which will be passed to a fixture or pseudo-fixture
# of the same name. (indirect or direct parametrization respectively)
params: Dict[str, object] = dataclasses.field(default_factory=dict)
params: dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index.
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
indices: dict[str, int] = dataclasses.field(default_factory=dict)
# Used for sorting parametrized resources.
_arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict)
# Parts which will be added to the item's name in `[..]` separated by "-".
_idlist: Sequence[str] = dataclasses.field(default_factory=tuple)
# Marks which will be applied to the item.
marks: List[Mark] = dataclasses.field(default_factory=list)
marks: list[Mark] = dataclasses.field(default_factory=list)
def setmulti(
self,
@ -1052,10 +1047,10 @@ class CallSpec2:
argnames: Iterable[str],
valset: Iterable[object],
id: str,
marks: Iterable[Union[Mark, MarkDecorator]],
marks: Iterable[Mark | MarkDecorator],
scope: Scope,
param_index: int,
) -> "CallSpec2":
) -> CallSpec2:
params = self.params.copy()
indices = self.indices.copy()
arg2scope = dict(self._arg2scope)
@ -1103,7 +1098,7 @@ class Metafunc:
def __init__(
self,
definition: "FunctionDefinition",
definition: FunctionDefinition,
fixtureinfo: fixtures.FuncFixtureInfo,
config: Config,
cls=None,
@ -1134,19 +1129,17 @@ class Metafunc:
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
# Result of parametrize().
self._calls: List[CallSpec2] = []
self._calls: list[CallSpec2] = []
def parametrize(
self,
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
indirect: Union[bool, Sequence[str]] = False,
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
scope: Optional[_ScopeName] = None,
argnames: str | Sequence[str],
argvalues: Iterable[ParameterSet | Sequence[object] | object],
indirect: bool | Sequence[str] = False,
ids: Iterable[object | None] | Callable[[Any], object | None] | None = None,
scope: _ScopeName | None = None,
*,
_param_mark: Optional[Mark] = None,
_param_mark: Mark | None = None,
) -> None:
"""Add new invocations to the underlying test function using the list
of argvalues for the given argnames. Parametrization is performed
@ -1175,7 +1168,7 @@ class Metafunc:
If N argnames were specified, argvalues must be a list of
N-tuples, where each tuple-element specifies a value for its
respective argname.
:type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object]
:param indirect:
A list of arguments' names (subset of argnames) or a boolean.
If True the list contains all names from the argnames. Each
@ -1275,7 +1268,7 @@ class Metafunc:
if node is None:
name2pseudofixturedef = None
else:
default: Dict[str, FixtureDef[Any]] = {}
default: dict[str, FixtureDef[Any]] = {}
name2pseudofixturedef = node.stash.setdefault(
name2pseudofixturedef_key, default
)
@ -1322,12 +1315,10 @@ class Metafunc:
def _resolve_parameter_set_ids(
self,
argnames: Sequence[str],
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
],
ids: Iterable[object | None] | Callable[[Any], object | None] | None,
parametersets: Sequence[ParameterSet],
nodeid: str,
) -> List[str]:
) -> list[str]:
"""Resolve the actual ids for the given parameter sets.
:param argnames:
@ -1365,10 +1356,10 @@ class Metafunc:
def _validate_ids(
self,
ids: Iterable[Optional[object]],
ids: Iterable[object | None],
parametersets: Sequence[ParameterSet],
func_name: str,
) -> List[Optional[object]]:
) -> list[object | None]:
try:
num_ids = len(ids) # type: ignore[arg-type]
except TypeError:
@ -1388,8 +1379,8 @@ class Metafunc:
def _resolve_args_directness(
self,
argnames: Sequence[str],
indirect: Union[bool, Sequence[str]],
) -> Dict[str, Literal["indirect", "direct"]]:
indirect: bool | Sequence[str],
) -> dict[str, Literal["indirect", "direct"]]:
"""Resolve if each parametrized argument must be considered an indirect
parameter to a fixture of the same name, or a direct parameter to the
parametrized function, based on the ``indirect`` parameter of the
@ -1402,7 +1393,7 @@ class Metafunc:
:returns
A dict mapping each arg name to either "indirect" or "direct".
"""
arg_directness: Dict[str, Literal["indirect", "direct"]]
arg_directness: dict[str, Literal["indirect", "direct"]]
if isinstance(indirect, bool):
arg_directness = dict.fromkeys(
argnames, "indirect" if indirect else "direct"
@ -1427,7 +1418,7 @@ class Metafunc:
def _validate_if_using_arg_names(
self,
argnames: Sequence[str],
indirect: Union[bool, Sequence[str]],
indirect: bool | Sequence[str],
) -> None:
"""Check if all argnames are being used, by default values, or directly/indirectly.
@ -1458,7 +1449,7 @@ class Metafunc:
def _find_parametrized_scope(
argnames: Sequence[str],
arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
indirect: Union[bool, Sequence[str]],
indirect: bool | Sequence[str],
) -> Scope:
"""Find the most appropriate scope for a parametrized call based on its arguments.
@ -1487,7 +1478,7 @@ def _find_parametrized_scope(
return Scope.Function
def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str:
def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str:
if config is None:
escape_option = False
else:
@ -1536,13 +1527,13 @@ class Function(PyobjMixin, nodes.Item):
self,
name: str,
parent,
config: Optional[Config] = None,
callspec: Optional[CallSpec2] = None,
config: Config | None = None,
callspec: CallSpec2 | None = None,
callobj=NOTSET,
keywords: Optional[Mapping[str, Any]] = None,
session: Optional[Session] = None,
fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname: Optional[str] = None,
keywords: Mapping[str, Any] | None = None,
session: Session | None = None,
fixtureinfo: FuncFixtureInfo | None = None,
originalname: str | None = None,
) -> None:
super().__init__(name, parent, config=config, session=session)
@ -1585,12 +1576,12 @@ class Function(PyobjMixin, nodes.Item):
# todo: determine sound type limitations
@classmethod
def from_parent(cls, parent, **kw) -> "Self":
def from_parent(cls, parent, **kw) -> Self:
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self.funcargs: dict[str, object] = {}
self._request = fixtures.TopRequest(self, _ispytest=True)
@property
@ -1671,7 +1662,7 @@ class Function(PyobjMixin, nodes.Item):
def repr_failure( # type: ignore[override]
self,
excinfo: ExceptionInfo[BaseException],
) -> Union[str, TerminalRepr]:
) -> str | TerminalRepr:
style = self.config.getoption("tbstyle", "auto")
if style == "auto":
style = "long"

View File

@ -1,4 +1,6 @@
# mypy: allow-untyped-defs
from __future__ import annotations
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
@ -11,9 +13,7 @@ from typing import Callable
from typing import cast
from typing import ContextManager
from typing import final
from typing import List
from typing import Mapping
from typing import Optional
from typing import overload
from typing import Pattern
from typing import Sequence
@ -21,7 +21,6 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import _pytest._code
from _pytest.outcomes import fail
@ -33,12 +32,12 @@ if TYPE_CHECKING:
def _compare_approx(
full_object: object,
message_data: Sequence[Tuple[str, str, str]],
message_data: Sequence[tuple[str, str, str]],
number_of_elements: int,
different_ids: Sequence[object],
max_abs_diff: float,
max_rel_diff: float,
) -> List[str]:
) -> list[str]:
message_list = list(message_data)
message_list.insert(0, ("Index", "Obtained", "Expected"))
max_sizes = [0, 0, 0]
@ -79,7 +78,7 @@ class ApproxBase:
def __repr__(self) -> str:
raise NotImplementedError
def _repr_compare(self, other_side: Any) -> List[str]:
def _repr_compare(self, other_side: Any) -> list[str]:
return [
"comparison failed",
f"Obtained: {other_side}",
@ -103,7 +102,7 @@ class ApproxBase:
def __ne__(self, actual) -> bool:
return not (actual == self)
def _approx_scalar(self, x) -> "ApproxScalar":
def _approx_scalar(self, x) -> ApproxScalar:
if isinstance(x, Decimal):
return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
@ -144,12 +143,12 @@ class ApproxNumpy(ApproxBase):
)
return f"approx({list_scalars!r})"
def _repr_compare(self, other_side: Union["ndarray", List[Any]]) -> List[str]:
def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]:
import itertools
import math
def get_value_from_nested_list(
nested_list: List[Any], nd_index: Tuple[Any, ...]
nested_list: list[Any], nd_index: tuple[Any, ...]
) -> Any:
"""
Helper function to get the value out of a nested list, given an n-dimensional index.
@ -246,7 +245,7 @@ class ApproxMapping(ApproxBase):
def __repr__(self) -> str:
return f"approx({({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})"
def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]:
def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]:
import math
approx_side_as_map = {
@ -321,7 +320,7 @@ class ApproxSequenceLike(ApproxBase):
seq_type = list
return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})"
def _repr_compare(self, other_side: Sequence[float]) -> List[str]:
def _repr_compare(self, other_side: Sequence[float]) -> list[str]:
import math
if len(self.expected) != len(other_side):
@ -386,8 +385,8 @@ class ApproxScalar(ApproxBase):
# Using Real should be better than this Union, but not possible yet:
# https://github.com/python/typeshed/pull/3108
DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12
DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6
DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12
DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6
def __repr__(self) -> str:
"""Return a string communicating both the expected value and the
@ -717,7 +716,7 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
__tracebackhide__ = True
if isinstance(expected, Decimal):
cls: Type[ApproxBase] = ApproxDecimal
cls: type[ApproxBase] = ApproxDecimal
elif isinstance(expected, Mapping):
cls = ApproxMapping
elif _is_numpy_array(expected):
@ -750,7 +749,7 @@ def _is_numpy_array(obj: object) -> bool:
return _as_numpy_array(obj) is not None
def _as_numpy_array(obj: object) -> Optional["ndarray"]:
def _as_numpy_array(obj: object) -> ndarray | None:
"""
Return an ndarray if the given object is implicitly convertible to ndarray,
and numpy is already imported, otherwise None.
@ -776,15 +775,15 @@ E = TypeVar("E", bound=BaseException)
@overload
def raises(
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
expected_exception: type[E] | tuple[type[E], ...],
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "RaisesContext[E]": ...
match: str | Pattern[str] | None = ...,
) -> RaisesContext[E]: ...
@overload
def raises(
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
expected_exception: type[E] | tuple[type[E], ...],
func: Callable[..., Any],
*args: Any,
**kwargs: Any,
@ -792,8 +791,8 @@ def raises(
def raises(
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
expected_exception: type[E] | tuple[type[E], ...], *args: Any, **kwargs: Any
) -> RaisesContext[E] | _pytest._code.ExceptionInfo[E]:
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
:param expected_exception:
@ -941,7 +940,7 @@ def raises(
f"any special code to say 'this should never raise an exception'."
)
if isinstance(expected_exception, type):
expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
expected_exceptions: tuple[type[E], ...] = (expected_exception,)
else:
expected_exceptions = expected_exception
for exc in expected_exceptions:
@ -953,7 +952,7 @@ def raises(
message = f"DID NOT RAISE {expected_exception}"
if not args:
match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None)
match: str | Pattern[str] | None = kwargs.pop("match", None)
if kwargs:
msg = "Unexpected keyword arguments passed to pytest.raises: "
msg += ", ".join(sorted(kwargs))
@ -979,14 +978,14 @@ raises.Exception = fail.Exception # type: ignore
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __init__(
self,
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
expected_exception: type[E] | tuple[type[E], ...],
message: str,
match_expr: Optional[Union[str, Pattern[str]]] = None,
match_expr: str | Pattern[str] | None = None,
) -> None:
self.expected_exception = expected_exception
self.message = message
self.match_expr = match_expr
self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None
self.excinfo: _pytest._code.ExceptionInfo[E] | None = None
def __enter__(self) -> _pytest._code.ExceptionInfo[E]:
self.excinfo = _pytest._code.ExceptionInfo.for_later()
@ -994,9 +993,9 @@ class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool:
__tracebackhide__ = True
if exc_type is None:

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import sys
import pytest

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Record warnings during test function execution."""
from __future__ import annotations
from pprint import pformat
import re
from types import TracebackType
@ -9,14 +11,15 @@ from typing import Callable
from typing import final
from typing import Generator
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Pattern
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
if TYPE_CHECKING:
from typing_extensions import Self
import warnings
from _pytest.deprecated import check_ispytest
@ -29,7 +32,7 @@ T = TypeVar("T")
@fixture
def recwarn() -> Generator["WarningsRecorder", None, None]:
def recwarn() -> Generator[WarningsRecorder, None, None]:
"""Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
@ -42,9 +45,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
@overload
def deprecated_call(
*, match: Optional[Union[str, Pattern[str]]] = ...
) -> "WarningsRecorder": ...
def deprecated_call(*, match: str | Pattern[str] | None = ...) -> WarningsRecorder: ...
@overload
@ -52,8 +53,8 @@ def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
def deprecated_call(
func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
) -> Union["WarningsRecorder", Any]:
func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any
) -> WarningsRecorder | Any:
"""Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
This function can be used as a context manager::
@ -87,15 +88,15 @@ def deprecated_call(
@overload
def warns(
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
expected_warning: type[Warning] | tuple[type[Warning], ...] = ...,
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "WarningsChecker": ...
match: str | Pattern[str] | None = ...,
) -> WarningsChecker: ...
@overload
def warns(
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
expected_warning: type[Warning] | tuple[type[Warning], ...],
func: Callable[..., T],
*args: Any,
**kwargs: Any,
@ -103,11 +104,11 @@ def warns(
def warns(
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
*args: Any,
match: Optional[Union[str, Pattern[str]]] = None,
match: str | Pattern[str] | None = None,
**kwargs: Any,
) -> Union["WarningsChecker", Any]:
) -> WarningsChecker | Any:
r"""Assert that code raises a particular class of warning.
Specifically, the parameter ``expected_warning`` can be a warning class or tuple
@ -183,18 +184,18 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
check_ispytest(_ispytest)
super().__init__(record=True)
self._entered = False
self._list: List[warnings.WarningMessage] = []
self._list: list[warnings.WarningMessage] = []
@property
def list(self) -> List["warnings.WarningMessage"]:
def list(self) -> list[warnings.WarningMessage]:
"""The list of recorded warnings."""
return self._list
def __getitem__(self, i: int) -> "warnings.WarningMessage":
def __getitem__(self, i: int) -> warnings.WarningMessage:
"""Get a recorded warning by index."""
return self._list[i]
def __iter__(self) -> Iterator["warnings.WarningMessage"]:
def __iter__(self) -> Iterator[warnings.WarningMessage]:
"""Iterate through the recorded warnings."""
return iter(self._list)
@ -202,12 +203,12 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
"""The number of recorded warnings."""
return len(self._list)
def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage":
def pop(self, cls: type[Warning] = Warning) -> warnings.WarningMessage:
"""Pop the first recorded warning which is an instance of ``cls``,
but not an instance of a child class of any other match.
Raises ``AssertionError`` if there is no match.
"""
best_idx: Optional[int] = None
best_idx: int | None = None
for i, w in enumerate(self._list):
if w.category == cls:
return self._list.pop(i) # exact match, stop looking
@ -225,9 +226,7 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
"""Clear the list of recorded warnings."""
self._list[:] = []
# Type ignored because it doesn't exactly warnings.catch_warnings.__enter__
# -- it returns a List but we only emulate one.
def __enter__(self) -> "WarningsRecorder": # type: ignore
def __enter__(self) -> Self:
if self._entered:
__tracebackhide__ = True
raise RuntimeError(f"Cannot enter {self!r} twice")
@ -240,9 +239,9 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
if not self._entered:
__tracebackhide__ = True
@ -259,8 +258,8 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
class WarningsChecker(WarningsRecorder):
def __init__(
self,
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
match_expr: Optional[Union[str, Pattern[str]]] = None,
expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
match_expr: str | Pattern[str] | None = None,
*,
_ispytest: bool = False,
) -> None:
@ -291,9 +290,9 @@ class WarningsChecker(WarningsRecorder):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
super().__exit__(exc_type, exc_val, exc_tb)

View File

@ -1,24 +1,19 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import dataclasses
from io import StringIO
import os
from pprint import pprint
from typing import Any
from typing import cast
from typing import Dict
from typing import final
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Literal
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
@ -39,6 +34,8 @@ from _pytest.outcomes import skip
if TYPE_CHECKING:
from typing_extensions import Self
from _pytest.runner import CallInfo
@ -54,16 +51,13 @@ def getworkerinfoline(node):
return s
_R = TypeVar("_R", bound="BaseReport")
class BaseReport:
when: Optional[str]
location: Optional[Tuple[str, Optional[int], str]]
longrepr: Union[
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
]
sections: List[Tuple[str, str]]
when: str | None
location: tuple[str, int | None, str] | None
longrepr: (
None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr
)
sections: list[tuple[str, str]]
nodeid: str
outcome: Literal["passed", "failed", "skipped"]
@ -94,7 +88,7 @@ class BaseReport:
s = "<unprintable longrepr>"
out.line(s)
def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]:
def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]:
for name, content in self.sections:
if name.startswith(prefix):
yield prefix, content
@ -176,7 +170,7 @@ class BaseReport:
return True
@property
def head_line(self) -> Optional[str]:
def head_line(self) -> str | None:
"""**Experimental** The head line shown with longrepr output for this
report, more commonly during traceback representation during
failures::
@ -202,7 +196,7 @@ class BaseReport:
)
return verbose
def _to_json(self) -> Dict[str, Any]:
def _to_json(self) -> dict[str, Any]:
"""Return the contents of this report as a dict of builtin entries,
suitable for serialization.
@ -213,7 +207,7 @@ class BaseReport:
return _report_to_json(self)
@classmethod
def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R:
def _from_json(cls, reportdict: dict[str, object]) -> Self:
"""Create either a TestReport or CollectReport, depending on the calling class.
It is the callers responsibility to know which class to pass here.
@ -227,7 +221,7 @@ class BaseReport:
def _report_unserialization_failure(
type_name: str, report_class: Type[BaseReport], reportdict
type_name: str, report_class: type[BaseReport], reportdict
) -> NoReturn:
url = "https://github.com/pytest-dev/pytest/issues"
stream = StringIO()
@ -256,18 +250,20 @@ class TestReport(BaseReport):
def __init__(
self,
nodeid: str,
location: Tuple[str, Optional[int], str],
location: tuple[str, int | None, str],
keywords: Mapping[str, Any],
outcome: Literal["passed", "failed", "skipped"],
longrepr: Union[
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
],
longrepr: None
| ExceptionInfo[BaseException]
| tuple[str, int, str]
| str
| TerminalRepr,
when: Literal["setup", "call", "teardown"],
sections: Iterable[Tuple[str, str]] = (),
sections: Iterable[tuple[str, str]] = (),
duration: float = 0,
start: float = 0,
stop: float = 0,
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
user_properties: Iterable[tuple[str, object]] | None = None,
**extra,
) -> None:
#: Normalized collection nodeid.
@ -278,7 +274,7 @@ class TestReport(BaseReport):
#: collected one e.g. if a method is inherited from a different module.
#: The filesystempath may be relative to ``config.rootdir``.
#: The line number is 0-based.
self.location: Tuple[str, Optional[int], str] = location
self.location: tuple[str, int | None, str] = location
#: A name -> value dictionary containing all keywords and
#: markers associated with a test invocation.
@ -317,7 +313,7 @@ class TestReport(BaseReport):
return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>"
@classmethod
def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport:
"""Create and fill a TestReport with standard item and call info.
:param item: The item.
@ -334,13 +330,13 @@ class TestReport(BaseReport):
sections = []
if not call.excinfo:
outcome: Literal["passed", "failed", "skipped"] = "passed"
longrepr: Union[
None,
ExceptionInfo[BaseException],
Tuple[str, int, str],
str,
TerminalRepr,
] = None
longrepr: (
None
| ExceptionInfo[BaseException]
| tuple[str, int, str]
| str
| TerminalRepr
) = None
else:
if not isinstance(excinfo, ExceptionInfo):
outcome = "failed"
@ -394,12 +390,14 @@ class CollectReport(BaseReport):
def __init__(
self,
nodeid: str,
outcome: "Literal['passed', 'failed', 'skipped']",
longrepr: Union[
None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr
],
result: Optional[List[Union[Item, Collector]]],
sections: Iterable[Tuple[str, str]] = (),
outcome: Literal["passed", "failed", "skipped"],
longrepr: None
| ExceptionInfo[BaseException]
| tuple[str, int, str]
| str
| TerminalRepr,
result: list[Item | Collector] | None,
sections: Iterable[tuple[str, str]] = (),
**extra,
) -> None:
#: Normalized collection nodeid.
@ -425,7 +423,7 @@ class CollectReport(BaseReport):
@property
def location( # type:ignore[override]
self,
) -> Optional[Tuple[str, Optional[int], str]]:
) -> tuple[str, int | None, str] | None:
return (self.fspath, None, self.fspath)
def __repr__(self) -> str:
@ -441,8 +439,8 @@ class CollectErrorRepr(TerminalRepr):
def pytest_report_to_serializable(
report: Union[CollectReport, TestReport],
) -> Optional[Dict[str, Any]]:
report: CollectReport | TestReport,
) -> dict[str, Any] | None:
if isinstance(report, (TestReport, CollectReport)):
data = report._to_json()
data["$report_type"] = report.__class__.__name__
@ -452,8 +450,8 @@ def pytest_report_to_serializable(
def pytest_report_from_serializable(
data: Dict[str, Any],
) -> Optional[Union[CollectReport, TestReport]]:
data: dict[str, Any],
) -> CollectReport | TestReport | None:
if "$report_type" in data:
if data["$report_type"] == "TestReport":
return TestReport._from_json(data)
@ -465,7 +463,7 @@ def pytest_report_from_serializable(
return None
def _report_to_json(report: BaseReport) -> Dict[str, Any]:
def _report_to_json(report: BaseReport) -> dict[str, Any]:
"""Return the contents of this report as a dict of builtin entries,
suitable for serialization.
@ -473,8 +471,8 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
"""
def serialize_repr_entry(
entry: Union[ReprEntry, ReprEntryNative],
) -> Dict[str, Any]:
entry: ReprEntry | ReprEntryNative,
) -> dict[str, Any]:
data = dataclasses.asdict(entry)
for key, value in data.items():
if hasattr(value, "__dict__"):
@ -482,7 +480,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
entry_data = {"type": type(entry).__name__, "data": data}
return entry_data
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]:
result = dataclasses.asdict(reprtraceback)
result["reprentries"] = [
serialize_repr_entry(x) for x in reprtraceback.reprentries
@ -490,18 +488,18 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
return result
def serialize_repr_crash(
reprcrash: Optional[ReprFileLocation],
) -> Optional[Dict[str, Any]]:
reprcrash: ReprFileLocation | None,
) -> dict[str, Any] | None:
if reprcrash is not None:
return dataclasses.asdict(reprcrash)
else:
return None
def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]:
assert rep.longrepr is not None
# TODO: Investigate whether the duck typing is really necessary here.
longrepr = cast(ExceptionRepr, rep.longrepr)
result: Dict[str, Any] = {
result: dict[str, Any] = {
"reprcrash": serialize_repr_crash(longrepr.reprcrash),
"reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
"sections": longrepr.sections,
@ -538,7 +536,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
return d
def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
"""Return **kwargs that can be used to construct a TestReport or
CollectReport instance.
@ -559,7 +557,7 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
if data["reprlocals"]:
reprlocals = ReprLocals(data["reprlocals"]["lines"])
reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry(
reprentry: ReprEntry | ReprEntryNative = ReprEntry(
lines=data["lines"],
reprfuncargs=reprfuncargs,
reprlocals=reprlocals,
@ -578,7 +576,7 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
]
return ReprTraceback(**repr_traceback_dict)
def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]):
def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None):
if repr_crash_dict is not None:
return ReprFileLocation(**repr_crash_dict)
else:
@ -605,8 +603,8 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
description,
)
)
exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = (
ExceptionChainRepr(chain)
exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr(
chain
)
else:
exception_info = ReprExceptionInfo(

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Basic collect and runtest protocol implementations."""
from __future__ import annotations
import bdb
import dataclasses
import os
@ -8,17 +10,11 @@ import sys
import types
from typing import Callable
from typing import cast
from typing import Dict
from typing import final
from typing import Generic
from typing import List
from typing import Literal
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from .reports import BaseReport
from .reports import CollectErrorRepr
@ -72,7 +68,7 @@ def pytest_addoption(parser: Parser) -> None:
)
def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
durations = terminalreporter.config.option.durations
durations_min = terminalreporter.config.option.durations_min
verbose = terminalreporter.config.getvalue("verbose")
@ -103,15 +99,15 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
def pytest_sessionstart(session: "Session") -> None:
def pytest_sessionstart(session: Session) -> None:
session._setupstate = SetupState()
def pytest_sessionfinish(session: "Session") -> None:
def pytest_sessionfinish(session: Session) -> None:
session._setupstate.teardown_exact(None)
def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool:
ihook = item.ihook
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
runtestprotocol(item, nextitem=nextitem)
@ -120,8 +116,8 @@ def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool:
def runtestprotocol(
item: Item, log: bool = True, nextitem: Optional[Item] = None
) -> List[TestReport]:
item: Item, log: bool = True, nextitem: Item | None = None
) -> list[TestReport]:
hasrequest = hasattr(item, "_request")
if hasrequest and not item._request: # type: ignore[attr-defined]
# This only happens if the item is re-run, as is done by
@ -188,14 +184,14 @@ def pytest_runtest_call(item: Item) -> None:
raise
def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None:
def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
_update_current_test_var(item, "teardown")
item.session._setupstate.teardown_exact(nextitem)
_update_current_test_var(item, None)
def _update_current_test_var(
item: Item, when: Optional[Literal["setup", "call", "teardown"]]
item: Item, when: Literal["setup", "call", "teardown"] | None
) -> None:
"""Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
@ -211,7 +207,7 @@ def _update_current_test_var(
os.environ.pop(var_name)
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
if report.when in ("setup", "teardown"):
if report.failed:
# category, shortletter, verbose-word
@ -239,7 +235,7 @@ def call_and_report(
runtest_hook = ihook.pytest_runtest_teardown
else:
assert False, f"Unhandled runtest hook case: {when}"
reraise: Tuple[Type[BaseException], ...] = (Exit,)
reraise: tuple[type[BaseException], ...] = (Exit,)
if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,)
call = CallInfo.from_call(
@ -253,7 +249,7 @@ def call_and_report(
return report
def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool:
def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool:
"""Check whether the call raised an exception that should be reported as
interactive."""
if call.excinfo is None:
@ -276,9 +272,9 @@ TResult = TypeVar("TResult", covariant=True)
class CallInfo(Generic[TResult]):
"""Result/Exception info of a function invocation."""
_result: Optional[TResult]
_result: TResult | None
#: The captured exception of the call, if it raised.
excinfo: Optional[ExceptionInfo[BaseException]]
excinfo: ExceptionInfo[BaseException] | None
#: The system time when the call started, in seconds since the epoch.
start: float
#: The system time when the call ended, in seconds since the epoch.
@ -290,8 +286,8 @@ class CallInfo(Generic[TResult]):
def __init__(
self,
result: Optional[TResult],
excinfo: Optional[ExceptionInfo[BaseException]],
result: TResult | None,
excinfo: ExceptionInfo[BaseException] | None,
start: float,
stop: float,
duration: float,
@ -325,14 +321,13 @@ class CallInfo(Generic[TResult]):
cls,
func: Callable[[], TResult],
when: Literal["collect", "setup", "call", "teardown"],
reraise: Optional[
Union[Type[BaseException], Tuple[Type[BaseException], ...]]
] = None,
) -> "CallInfo[TResult]":
reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
) -> CallInfo[TResult]:
"""Call func, wrapping the result in a CallInfo.
:param func:
The function to call. Called without arguments.
:type func: Callable[[], _pytest.runner.TResult]
:param when:
The phase in which the function is called.
:param reraise:
@ -343,7 +338,7 @@ class CallInfo(Generic[TResult]):
start = timing.time()
precise_start = timing.perf_counter()
try:
result: Optional[TResult] = func()
result: TResult | None = func()
except BaseException:
excinfo = ExceptionInfo.from_current()
if reraise is not None and isinstance(excinfo.value, reraise):
@ -374,7 +369,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
def pytest_make_collect_report(collector: Collector) -> CollectReport:
def collect() -> List[Union[Item, Collector]]:
def collect() -> list[Item | Collector]:
# Before collecting, if this is a Directory, load the conftests.
# If a conftest import fails to load, it is considered a collection
# error of the Directory collector. This is why it's done inside of the
@ -396,7 +391,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
call = CallInfo.from_call(
collect, "collect", reraise=(KeyboardInterrupt, SystemExit)
)
longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
longrepr: None | tuple[str, int, str] | str | TerminalRepr = None
if not call.excinfo:
outcome: Literal["passed", "skipped", "failed"] = "passed"
else:
@ -490,18 +485,13 @@ class SetupState:
def __init__(self) -> None:
# The stack is in the dict insertion order.
self.stack: Dict[
self.stack: dict[
Node,
Tuple[
tuple[
# Node's finalizers.
List[Callable[[], object]],
list[Callable[[], object]],
# Node's exception and original traceback, if its setup raised.
Optional[
Tuple[
Union[OutcomeException, Exception],
Optional[types.TracebackType],
]
],
tuple[OutcomeException | Exception, types.TracebackType | None] | None,
],
] = {}
@ -536,7 +526,7 @@ class SetupState:
assert node in self.stack, (node, self.stack)
self.stack[node][0].append(finalizer)
def teardown_exact(self, nextitem: Optional[Item]) -> None:
def teardown_exact(self, nextitem: Item | None) -> None:
"""Teardown the current stack up until reaching nodes that nextitem
also descends from.
@ -544,7 +534,7 @@ class SetupState:
stack is torn down.
"""
needed_collectors = nextitem and nextitem.listchain() or []
exceptions: List[BaseException] = []
exceptions: list[BaseException] = []
while self.stack:
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
break

View File

@ -8,10 +8,11 @@ would cause circular references.
Also this makes the module light to import, as it should.
"""
from __future__ import annotations
from enum import Enum
from functools import total_ordering
from typing import Literal
from typing import Optional
_ScopeName = Literal["session", "package", "module", "class", "function"]
@ -38,29 +39,29 @@ class Scope(Enum):
Package: _ScopeName = "package"
Session: _ScopeName = "session"
def next_lower(self) -> "Scope":
def next_lower(self) -> Scope:
"""Return the next lower scope."""
index = _SCOPE_INDICES[self]
if index == 0:
raise ValueError(f"{self} is the lower-most scope")
return _ALL_SCOPES[index - 1]
def next_higher(self) -> "Scope":
def next_higher(self) -> Scope:
"""Return the next higher scope."""
index = _SCOPE_INDICES[self]
if index == len(_SCOPE_INDICES) - 1:
raise ValueError(f"{self} is the upper-most scope")
return _ALL_SCOPES[index + 1]
def __lt__(self, other: "Scope") -> bool:
def __lt__(self, other: Scope) -> bool:
self_index = _SCOPE_INDICES[self]
other_index = _SCOPE_INDICES[other]
return self_index < other_index
@classmethod
def from_user(
cls, scope_name: _ScopeName, descr: str, where: Optional[str] = None
) -> "Scope":
cls, scope_name: _ScopeName, descr: str, where: str | None = None
) -> Scope:
"""
Given a scope name from the user, return the equivalent Scope enum. Should be used
whenever we want to convert a user provided scope name to its enum object.

View File

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Generator
from typing import Optional
from typing import Union
from _pytest._io.saferepr import saferepr
from _pytest.config import Config
@ -96,7 +96,7 @@ def _show_fixture_action(
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.setuponly:
config.option.setupshow = True
return None

View File

@ -1,5 +1,4 @@
from typing import Optional
from typing import Union
from __future__ import annotations
from _pytest.config import Config
from _pytest.config import ExitCode
@ -23,7 +22,7 @@ def pytest_addoption(parser: Parser) -> None:
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(
fixturedef: FixtureDef[object], request: SubRequest
) -> Optional[object]:
) -> object | None:
# Will return a dummy fixture if the setuponly option is provided.
if request.config.option.setupplan:
my_cache_key = fixturedef.cache_key(request)
@ -33,7 +32,7 @@ def pytest_fixture_setup(
@pytest.hookimpl(tryfirst=True)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
if config.option.setupplan:
config.option.setuponly = True
config.option.setupshow = True

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Support for skip/xfail functions and markers."""
from __future__ import annotations
from collections.abc import Mapping
import dataclasses
import os
@ -9,8 +11,6 @@ import sys
import traceback
from typing import Generator
from typing import Optional
from typing import Tuple
from typing import Type
from _pytest.config import Config
from _pytest.config import hookimpl
@ -84,7 +84,7 @@ def pytest_configure(config: Config) -> None:
)
def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]:
def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]:
"""Evaluate a single skipif/xfail condition.
If an old-style string condition is given, it is eval()'d, otherwise the
@ -164,7 +164,7 @@ class Skip:
reason: str = "unconditional skip"
def evaluate_skip_marks(item: Item) -> Optional[Skip]:
def evaluate_skip_marks(item: Item) -> Skip | None:
"""Evaluate skip and skipif marks on item, returning Skip if triggered."""
for mark in item.iter_markers(name="skipif"):
if "condition" not in mark.kwargs:
@ -201,10 +201,10 @@ class Xfail:
reason: str
run: bool
strict: bool
raises: Optional[Tuple[Type[BaseException], ...]]
raises: tuple[type[BaseException], ...] | None
def evaluate_xfail_marks(item: Item) -> Optional[Xfail]:
def evaluate_xfail_marks(item: Item) -> Xfail | None:
"""Evaluate xfail marks on item, returning Xfail if triggered."""
for mark in item.iter_markers(name="xfail"):
run = mark.kwargs.get("run", True)
@ -292,7 +292,7 @@ def pytest_runtest_makereport(
return rep
def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]:
def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
if hasattr(report, "wasxfail"):
if report.skipped:
return "xfailed", "x", "XFAIL"

View File

@ -1,9 +1,9 @@
from __future__ import annotations
from typing import Any
from typing import cast
from typing import Dict
from typing import Generic
from typing import TypeVar
from typing import Union
__all__ = ["Stash", "StashKey"]
@ -70,7 +70,7 @@ class Stash:
__slots__ = ("_storage",)
def __init__(self) -> None:
self._storage: Dict[StashKey[Any], object] = {}
self._storage: dict[StashKey[Any], object] = {}
def __setitem__(self, key: StashKey[T], value: T) -> None:
"""Set a value for key."""
@ -83,7 +83,7 @@ class Stash:
"""
return cast(T, self._storage[key])
def get(self, key: StashKey[T], default: D) -> Union[T, D]:
def get(self, key: StashKey[T], default: D) -> T | D:
"""Get the value for key, or return default if the key wasn't set
before."""
try:

View File

@ -1,5 +1,4 @@
from typing import List
from typing import Optional
from __future__ import annotations
from _pytest import nodes
from _pytest.cacheprovider import Cache
@ -55,18 +54,18 @@ def pytest_sessionfinish(session: Session) -> None:
class StepwisePlugin:
def __init__(self, config: Config) -> None:
self.config = config
self.session: Optional[Session] = None
self.session: Session | None = None
self.report_status = ""
assert config.cache is not None
self.cache: Cache = config.cache
self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None)
self.lastfailed: str | None = self.cache.get(STEPWISE_CACHE_DIR, None)
self.skip: bool = config.getoption("stepwise_skip")
def pytest_sessionstart(self, session: Session) -> None:
self.session = session
def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item]
self, config: Config, items: list[nodes.Item]
) -> None:
if not self.lastfailed:
self.report_status = "no previously failed tests, not skipping."
@ -113,7 +112,7 @@ class StepwisePlugin:
if report.nodeid == self.lastfailed:
self.lastfailed = None
def pytest_report_collectionfinish(self) -> Optional[str]:
def pytest_report_collectionfinish(self) -> str | None:
if self.config.getoption("verbose") >= 0 and self.report_status:
return f"stepwise: {self.report_status}"
return None

View File

@ -4,6 +4,8 @@
This is a good source for looking at the various reporting hooks.
"""
from __future__ import annotations
import argparse
from collections import Counter
import dataclasses
@ -17,20 +19,14 @@ import textwrap
from typing import Any
from typing import Callable
from typing import ClassVar
from typing import Dict
from typing import final
from typing import Generator
from typing import List
from typing import Literal
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
from typing import TextIO
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import warnings
import pluggy
@ -90,7 +86,7 @@ class MoreQuietAction(argparse.Action):
dest: str,
default: object = None,
required: bool = False,
help: Optional[str] = None,
help: str | None = None,
) -> None:
super().__init__(
option_strings=option_strings,
@ -105,8 +101,8 @@ class MoreQuietAction(argparse.Action):
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: Union[str, Sequence[object], None],
option_string: Optional[str] = None,
values: str | Sequence[object] | None,
option_string: str | None = None,
) -> None:
new_count = getattr(namespace, self.dest, 0) - 1
setattr(namespace, self.dest, new_count)
@ -131,7 +127,7 @@ class TestShortLogReport(NamedTuple):
category: str
letter: str
word: Union[str, Tuple[str, Mapping[str, bool]]]
word: str | tuple[str, Mapping[str, bool]]
def pytest_addoption(parser: Parser) -> None:
@ -311,7 +307,7 @@ def getreportopt(config: Config) -> str:
@hookimpl(trylast=True) # after _pytest.runner
def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str]:
letter = "F"
if report.passed:
letter = "."
@ -339,12 +335,12 @@ class WarningReport:
"""
message: str
nodeid: Optional[str] = None
fslocation: Optional[Tuple[str, int]] = None
nodeid: str | None = None
fslocation: tuple[str, int] | None = None
count_towards_summary: ClassVar = True
def get_location(self, config: Config) -> Optional[str]:
def get_location(self, config: Config) -> str | None:
"""Return the more user-friendly information about the location of a warning, or None."""
if self.nodeid:
return self.nodeid
@ -357,31 +353,31 @@ class WarningReport:
@final
class TerminalReporter:
def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
def __init__(self, config: Config, file: TextIO | None = None) -> None:
import _pytest.config
self.config = config
self._numcollected = 0
self._session: Optional[Session] = None
self._showfspath: Optional[bool] = None
self._session: Session | None = None
self._showfspath: bool | None = None
self.stats: Dict[str, List[Any]] = {}
self._main_color: Optional[str] = None
self._known_types: Optional[List[str]] = None
self.stats: dict[str, list[Any]] = {}
self._main_color: str | None = None
self._known_types: list[str] | None = None
self.startpath = config.invocation_params.dir
if file is None:
file = sys.stdout
self._tw = _pytest.config.create_terminal_writer(config, file)
self._screen_width = self._tw.fullwidth
self.currentfspath: Union[None, Path, str, int] = None
self.currentfspath: None | Path | str | int = None
self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty()
self._progress_nodeids_reported: Set[str] = set()
self._progress_nodeids_reported: set[str] = set()
self._show_progress_info = self._determine_show_progress_info()
self._collect_report_last_write: Optional[float] = None
self._already_displayed_warnings: Optional[int] = None
self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None
self._collect_report_last_write: float | None = None
self._already_displayed_warnings: int | None = None
self._keyboardinterrupt_memo: ExceptionRepr | None = None
def _determine_show_progress_info(self) -> Literal["progress", "count", False]:
"""Return whether we should display progress information based on the current config."""
@ -428,7 +424,7 @@ class TerminalReporter:
return self._showfspath
@showfspath.setter
def showfspath(self, value: Optional[bool]) -> None:
def showfspath(self, value: bool | None) -> None:
self._showfspath = value
@property
@ -492,7 +488,7 @@ class TerminalReporter:
def flush(self) -> None:
self._tw.flush()
def write_line(self, line: Union[str, bytes], **markup: bool) -> None:
def write_line(self, line: str | bytes, **markup: bool) -> None:
if not isinstance(line, str):
line = str(line, errors="replace")
self.ensure_newline()
@ -519,8 +515,8 @@ class TerminalReporter:
def write_sep(
self,
sep: str,
title: Optional[str] = None,
fullwidth: Optional[int] = None,
title: str | None = None,
fullwidth: int | None = None,
**markup: bool,
) -> None:
self.ensure_newline()
@ -570,7 +566,7 @@ class TerminalReporter:
self._add_stats("deselected", items)
def pytest_runtest_logstart(
self, nodeid: str, location: Tuple[str, Optional[int], str]
self, nodeid: str, location: tuple[str, int | None, str]
) -> None:
fspath, lineno, domain = location
# Ensure that the path is printed before the
@ -777,7 +773,7 @@ class TerminalReporter:
self.write_line(line)
@hookimpl(trylast=True)
def pytest_sessionstart(self, session: "Session") -> None:
def pytest_sessionstart(self, session: Session) -> None:
self._session = session
self._sessionstarttime = timing.time()
if not self.showheader:
@ -804,7 +800,7 @@ class TerminalReporter:
self._write_report_lines_from_hooks(lines)
def _write_report_lines_from_hooks(
self, lines: Sequence[Union[str, Sequence[str]]]
self, lines: Sequence[str | Sequence[str]]
) -> None:
for line_or_lines in reversed(lines):
if isinstance(line_or_lines, str):
@ -813,14 +809,14 @@ class TerminalReporter:
for line in line_or_lines:
self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]:
def pytest_report_header(self, config: Config) -> list[str]:
result = [f"rootdir: {config.rootpath}"]
if config.inipath:
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: List[str] = config.getini("testpaths")
testpaths: list[str] = config.getini("testpaths")
result.append("testpaths: {}".format(", ".join(testpaths)))
plugininfo = config.pluginmanager.list_plugin_distinfo()
@ -830,7 +826,7 @@ class TerminalReporter:
)
return result
def pytest_collection_finish(self, session: "Session") -> None:
def pytest_collection_finish(self, session: Session) -> None:
self.report_collect(True)
lines = self.config.hook.pytest_report_collectionfinish(
@ -863,7 +859,7 @@ class TerminalReporter:
for item in items:
self._tw.line(item.nodeid)
return
stack: List[Node] = []
stack: list[Node] = []
indent = ""
for item in items:
needed_collectors = item.listchain()[1:] # strip root node
@ -884,7 +880,7 @@ class TerminalReporter:
@hookimpl(wrapper=True)
def pytest_sessionfinish(
self, session: "Session", exitstatus: Union[int, ExitCode]
self, session: Session, exitstatus: int | ExitCode
) -> Generator[None, None, None]:
result = yield
self._tw.line("")
@ -948,7 +944,7 @@ class TerminalReporter:
)
def _locationline(
self, nodeid: str, fspath: str, lineno: Optional[int], domain: str
self, nodeid: str, fspath: str, lineno: int | None, domain: str
) -> str:
def mkrel(nodeid: str) -> str:
line = self.config.cwd_relative_nodeid(nodeid)
@ -993,7 +989,7 @@ class TerminalReporter:
def summary_warnings(self) -> None:
if self.hasopt("w"):
all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings")
all_warnings: list[WarningReport] | None = self.stats.get("warnings")
if not all_warnings:
return
@ -1006,11 +1002,11 @@ class TerminalReporter:
if not warning_reports:
return
reports_grouped_by_message: Dict[str, List[WarningReport]] = {}
reports_grouped_by_message: dict[str, list[WarningReport]] = {}
for wr in warning_reports:
reports_grouped_by_message.setdefault(wr.message, []).append(wr)
def collapsed_location_report(reports: List[WarningReport]) -> str:
def collapsed_location_report(reports: list[WarningReport]) -> str:
locations = []
for w in reports:
location = w.get_location(self.config)
@ -1056,7 +1052,7 @@ class TerminalReporter:
) -> None:
if self.config.option.tbstyle != "no":
if self.hasopt(needed_opt):
reports: List[TestReport] = self.getreports(which_reports)
reports: list[TestReport] = self.getreports(which_reports)
if not reports:
return
self.write_sep("=", sep_title)
@ -1067,7 +1063,7 @@ class TerminalReporter:
self._outrep_summary(rep)
self._handle_teardown_sections(rep.nodeid)
def _get_teardown_reports(self, nodeid: str) -> List[TestReport]:
def _get_teardown_reports(self, nodeid: str) -> list[TestReport]:
reports = self.getreports("")
return [
report
@ -1107,11 +1103,11 @@ class TerminalReporter:
sep_title: str,
*,
style: str,
needed_opt: Optional[str] = None,
needed_opt: str | None = None,
) -> None:
if style != "no":
if not needed_opt or self.hasopt(needed_opt):
reports: List[BaseReport] = self.getreports(which_reports)
reports: list[BaseReport] = self.getreports(which_reports)
if not reports:
return
self.write_sep("=", sep_title)
@ -1128,7 +1124,7 @@ class TerminalReporter:
def summary_errors(self) -> None:
if self.config.option.tbstyle != "no":
reports: List[BaseReport] = self.getreports("error")
reports: list[BaseReport] = self.getreports("error")
if not reports:
return
self.write_sep("=", "ERRORS")
@ -1195,7 +1191,7 @@ class TerminalReporter:
if not self.reportchars:
return
def show_simple(lines: List[str], *, stat: str) -> None:
def show_simple(lines: list[str], *, stat: str) -> None:
failed = self.stats.get(stat, [])
if not failed:
return
@ -1207,7 +1203,7 @@ class TerminalReporter:
)
lines.append(line)
def show_xfailed(lines: List[str]) -> None:
def show_xfailed(lines: list[str]) -> None:
xfailed = self.stats.get("xfailed", [])
for rep in xfailed:
verbose_word = rep._get_verbose_word(self.config)
@ -1222,7 +1218,7 @@ class TerminalReporter:
lines.append(line)
def show_xpassed(lines: List[str]) -> None:
def show_xpassed(lines: list[str]) -> None:
xpassed = self.stats.get("xpassed", [])
for rep in xpassed:
verbose_word = rep._get_verbose_word(self.config)
@ -1236,8 +1232,8 @@ class TerminalReporter:
line += " - " + str(reason)
lines.append(line)
def show_skipped(lines: List[str]) -> None:
skipped: List[CollectReport] = self.stats.get("skipped", [])
def show_skipped(lines: list[str]) -> None:
skipped: list[CollectReport] = self.stats.get("skipped", [])
fskips = _folded_skips(self.startpath, skipped) if skipped else []
if not fskips:
return
@ -1256,7 +1252,7 @@ class TerminalReporter:
else:
lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason))
REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = {
REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = {
"x": show_xfailed,
"X": show_xpassed,
"f": partial(show_simple, stat="failed"),
@ -1265,7 +1261,7 @@ class TerminalReporter:
"E": partial(show_simple, stat="error"),
}
lines: List[str] = []
lines: list[str] = []
for char in self.reportchars:
action = REPORTCHAR_ACTIONS.get(char)
if action: # skipping e.g. "P" (passed with output) here.
@ -1276,7 +1272,7 @@ class TerminalReporter:
for line in lines:
self.write_line(line)
def _get_main_color(self) -> Tuple[str, List[str]]:
def _get_main_color(self) -> tuple[str, list[str]]:
if self._main_color is None or self._known_types is None or self._is_last_item:
self._set_main_color()
assert self._main_color
@ -1296,7 +1292,7 @@ class TerminalReporter:
return main_color
def _set_main_color(self) -> None:
unknown_types: List[str] = []
unknown_types: list[str] = []
for found_type in self.stats:
if found_type: # setup/teardown reports have an empty key, ignore them
if found_type not in KNOWN_TYPES and found_type not in unknown_types:
@ -1304,7 +1300,7 @@ class TerminalReporter:
self._known_types = list(KNOWN_TYPES) + unknown_types
self._main_color = self._determine_main_color(bool(unknown_types))
def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], str]:
"""
Build the parts used in the last summary stats line.
@ -1329,14 +1325,14 @@ class TerminalReporter:
else:
return self._build_normal_summary_stats_line()
def _get_reports_to_display(self, key: str) -> List[Any]:
def _get_reports_to_display(self, key: str) -> list[Any]:
"""Get test/collection reports for the given status key, such as `passed` or `error`."""
reports = self.stats.get(key, [])
return [x for x in reports if getattr(x, "count_towards_summary", True)]
def _build_normal_summary_stats_line(
self,
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
) -> tuple[list[tuple[str, dict[str, bool]]], str]:
main_color, known_types = self._get_main_color()
parts = []
@ -1355,7 +1351,7 @@ class TerminalReporter:
def _build_collect_only_summary_stats_line(
self,
) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]:
) -> tuple[list[tuple[str, dict[str, bool]]], str]:
deselected = len(self._get_reports_to_display("deselected"))
errors = len(self._get_reports_to_display("error"))
@ -1396,7 +1392,7 @@ def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport
return path
def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]:
def _format_trimmed(format: str, msg: str, available_width: int) -> str | None:
"""Format msg into format, ellipsizing it if doesn't fit in available_width.
Returns None if even the ellipsis can't fit.
@ -1422,7 +1418,7 @@ def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str
def _get_line_with_reprcrash_message(
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: Dict[str, bool]
config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
) -> str:
"""Get summary line for a report, trying to add reprcrash message."""
verbose_word = rep._get_verbose_word(config)
@ -1452,8 +1448,8 @@ def _get_line_with_reprcrash_message(
def _folded_skips(
startpath: Path,
skipped: Sequence[CollectReport],
) -> List[Tuple[int, str, Optional[int], str]]:
d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {}
) -> list[tuple[int, str, int | None, str]]:
d: dict[tuple[str, int | None, str], list[CollectReport]] = {}
for event in skipped:
assert event.longrepr is not None
assert isinstance(event.longrepr, tuple), (event, event.longrepr)
@ -1470,11 +1466,11 @@ def _folded_skips(
and "skip" in keywords
and "pytestmark" not in keywords
):
key: Tuple[str, Optional[int], str] = (fspath, None, reason)
key: tuple[str, int | None, str] = (fspath, None, reason)
else:
key = (fspath, lineno, reason)
d.setdefault(key, []).append(event)
values: List[Tuple[int, str, Optional[int], str]] = []
values: list[tuple[int, str, int | None, str]] = []
for key, events in d.items():
values.append((len(events), *key))
return values
@ -1489,7 +1485,7 @@ _color_for_type = {
_color_for_type_default = "yellow"
def pluralize(count: int, noun: str) -> Tuple[int, str]:
def pluralize(count: int, noun: str) -> tuple[int, str]:
# No need to pluralize words such as `failed` or `passed`.
if noun not in ["error", "warnings", "test"]:
return count, noun
@ -1502,8 +1498,8 @@ def pluralize(count: int, noun: str) -> Tuple[int, str]:
return count, noun + "s" if count != 1 else noun
def _plugin_nameversions(plugininfo) -> List[str]:
values: List[str] = []
def _plugin_nameversions(plugininfo) -> list[str]:
values: list[str] = []
for plugin, dist in plugininfo:
# Gets us name and version!
name = f"{dist.project_name}-{dist.version}"

View File

@ -1,16 +1,21 @@
from __future__ import annotations
import threading
import traceback
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Generator
from typing import Optional
from typing import Type
from typing import TYPE_CHECKING
import warnings
import pytest
if TYPE_CHECKING:
from typing_extensions import Self
# Copied from cpython/Lib/test/support/threading_helper.py, with modifications.
class catch_threading_exception:
"""Context manager catching threading.Thread exception using
@ -34,22 +39,22 @@ class catch_threading_exception:
"""
def __init__(self) -> None:
self.args: Optional[threading.ExceptHookArgs] = None
self._old_hook: Optional[Callable[[threading.ExceptHookArgs], Any]] = None
self.args: threading.ExceptHookArgs | None = None
self._old_hook: Callable[[threading.ExceptHookArgs], Any] | None = None
def _hook(self, args: "threading.ExceptHookArgs") -> None:
def _hook(self, args: threading.ExceptHookArgs) -> None:
self.args = args
def __enter__(self) -> "catch_threading_exception":
def __enter__(self) -> Self:
self._old_hook = threading.excepthook
threading.excepthook = self._hook
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
assert self._old_hook is not None
threading.excepthook = self._old_hook

View File

@ -6,6 +6,8 @@ pytest runtime information (issue #185).
Fixture "mock_timing" also interacts with this module for pytest's own tests.
"""
from __future__ import annotations
from time import perf_counter
from time import sleep
from time import time

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Support for providing temporary directories to test functions."""
from __future__ import annotations
import dataclasses
import os
from pathlib import Path
@ -12,8 +14,6 @@ from typing import Dict
from typing import final
from typing import Generator
from typing import Literal
from typing import Optional
from typing import Union
from .pathlib import cleanup_dead_symlinks
from .pathlib import LOCK_TIMEOUT
@ -46,20 +46,20 @@ class TempPathFactory:
The base directory can be configured using the ``--basetemp`` option.
"""
_given_basetemp: Optional[Path]
_given_basetemp: Path | None
# pluggy TagTracerSub, not currently exposed, so Any.
_trace: Any
_basetemp: Optional[Path]
_basetemp: Path | None
_retention_count: int
_retention_policy: RetentionType
def __init__(
self,
given_basetemp: Optional[Path],
given_basetemp: Path | None,
retention_count: int,
retention_policy: RetentionType,
trace,
basetemp: Optional[Path] = None,
basetemp: Path | None = None,
*,
_ispytest: bool = False,
) -> None:
@ -82,7 +82,7 @@ class TempPathFactory:
config: Config,
*,
_ispytest: bool = False,
) -> "TempPathFactory":
) -> TempPathFactory:
"""Create a factory according to pytest configuration.
:meta private:
@ -198,7 +198,7 @@ class TempPathFactory:
return basetemp
def get_user() -> Optional[str]:
def get_user() -> str | None:
"""Return the current user name, or None if getuser() does not work
in the current environment (see #1010)."""
try:
@ -286,7 +286,7 @@ def tmp_path(
del request.node.stash[tmppath_result_key]
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
def pytest_sessionfinish(session, exitstatus: int | ExitCode):
"""After each session, remove base directory if all the tests passed,
the policy is "failed", and the basetemp is not specified by a user.
"""
@ -317,6 +317,6 @@ def pytest_runtest_makereport(
) -> Generator[None, TestReport, TestReport]:
rep = yield
assert rep.when is not None
empty: Dict[str, bool] = {}
empty: dict[str, bool] = {}
item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed
return rep

View File

@ -1,6 +1,8 @@
# mypy: allow-untyped-defs
"""Discover and run std-library "unittest" style tests."""
from __future__ import annotations
import inspect
import sys
import traceback
@ -9,8 +11,6 @@ from typing import Any
from typing import Callable
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
@ -49,8 +49,8 @@ _SysExcInfoType = Union[
def pytest_pycollect_makeitem(
collector: Union[Module, Class], name: str, obj: object
) -> Optional["UnitTestCase"]:
collector: Module | Class, name: str, obj: object
) -> UnitTestCase | None:
try:
# Has unittest been imported?
ut = sys.modules["unittest"]
@ -81,7 +81,7 @@ class UnitTestCase(Class):
# it.
return self.obj("runTest")
def collect(self) -> Iterable[Union[Item, Collector]]:
def collect(self) -> Iterable[Item | Collector]:
from unittest import TestLoader
cls = self.obj
@ -201,7 +201,7 @@ class UnitTestCase(Class):
class TestCaseFunction(Function):
nofuncargs = True
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
_excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None
def _getinstance(self):
assert isinstance(self.parent, UnitTestCase)
@ -215,7 +215,7 @@ class TestCaseFunction(Function):
def setup(self) -> None:
# A bound method to be called during teardown() if set (see 'runtest()').
self._explicit_tearDown: Optional[Callable[[], None]] = None
self._explicit_tearDown: Callable[[], None] | None = None
super().setup()
def teardown(self) -> None:
@ -226,7 +226,7 @@ class TestCaseFunction(Function):
del self._instance
super().teardown()
def startTest(self, testcase: "unittest.TestCase") -> None:
def startTest(self, testcase: unittest.TestCase) -> None:
pass
def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
@ -265,7 +265,7 @@ class TestCaseFunction(Function):
self.__dict__.setdefault("_excinfo", []).append(excinfo)
def addError(
self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType
) -> None:
try:
if isinstance(rawexcinfo[1], exit.Exception):
@ -275,11 +275,11 @@ class TestCaseFunction(Function):
self._addexcinfo(rawexcinfo)
def addFailure(
self, testcase: "unittest.TestCase", rawexcinfo: _SysExcInfoType
self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType
) -> None:
self._addexcinfo(rawexcinfo)
def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None:
def addSkip(self, testcase: unittest.TestCase, reason: str) -> None:
try:
raise pytest.skip.Exception(reason, _use_item_location=True)
except skip.Exception:
@ -287,7 +287,7 @@ class TestCaseFunction(Function):
def addExpectedFailure(
self,
testcase: "unittest.TestCase",
testcase: unittest.TestCase,
rawexcinfo: _SysExcInfoType,
reason: str = "",
) -> None:
@ -298,8 +298,8 @@ class TestCaseFunction(Function):
def addUnexpectedSuccess(
self,
testcase: "unittest.TestCase",
reason: Optional["twisted.trial.unittest.Todo"] = None,
testcase: unittest.TestCase,
reason: twisted.trial.unittest.Todo | None = None,
) -> None:
msg = "Unexpected success"
if reason:
@ -310,13 +310,13 @@ class TestCaseFunction(Function):
except fail.Exception:
self._addexcinfo(sys.exc_info())
def addSuccess(self, testcase: "unittest.TestCase") -> None:
def addSuccess(self, testcase: unittest.TestCase) -> None:
pass
def stopTest(self, testcase: "unittest.TestCase") -> None:
def stopTest(self, testcase: unittest.TestCase) -> None:
pass
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
def addDuration(self, testcase: unittest.TestCase, elapsed: float) -> None:
pass
def runtest(self) -> None:

View File

@ -1,16 +1,21 @@
from __future__ import annotations
import sys
import traceback
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Generator
from typing import Optional
from typing import Type
from typing import TYPE_CHECKING
import warnings
import pytest
if TYPE_CHECKING:
from typing_extensions import Self
# Copied from cpython/Lib/test/support/__init__.py, with modifications.
class catch_unraisable_exception:
"""Context manager catching unraisable exception using sys.unraisablehook.
@ -34,24 +39,24 @@ class catch_unraisable_exception:
"""
def __init__(self) -> None:
self.unraisable: Optional[sys.UnraisableHookArgs] = None
self._old_hook: Optional[Callable[[sys.UnraisableHookArgs], Any]] = None
self.unraisable: sys.UnraisableHookArgs | None = None
self._old_hook: Callable[[sys.UnraisableHookArgs], Any] | None = None
def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
def _hook(self, unraisable: sys.UnraisableHookArgs) -> None:
# Storing unraisable.object can resurrect an object which is being
# finalized. Storing unraisable.exc_value creates a reference cycle.
self.unraisable = unraisable
def __enter__(self) -> "catch_unraisable_exception":
def __enter__(self) -> Self:
self._old_hook = sys.unraisablehook
sys.unraisablehook = self._hook
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
assert self._old_hook is not None
sys.unraisablehook = self._old_hook

View File

@ -1,10 +1,11 @@
from __future__ import annotations
import dataclasses
import inspect
from types import FunctionType
from typing import Any
from typing import final
from typing import Generic
from typing import Type
from typing import TypeVar
import warnings
@ -72,7 +73,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
__module__ = "pytest"
@classmethod
def simple(cls, apiname: str) -> "PytestExperimentalApiWarning":
def simple(cls, apiname: str) -> PytestExperimentalApiWarning:
return cls(f"{apiname} is an experimental api that may change over time")
@ -132,7 +133,7 @@ class UnformattedWarning(Generic[_W]):
as opposed to a direct message.
"""
category: Type["_W"]
category: type[_W]
template: str
def format(self, **kwargs: Any) -> _W:

View File

@ -1,9 +1,10 @@
# mypy: allow-untyped-defs
from __future__ import annotations
from contextlib import contextmanager
import sys
from typing import Generator
from typing import Literal
from typing import Optional
import warnings
from _pytest.config import apply_warning_filters
@ -28,7 +29,7 @@ def catch_warnings_for_item(
config: Config,
ihook,
when: Literal["config", "collect", "runtest"],
item: Optional[Item],
item: Item | None,
) -> Generator[None, None, None]:
"""Context manager that catches warnings generated in the contained execution block.
@ -142,7 +143,7 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]:
@pytest.hookimpl(wrapper=True)
def pytest_load_initial_conftests(
early_config: "Config",
early_config: Config,
) -> Generator[None, None, None]:
with catch_warnings_for_item(
config=early_config, ihook=early_config.hook, when="config", item=None

View File

@ -1,6 +1,8 @@
# shim for pylib going away
# if pylib is installed this file will get skipped
# (`py/__init__.py` has higher precedence)
from __future__ import annotations
import sys
import _pytest._py.error as error

View File

@ -1,6 +1,8 @@
# PYTHON_ARGCOMPLETE_OK
"""pytest: unit and functional testing with Python."""
from __future__ import annotations
from _pytest import __version__
from _pytest import version_tuple
from _pytest._code import ExceptionInfo

Some files were not shown because too many files have changed in this diff Show More