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"))