From 310b67b2271cb05f575054c1cdd2ece2412c89a2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 20 Jan 2023 11:13:36 +0200 Subject: [PATCH] Drop attrs dependency, use dataclasses instead (#10669) Since pytest now requires Python>=3.7, we can use the stdlib attrs clone, dataclasses, instead of the OG package. attrs is still somewhat nicer than dataclasses and has some extra functionality, but for pytest usage there's not really a justification IMO to impose the extra dependency on users when a standard alternative exists. --- changelog/10669.trivial.rst | 1 + setup.cfg | 2 +- src/_pytest/_code/code.py | 64 ++++++++++++++++++++-------------- src/_pytest/cacheprovider.py | 11 +++--- src/_pytest/compat.py | 7 ++-- src/_pytest/config/__init__.py | 21 +++++++---- src/_pytest/fixtures.py | 28 ++++++++------- src/_pytest/legacypath.py | 4 +-- src/_pytest/main.py | 7 ++-- src/_pytest/mark/__init__.py | 11 +++--- src/_pytest/mark/expression.py | 6 ++-- src/_pytest/mark/structures.py | 15 ++++---- src/_pytest/python.py | 33 +++++++++++------- src/_pytest/reports.py | 16 +++++---- src/_pytest/runner.py | 5 ++- src/_pytest/skipping.py | 9 ++--- src/_pytest/terminal.py | 4 +-- src/_pytest/tmpdir.py | 16 +++++---- src/_pytest/warning_types.py | 5 ++- testing/acceptance_test.py | 25 +++++++------ testing/conftest.py | 13 ++++--- testing/python/metafunc.py | 14 ++++---- testing/test_config.py | 19 +++++----- testing/test_pathlib.py | 8 ++--- testing/test_tmpdir.py | 14 ++++---- 25 files changed, 199 insertions(+), 159 deletions(-) create mode 100644 changelog/10669.trivial.rst diff --git a/changelog/10669.trivial.rst b/changelog/10669.trivial.rst new file mode 100644 index 000000000..6cebe12ec --- /dev/null +++ b/changelog/10669.trivial.rst @@ -0,0 +1 @@ +pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported). diff --git a/setup.cfg b/setup.cfg index 261ffec8f..56dadae7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,6 @@ packages = pytest py_modules = py install_requires = - attrs>=19.2.0 iniconfig packaging pluggy>=0.12,<2.0 @@ -68,6 +67,7 @@ console_scripts = [options.extras_require] testing = argcomplete + attrs>=19.2.0 hypothesis>=3.56 mock nose diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 97985def1..44ce8fac9 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1,4 +1,5 @@ import ast +import dataclasses import inspect import os import re @@ -32,7 +33,6 @@ from typing import TypeVar from typing import Union from weakref import ref -import attr import pluggy import _pytest @@ -445,7 +445,7 @@ E = TypeVar("E", bound=BaseException, covariant=True) @final -@attr.s(repr=False, init=False, auto_attribs=True) +@dataclasses.dataclass class ExceptionInfo(Generic[E]): """Wraps sys.exc_info() objects and offers help for navigating the traceback.""" @@ -649,12 +649,12 @@ class ExceptionInfo(Generic[E]): """ if style == "native": return ReprExceptionInfo( - ReprTracebackNative( + reprtraceback=ReprTracebackNative( traceback.format_exception( self.type, self.value, self.traceback[0]._rawentry ) ), - self._getreprcrash(), + reprcrash=self._getreprcrash(), ) fmt = FormattedExcinfo( @@ -684,7 +684,7 @@ class ExceptionInfo(Generic[E]): return True -@attr.s(auto_attribs=True) +@dataclasses.dataclass class FormattedExcinfo: """Presenting information about failing Functions and Generators.""" @@ -699,8 +699,8 @@ class FormattedExcinfo: funcargs: bool = False truncate_locals: bool = True chain: bool = True - astcache: Dict[Union[str, Path], ast.AST] = attr.ib( - factory=dict, init=False, repr=False + astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field( + default_factory=dict, init=False, repr=False ) def _getindent(self, source: "Source") -> int: @@ -978,7 +978,7 @@ class FormattedExcinfo: return ExceptionChainRepr(repr_chain) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class TerminalRepr: def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception @@ -996,14 +996,14 @@ class TerminalRepr: # This class is abstract -- only subclasses are instantiated. -@attr.s(eq=False) +@dataclasses.dataclass(eq=False) class ExceptionRepr(TerminalRepr): # Provided by subclasses. - reprcrash: Optional["ReprFileLocation"] reprtraceback: "ReprTraceback" - - def __attrs_post_init__(self) -> None: - self.sections: List[Tuple[str, str, str]] = [] + reprcrash: Optional["ReprFileLocation"] + sections: List[Tuple[str, str, str]] = dataclasses.field( + init=False, default_factory=list + ) def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) @@ -1014,16 +1014,23 @@ class ExceptionRepr(TerminalRepr): tw.line(content) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ExceptionChainRepr(ExceptionRepr): chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]] - def __attrs_post_init__(self) -> None: - super().__attrs_post_init__() + def __init__( + self, + chain: Sequence[ + Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] + ], + ) -> None: # reprcrash and reprtraceback of the outermost (the newest) exception # in the chain. - self.reprtraceback = self.chain[-1][0] - self.reprcrash = self.chain[-1][1] + super().__init__( + reprtraceback=chain[-1][0], + reprcrash=chain[-1][1], + ) + self.chain = chain def toterminal(self, tw: TerminalWriter) -> None: for element in self.chain: @@ -1034,7 +1041,7 @@ class ExceptionChainRepr(ExceptionRepr): super().toterminal(tw) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback: "ReprTraceback" reprcrash: "ReprFileLocation" @@ -1044,7 +1051,7 @@ class ReprExceptionInfo(ExceptionRepr): super().toterminal(tw) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprTraceback(TerminalRepr): reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] extraline: Optional[str] @@ -1073,12 +1080,12 @@ class ReprTraceback(TerminalRepr): class ReprTracebackNative(ReprTraceback): def __init__(self, tblines: Sequence[str]) -> None: - self.style = "native" self.reprentries = [ReprEntryNative(tblines)] self.extraline = None + self.style = "native" -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprEntryNative(TerminalRepr): lines: Sequence[str] @@ -1088,7 +1095,7 @@ class ReprEntryNative(TerminalRepr): tw.write("".join(self.lines)) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprEntry(TerminalRepr): lines: Sequence[str] reprfuncargs: Optional["ReprFuncArgs"] @@ -1168,12 +1175,15 @@ class ReprEntry(TerminalRepr): ) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprFileLocation(TerminalRepr): - path: str = attr.ib(converter=str) + path: str lineno: int message: str + def __post_init__(self) -> None: + self.path = str(self.path) + def toterminal(self, tw: TerminalWriter) -> None: # Filename and lineno output for each entry, using an output format # that most editors understand. @@ -1185,7 +1195,7 @@ class ReprFileLocation(TerminalRepr): tw.line(f":{self.lineno}: {msg}") -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprLocals(TerminalRepr): lines: Sequence[str] @@ -1194,7 +1204,7 @@ class ReprLocals(TerminalRepr): tw.line(indent + line) -@attr.s(eq=False, auto_attribs=True) +@dataclasses.dataclass(eq=False) class ReprFuncArgs(TerminalRepr): args: Sequence[Tuple[str, object]] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index c236dd417..719b32f7e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -1,6 +1,7 @@ """Implementation of the cache provider.""" # This plugin was not named "cache" to avoid conflicts with the external # pytest-cache version. +import dataclasses import json import os from pathlib import Path @@ -12,8 +13,6 @@ from typing import Optional from typing import Set from typing import Union -import attr - from .pathlib import resolve_from_str from .pathlib import rm_rf from .reports import CollectReport @@ -52,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55 @final -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class Cache: - _cachedir: Path = attr.ib(repr=False) - _config: Config = attr.ib(repr=False) + """Instance of the `cache` fixture.""" + + _cachedir: Path = dataclasses.field(repr=False) + _config: Config = dataclasses.field(repr=False) # Sub-directory under cache-dir for directories created by `mkdir()`. _CACHE_PREFIX_DIRS = "d" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 211407b23..6cede2133 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,4 +1,5 @@ """Python version compatibility code.""" +import dataclasses import enum import functools import inspect @@ -17,8 +18,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - import py # fmt: off @@ -253,7 +252,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str: return _translate_non_printable(ret) -@attr.s +@dataclasses.dataclass class _PytestWrapper: """Dummy wrapper around a function object for internal use only. @@ -262,7 +261,7 @@ class _PytestWrapper: decorator to issue warnings when the fixture function is called directly. """ - obj = attr.ib() + obj: Any def get_real_func(obj): diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 25f156f8b..5c0c62108 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -2,6 +2,7 @@ import argparse import collections.abc import copy +import dataclasses import enum import glob import inspect @@ -34,7 +35,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union -import attr from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager @@ -886,10 +886,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: yield from _iter_rewritable_modules(new_package_files) -def _args_converter(args: Iterable[str]) -> Tuple[str, ...]: - return tuple(args) - - @final class Config: """Access to configuration values, pluginmanager and plugin hooks. @@ -903,7 +899,7 @@ class Config: """ @final - @attr.s(frozen=True, auto_attribs=True) + @dataclasses.dataclass(frozen=True) class InvocationParams: """Holds parameters passed during :func:`pytest.main`. @@ -919,13 +915,24 @@ class Config: Plugins accessing ``InvocationParams`` must be aware of that. """ - args: Tuple[str, ...] = attr.ib(converter=_args_converter) + args: Tuple[str, ...] """The command-line arguments as passed to :func:`pytest.main`.""" plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] """Extra plugins, might be `None`.""" dir: Path """The directory from which :func:`pytest.main` was invoked.""" + def __init__( + self, + *, + args: Iterable[str], + plugins: Optional[Sequence[Union[str, _PluggyPlugin]]], + dir: Path, + ) -> None: + object.__setattr__(self, "args", tuple(args)) + object.__setattr__(self, "plugins", plugins) + object.__setattr__(self, "dir", dir) + class ArgsSource(enum.Enum): """Indicates the source of the test arguments. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7ef261b96..007245b24 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,3 +1,4 @@ +import dataclasses import functools import inspect import os @@ -28,8 +29,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - import _pytest from _pytest import nodes from _pytest._code import getfslineno @@ -103,7 +102,7 @@ _FixtureCachedResult = Union[ ] -@attr.s(frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class PseudoFixtureDef(Generic[FixtureValue]): cached_result: "_FixtureCachedResult[FixtureValue]" _scope: Scope @@ -350,8 +349,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any: return request.param -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class FuncFixtureInfo: + __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") + # Original function argument names. argnames: Tuple[str, ...] # Argnames that function immediately requires. These include argnames + @@ -1181,19 +1182,21 @@ def wrap_function_to_error_out_if_called_directly( @final -@attr.s(frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class FixtureFunctionMarker: scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" - params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter) + params: Optional[Tuple[object, ...]] autouse: bool = False ids: Optional[ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] - ] = attr.ib( - default=None, - converter=_ensure_immutable_ids, - ) + ] = None name: Optional[str] = None + _ispytest: dataclasses.InitVar[bool] = False + + def __post_init__(self, _ispytest: bool) -> None: + check_ispytest(_ispytest) + def __call__(self, function: FixtureFunction) -> FixtureFunction: if inspect.isclass(function): raise ValueError("class fixtures not supported (maybe in the future)") @@ -1313,10 +1316,11 @@ def fixture( # noqa: F811 """ fixture_marker = FixtureFunctionMarker( scope=scope, - params=params, + params=tuple(params) if params is not None else None, autouse=autouse, - ids=ids, + ids=None if ids is None else ids if callable(ids) else tuple(ids), name=name, + _ispytest=True, ) # Direct decoration. diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index f71e7e96e..af1d0c07e 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,4 +1,5 @@ """Add backward compatibility support for the legacy py path type.""" +import dataclasses import shlex import subprocess from pathlib import Path @@ -7,7 +8,6 @@ from typing import Optional from typing import TYPE_CHECKING from typing import Union -import attr from iniconfig import SectionWrapper from _pytest.cacheprovider import Cache @@ -268,7 +268,7 @@ class LegacyTestdirPlugin: @final -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class TempdirFactory: """Backward compatibility wrapper that implements :class:`py.path.local` for :class:`TempPathFactory`. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 61fb7eaa4..5f8ac4689 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -1,5 +1,6 @@ """Core implementation of the testing process: init, session, runtest loop.""" import argparse +import dataclasses import fnmatch import functools import importlib @@ -19,8 +20,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union -import attr - import _pytest._code from _pytest import nodes from _pytest.compat import final @@ -442,8 +441,10 @@ class Failed(Exception): """Signals a stop as failed test run.""" -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class _bestrelpath_cache(Dict[Path, str]): + __slots__ = ("path",) + path: Path def __missing__(self, path: Path) -> str: diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 6717d1135..de46b4c8a 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,4 +1,5 @@ """Generic mechanism for marking and selecting python functions.""" +import dataclasses from typing import AbstractSet from typing import Collection from typing import List @@ -6,8 +7,6 @@ from typing import Optional from typing import TYPE_CHECKING from typing import Union -import attr - from .expression import Expression from .expression import ParseError from .structures import EMPTY_PARAMETERSET_OPTION @@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: return None -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class KeywordMatcher: """A matcher for keywords. @@ -145,6 +144,8 @@ class KeywordMatcher: any item, as well as names directly assigned to test functions. """ + __slots__ = ("_names",) + _names: AbstractSet[str] @classmethod @@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None: items[:] = remaining -@attr.s(slots=True, auto_attribs=True) +@dataclasses.dataclass class MarkMatcher: """A matcher for markers which are present. Tries to match on any marker names, attached to the given colitem. """ + __slots__ = ("own_mark_names",) + own_mark_names: AbstractSet[str] @classmethod diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 0a2e7c656..f82a81d44 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -15,6 +15,7 @@ The semantics are: - or/and/not evaluate according to the usual boolean semantics. """ import ast +import dataclasses import enum import re import types @@ -25,8 +26,6 @@ from typing import NoReturn from typing import Optional from typing import Sequence -import attr - __all__ = [ "Expression", @@ -44,8 +43,9 @@ class TokenType(enum.Enum): EOF = "end of input" -@attr.s(frozen=True, slots=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Token: + __slots__ = ("type", "value", "pos") type: TokenType value: str pos: int diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 5186c9ea3..8dbff1dc9 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,4 +1,5 @@ import collections.abc +import dataclasses import inspect import warnings from typing import Any @@ -20,8 +21,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - from .._code import getfslineno from ..compat import ascii_escaped from ..compat import final @@ -191,8 +190,10 @@ class ParameterSet(NamedTuple): @final -@attr.s(frozen=True, init=False, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Mark: + """A pytest mark.""" + #: Name of the mark. name: str #: Positional arguments of the mark decorator. @@ -201,9 +202,11 @@ class Mark: kwargs: Mapping[str, Any] #: Source Mark for ids with parametrize Marks. - _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False) + _param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False) #: Resolved/generated ids with parametrize Marks. - _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False) + _param_ids_generated: Optional[Sequence[str]] = dataclasses.field( + default=None, repr=False + ) def __init__( self, @@ -261,7 +264,7 @@ class Mark: Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) -@attr.s(init=False, auto_attribs=True) +@dataclasses.dataclass class MarkDecorator: """A decorator for applying a mark on test functions and classes. diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e143d28d1..b24a3803e 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,4 +1,5 @@ """Python test discovery, setup and run of test functions.""" +import dataclasses import enum import fnmatch import inspect @@ -27,8 +28,6 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import attr - import _pytest from _pytest import fixtures from _pytest import nodes @@ -956,10 +955,20 @@ def hasnew(obj: object) -> bool: @final -@attr.s(frozen=True, auto_attribs=True, slots=True) +@dataclasses.dataclass(frozen=True) class IdMaker: """Make IDs for a parametrization.""" + __slots__ = ( + "argnames", + "parametersets", + "idfn", + "ids", + "config", + "nodeid", + "func_name", + ) + # The argnames of the parametrization. argnames: Sequence[str] # The ParameterSets of the parametrization. @@ -1109,7 +1118,7 @@ class IdMaker: @final -@attr.s(frozen=True, slots=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class CallSpec2: """A planned parameterized invocation of a test function. @@ -1120,18 +1129,18 @@ class CallSpec2: # arg name -> arg value which will be passed to the parametrized test # function (direct parameterization). - funcargs: Dict[str, object] = attr.Factory(dict) + funcargs: Dict[str, object] = dataclasses.field(default_factory=dict) # arg name -> arg value which will be passed to a fixture of the same name # (indirect parametrization). - params: Dict[str, object] = attr.Factory(dict) + params: Dict[str, object] = dataclasses.field(default_factory=dict) # arg name -> arg index. - indices: Dict[str, int] = attr.Factory(dict) + indices: Dict[str, int] = dataclasses.field(default_factory=dict) # Used for sorting parametrized resources. - _arg2scope: Dict[str, Scope] = attr.Factory(dict) + _arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict) # Parts which will be added to the item's name in `[..]` separated by "-". - _idlist: List[str] = attr.Factory(list) + _idlist: List[str] = dataclasses.field(default_factory=list) # Marks which will be applied to the item. - marks: List[Mark] = attr.Factory(list) + marks: List[Mark] = dataclasses.field(default_factory=list) def setmulti( self, @@ -1163,9 +1172,9 @@ class CallSpec2: return CallSpec2( funcargs=funcargs, params=params, - arg2scope=arg2scope, indices=indices, - idlist=[*self._idlist, id], + _arg2scope=arg2scope, + _idlist=[*self._idlist, id], marks=[*self.marks, *normalize_mark_list(marks)], ) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c35f7087e..1b2821c71 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,3 +1,4 @@ +import dataclasses import os from io import StringIO from pprint import pprint @@ -16,8 +17,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr @@ -459,15 +458,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: def serialize_repr_entry( entry: Union[ReprEntry, ReprEntryNative] ) -> Dict[str, Any]: - data = attr.asdict(entry) + data = dataclasses.asdict(entry) for key, value in data.items(): if hasattr(value, "__dict__"): - data[key] = attr.asdict(value) + data[key] = dataclasses.asdict(value) entry_data = {"type": type(entry).__name__, "data": data} return entry_data def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: - result = attr.asdict(reprtraceback) + result = dataclasses.asdict(reprtraceback) result["reprentries"] = [ serialize_repr_entry(x) for x in reprtraceback.reprentries ] @@ -477,7 +476,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: reprcrash: Optional[ReprFileLocation], ) -> Optional[Dict[str, Any]]: if reprcrash is not None: - return attr.asdict(reprcrash) + return dataclasses.asdict(reprcrash) else: return None @@ -594,7 +593,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: ExceptionChainRepr, ReprExceptionInfo ] = ExceptionChainRepr(chain) else: - exception_info = ReprExceptionInfo(reprtraceback, reprcrash) + exception_info = ReprExceptionInfo( + reprtraceback=reprtraceback, + reprcrash=reprcrash, + ) for section in reportdict["longrepr"]["sections"]: exception_info.addsection(*section) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index cc17cf2f4..f861c05a4 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -1,5 +1,6 @@ """Basic collect and runtest protocol implementations.""" import bdb +import dataclasses import os import sys from typing import Callable @@ -14,8 +15,6 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union -import attr - from .reports import BaseReport from .reports import CollectErrorRepr from .reports import CollectReport @@ -268,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True) @final -@attr.s(repr=False, init=False, auto_attribs=True) +@dataclasses.dataclass class CallInfo(Generic[TResult]): """Result/Exception info of a function invocation.""" diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index b20442350..26ce73758 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,4 +1,5 @@ """Support for skip/xfail functions and markers.""" +import dataclasses import os import platform import sys @@ -9,8 +10,6 @@ from typing import Optional from typing import Tuple from typing import Type -import attr - from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, return result, reason -@attr.s(slots=True, frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Skip: """The result of evaluate_skip_marks().""" @@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]: return None -@attr.s(slots=True, frozen=True, auto_attribs=True) +@dataclasses.dataclass(frozen=True) class Xfail: """The result of evaluate_xfail_marks().""" + __slots__ = ("reason", "run", "strict", "raises") + reason: str run: bool strict: bool diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d967a3ee6..1b73da89b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -3,6 +3,7 @@ This is a good source for looking at the various reporting hooks. """ import argparse +import dataclasses import datetime import inspect import platform @@ -27,7 +28,6 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union -import attr import pluggy import _pytest._version @@ -287,7 +287,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: return outcome, letter, outcome.upper() -@attr.s(auto_attribs=True) +@dataclasses.dataclass class WarningReport: """Simple structure to hold warnings information captured by ``pytest_warning_recorded``. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 8aca6d9f5..ec44623dc 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,10 +1,12 @@ """Support for providing temporary directories to test functions.""" +import dataclasses import os import re import sys import tempfile from pathlib import Path from shutil import rmtree +from typing import Any from typing import Dict from typing import Generator from typing import Optional @@ -21,7 +23,6 @@ if TYPE_CHECKING: RetentionType = Literal["all", "failed", "none"] -import attr from _pytest.config.argparsing import Parser from .pathlib import LOCK_TIMEOUT @@ -42,18 +43,19 @@ tmppath_result_key = StashKey[Dict[str, bool]]() @final -@attr.s(init=False) +@dataclasses.dataclass class TempPathFactory: """Factory for temporary directories under the common base temp directory. The base directory can be configured using the ``--basetemp`` option. """ - _given_basetemp = attr.ib(type=Optional[Path]) - _trace = attr.ib() - _basetemp = attr.ib(type=Optional[Path]) - _retention_count = attr.ib(type=int) - _retention_policy = attr.ib(type="RetentionType") + _given_basetemp: Optional[Path] + # pluggy TagTracerSub, not currently exposed, so Any. + _trace: Any + _basetemp: Optional[Path] + _retention_count: int + _retention_policy: "RetentionType" def __init__( self, diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 620860c1b..86fa9a07e 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,3 +1,4 @@ +import dataclasses import inspect import warnings from types import FunctionType @@ -6,8 +7,6 @@ from typing import Generic from typing import Type from typing import TypeVar -import attr - from _pytest.compat import final @@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning) @final -@attr.s(auto_attribs=True) +@dataclasses.dataclass class UnformattedWarning(Generic[_W]): """A warning meant to be formatted during runtime. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c7139b538..46352e130 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,9 +1,8 @@ +import dataclasses import os import sys import types -import attr - import pytest from _pytest.compat import importlib_metadata from _pytest.config import ExitCode @@ -115,11 +114,11 @@ class TestGeneralUsage: loaded = [] - @attr.s + @dataclasses.dataclass class DummyEntryPoint: - name = attr.ib() - module = attr.ib() - group = "pytest11" + name: str + module: str + group: str = "pytest11" def load(self): __import__(self.module) @@ -132,10 +131,10 @@ class TestGeneralUsage: DummyEntryPoint("mycov", "mycov_module"), ] - @attr.s + @dataclasses.dataclass class DummyDist: - entry_points = attr.ib() - files = () + entry_points: object + files: object = () def my_dists(): return (DummyDist(entry_points),) @@ -1037,14 +1036,14 @@ def test_fixture_values_leak(pytester: Pytester) -> None: """ pytester.makepyfile( """ - import attr + import dataclasses import gc import pytest import weakref - @attr.s - class SomeObj(object): - name = attr.ib() + @dataclasses.dataclass + class SomeObj: + name: str fix_of_test1_ref = None session_ref = None diff --git a/testing/conftest.py b/testing/conftest.py index 8a9816799..a83552fd2 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,3 +1,4 @@ +import dataclasses import re import sys from typing import List @@ -192,20 +193,18 @@ def mock_timing(monkeypatch: MonkeyPatch): Time is static, and only advances through `sleep` calls, thus tests might sleep over large numbers and obtain accurate time() calls at the end, making tests reliable and instant. """ - import attr - @attr.s + @dataclasses.dataclass class MockTiming: + _current_time: float = 1590150050.0 - _current_time = attr.ib(default=1590150050.0) - - def sleep(self, seconds): + def sleep(self, seconds: float) -> None: self._current_time += seconds - def time(self): + def time(self) -> float: return self._current_time - def patch(self): + def patch(self) -> None: from _pytest import timing monkeypatch.setattr(timing, "sleep", self.sleep) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2fed22718..c1cc9c3d3 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,3 +1,4 @@ +import dataclasses import itertools import re import sys @@ -12,7 +13,6 @@ from typing import Sequence from typing import Tuple from typing import Union -import attr import hypothesis from hypothesis import strategies @@ -39,14 +39,14 @@ class TestMetafunc: def __init__(self, names): self.names_closure = names - @attr.s + @dataclasses.dataclass class DefinitionMock(python.FunctionDefinition): - obj = attr.ib() - _nodeid = attr.ib() + _nodeid: str + obj: object names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) - definition: Any = DefinitionMock._create(func, "mock::nodeid") + definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid") return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) def test_no_funcargs(self) -> None: @@ -140,9 +140,9 @@ class TestMetafunc: """Unit test for _find_parametrized_scope (#3941).""" from _pytest.python import _find_parametrized_scope - @attr.s + @dataclasses.dataclass class DummyFixtureDef: - _scope = attr.ib() + _scope: Scope fixtures_defs = cast( Dict[str, Sequence[fixtures.FixtureDef[object]]], diff --git a/testing/test_config.py b/testing/test_config.py index f5b6d7f98..db50869c5 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,3 +1,4 @@ +import dataclasses import os import re import sys @@ -10,8 +11,6 @@ from typing import Tuple from typing import Type from typing import Union -import attr - import _pytest._code import pytest from _pytest.compat import importlib_metadata @@ -423,11 +422,11 @@ class TestParseIni: This test installs a mock "myplugin-1.5" which is used in the parametrized test cases. """ - @attr.s + @dataclasses.dataclass class DummyEntryPoint: - name = attr.ib() - module = attr.ib() - group = "pytest11" + name: str + module: str + group: str = "pytest11" def load(self): __import__(self.module) @@ -437,11 +436,11 @@ class TestParseIni: DummyEntryPoint("myplugin1", "myplugin1_module"), ] - @attr.s + @dataclasses.dataclass class DummyDist: - entry_points = attr.ib() - files = () - version = plugin_version + entry_points: object + files: object = () + version: str = plugin_version @property def metadata(self): diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 577c7749f..481d7a606 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -518,10 +518,10 @@ class TestImportLibMode: fn1.write_text( dedent( """ - import attr + import dataclasses import pickle - @attr.s(auto_attribs=True) + @dataclasses.dataclass class Data: x: int = 42 """ @@ -533,10 +533,10 @@ class TestImportLibMode: fn2.write_text( dedent( """ - import attr + import dataclasses import pickle - @attr.s(auto_attribs=True) + @dataclasses.dataclass class Data: x: str = "" """ diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 43437c9ab..fcb0775dd 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,3 +1,4 @@ +import dataclasses import os import stat import sys @@ -6,8 +7,7 @@ from pathlib import Path from typing import Callable from typing import cast from typing import List - -import attr +from typing import Union import pytest from _pytest import pathlib @@ -31,9 +31,9 @@ def test_tmp_path_fixture(pytester: Pytester) -> None: results.stdout.fnmatch_lines(["*1 passed*"]) -@attr.s +@dataclasses.dataclass class FakeConfig: - basetemp = attr.ib() + basetemp: Union[str, Path] @property def trace(self): @@ -56,7 +56,7 @@ class FakeConfig: class TestTmpPathHandler: - def test_mktemp(self, tmp_path): + def test_mktemp(self, tmp_path: Path) -> None: config = cast(Config, FakeConfig(tmp_path)) t = TempPathFactory.from_config(config, _ispytest=True) tmp = t.mktemp("world") @@ -67,7 +67,9 @@ class TestTmpPathHandler: assert str(tmp2.relative_to(t.getbasetemp())).startswith("this") assert tmp2 != tmp - def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): + def test_tmppath_relative_basetemp_absolute( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: """#4425""" monkeypatch.chdir(tmp_path) config = cast(Config, FakeConfig("hello"))