Skip to content

Commit 5315c08

Browse files
committed
Drop attrs dependency, use dataclasses instead
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.
1 parent 03b1994 commit 5315c08

25 files changed

+208
-157
lines changed

changelog/TBD.trivial.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ packages =
4444
pytest
4545
py_modules = py
4646
install_requires =
47-
attrs>=19.2.0
4847
iniconfig
4948
packaging
5049
pluggy>=0.12,<2.0
@@ -68,6 +67,7 @@ console_scripts =
6867
[options.extras_require]
6968
testing =
7069
argcomplete
70+
attrs>=19.2.0
7171
hypothesis>=3.56
7272
mock
7373
nose

src/_pytest/_code/code.py

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ast
2+
import dataclasses
23
import inspect
34
import os
45
import re
@@ -32,7 +33,6 @@
3233
from typing import Union
3334
from weakref import ref
3435

35-
import attr
3636
import pluggy
3737

3838
import _pytest
@@ -445,7 +445,7 @@ def recursionindex(self) -> Optional[int]:
445445

446446

447447
@final
448-
@attr.s(repr=False, init=False, auto_attribs=True)
448+
@dataclasses.dataclass
449449
class ExceptionInfo(Generic[E]):
450450
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
451451

@@ -649,12 +649,12 @@ def getrepr(
649649
"""
650650
if style == "native":
651651
return ReprExceptionInfo(
652-
ReprTracebackNative(
652+
reprtraceback=ReprTracebackNative(
653653
traceback.format_exception(
654654
self.type, self.value, self.traceback[0]._rawentry
655655
)
656656
),
657-
self._getreprcrash(),
657+
reprcrash=self._getreprcrash(),
658658
)
659659

660660
fmt = FormattedExcinfo(
@@ -684,7 +684,7 @@ def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
684684
return True
685685

686686

687-
@attr.s(auto_attribs=True)
687+
@dataclasses.dataclass
688688
class FormattedExcinfo:
689689
"""Presenting information about failing Functions and Generators."""
690690

@@ -699,8 +699,8 @@ class FormattedExcinfo:
699699
funcargs: bool = False
700700
truncate_locals: bool = True
701701
chain: bool = True
702-
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
703-
factory=dict, init=False, repr=False
702+
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
703+
default_factory=dict, init=False, repr=False
704704
)
705705

706706
def _getindent(self, source: "Source") -> int:
@@ -978,7 +978,7 @@ def repr_excinfo(
978978
return ExceptionChainRepr(repr_chain)
979979

980980

981-
@attr.s(eq=False, auto_attribs=True)
981+
@dataclasses.dataclass(eq=False)
982982
class TerminalRepr:
983983
def __str__(self) -> str:
984984
# FYI this is called from pytest-xdist's serialization of exception
@@ -996,14 +996,14 @@ def toterminal(self, tw: TerminalWriter) -> None:
996996

997997

998998
# This class is abstract -- only subclasses are instantiated.
999-
@attr.s(eq=False)
999+
@dataclasses.dataclass(eq=False)
10001000
class ExceptionRepr(TerminalRepr):
10011001
# Provided by subclasses.
1002-
reprcrash: Optional["ReprFileLocation"]
10031002
reprtraceback: "ReprTraceback"
1004-
1005-
def __attrs_post_init__(self) -> None:
1006-
self.sections: List[Tuple[str, str, str]] = []
1003+
reprcrash: Optional["ReprFileLocation"]
1004+
sections: List[Tuple[str, str, str]] = dataclasses.field(
1005+
init=False, default_factory=list
1006+
)
10071007

10081008
def addsection(self, name: str, content: str, sep: str = "-") -> None:
10091009
self.sections.append((name, content, sep))
@@ -1014,16 +1014,23 @@ def toterminal(self, tw: TerminalWriter) -> None:
10141014
tw.line(content)
10151015

10161016

1017-
@attr.s(eq=False, auto_attribs=True)
1017+
@dataclasses.dataclass(eq=False)
10181018
class ExceptionChainRepr(ExceptionRepr):
10191019
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
10201020

1021-
def __attrs_post_init__(self) -> None:
1022-
super().__attrs_post_init__()
1021+
def __init__(
1022+
self,
1023+
chain: Sequence[
1024+
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
1025+
],
1026+
) -> None:
10231027
# reprcrash and reprtraceback of the outermost (the newest) exception
10241028
# in the chain.
1025-
self.reprtraceback = self.chain[-1][0]
1026-
self.reprcrash = self.chain[-1][1]
1029+
super().__init__(
1030+
reprtraceback=chain[-1][0],
1031+
reprcrash=chain[-1][1],
1032+
)
1033+
self.chain = chain
10271034

10281035
def toterminal(self, tw: TerminalWriter) -> None:
10291036
for element in self.chain:
@@ -1034,7 +1041,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10341041
super().toterminal(tw)
10351042

10361043

1037-
@attr.s(eq=False, auto_attribs=True)
1044+
@dataclasses.dataclass(eq=False)
10381045
class ReprExceptionInfo(ExceptionRepr):
10391046
reprtraceback: "ReprTraceback"
10401047
reprcrash: "ReprFileLocation"
@@ -1044,7 +1051,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10441051
super().toterminal(tw)
10451052

10461053

1047-
@attr.s(eq=False, auto_attribs=True)
1054+
@dataclasses.dataclass(eq=False)
10481055
class ReprTraceback(TerminalRepr):
10491056
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
10501057
extraline: Optional[str]
@@ -1073,12 +1080,12 @@ def toterminal(self, tw: TerminalWriter) -> None:
10731080

10741081
class ReprTracebackNative(ReprTraceback):
10751082
def __init__(self, tblines: Sequence[str]) -> None:
1076-
self.style = "native"
10771083
self.reprentries = [ReprEntryNative(tblines)]
10781084
self.extraline = None
1085+
self.style = "native"
10791086

10801087

1081-
@attr.s(eq=False, auto_attribs=True)
1088+
@dataclasses.dataclass(eq=False)
10821089
class ReprEntryNative(TerminalRepr):
10831090
lines: Sequence[str]
10841091

@@ -1088,7 +1095,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10881095
tw.write("".join(self.lines))
10891096

10901097

1091-
@attr.s(eq=False, auto_attribs=True)
1098+
@dataclasses.dataclass(eq=False)
10921099
class ReprEntry(TerminalRepr):
10931100
lines: Sequence[str]
10941101
reprfuncargs: Optional["ReprFuncArgs"]
@@ -1168,12 +1175,15 @@ def __str__(self) -> str:
11681175
)
11691176

11701177

1171-
@attr.s(eq=False, auto_attribs=True)
1178+
@dataclasses.dataclass(eq=False)
11721179
class ReprFileLocation(TerminalRepr):
1173-
path: str = attr.ib(converter=str)
1180+
path: str
11741181
lineno: int
11751182
message: str
11761183

1184+
def __post_init__(self) -> None:
1185+
self.path = str(self.path)
1186+
11771187
def toterminal(self, tw: TerminalWriter) -> None:
11781188
# Filename and lineno output for each entry, using an output format
11791189
# that most editors understand.
@@ -1185,7 +1195,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
11851195
tw.line(f":{self.lineno}: {msg}")
11861196

11871197

1188-
@attr.s(eq=False, auto_attribs=True)
1198+
@dataclasses.dataclass(eq=False)
11891199
class ReprLocals(TerminalRepr):
11901200
lines: Sequence[str]
11911201

@@ -1194,7 +1204,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None:
11941204
tw.line(indent + line)
11951205

11961206

1197-
@attr.s(eq=False, auto_attribs=True)
1207+
@dataclasses.dataclass(eq=False)
11981208
class ReprFuncArgs(TerminalRepr):
11991209
args: Sequence[Tuple[str, object]]
12001210

src/_pytest/cacheprovider.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Implementation of the cache provider."""
22
# This plugin was not named "cache" to avoid conflicts with the external
33
# pytest-cache version.
4+
import dataclasses
45
import json
56
import os
67
from pathlib import Path
@@ -12,8 +13,6 @@
1213
from typing import Set
1314
from typing import Union
1415

15-
import attr
16-
1716
from .pathlib import resolve_from_str
1817
from .pathlib import rm_rf
1918
from .reports import CollectReport
@@ -53,10 +52,10 @@
5352

5453

5554
@final
56-
@attr.s(init=False, auto_attribs=True)
55+
@dataclasses.dataclass
5756
class Cache:
58-
_cachedir: Path = attr.ib(repr=False)
59-
_config: Config = attr.ib(repr=False)
57+
_cachedir: Path = dataclasses.field(repr=False)
58+
_config: Config = dataclasses.field(repr=False)
6059

6160
# Sub-directory under cache-dir for directories created by `mkdir()`.
6261
_CACHE_PREFIX_DIRS = "d"

src/_pytest/compat.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Python version compatibility code."""
2+
import dataclasses
23
import enum
34
import functools
45
import inspect
@@ -17,8 +18,6 @@
1718
from typing import TypeVar
1819
from typing import Union
1920

20-
import attr
21-
2221
import py
2322

2423
# fmt: off
@@ -253,7 +252,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
253252
return _translate_non_printable(ret)
254253

255254

256-
@attr.s
255+
@dataclasses.dataclass
257256
class _PytestWrapper:
258257
"""Dummy wrapper around a function object for internal use only.
259258
@@ -262,7 +261,7 @@ class _PytestWrapper:
262261
decorator to issue warnings when the fixture function is called directly.
263262
"""
264263

265-
obj = attr.ib()
264+
obj: Any
266265

267266

268267
def get_real_func(obj):

src/_pytest/config/__init__.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import argparse
33
import collections.abc
44
import copy
5+
import dataclasses
56
import enum
67
import glob
78
import inspect
@@ -34,7 +35,6 @@
3435
from typing import TYPE_CHECKING
3536
from typing import Union
3637

37-
import attr
3838
from pluggy import HookimplMarker
3939
from pluggy import HookspecMarker
4040
from pluggy import PluginManager
@@ -886,10 +886,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
886886
yield from _iter_rewritable_modules(new_package_files)
887887

888888

889-
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
890-
return tuple(args)
891-
892-
893889
@final
894890
class Config:
895891
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -903,7 +899,7 @@ class Config:
903899
"""
904900

905901
@final
906-
@attr.s(frozen=True, auto_attribs=True)
902+
@dataclasses.dataclass(frozen=True)
907903
class InvocationParams:
908904
"""Holds parameters passed during :func:`pytest.main`.
909905
@@ -919,13 +915,24 @@ class InvocationParams:
919915
Plugins accessing ``InvocationParams`` must be aware of that.
920916
"""
921917

922-
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
918+
args: Tuple[str, ...]
923919
"""The command-line arguments as passed to :func:`pytest.main`."""
924920
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
925921
"""Extra plugins, might be `None`."""
926922
dir: Path
927923
"""The directory from which :func:`pytest.main` was invoked."""
928924

925+
def __init__(
926+
self,
927+
*,
928+
args: Iterable[str],
929+
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
930+
dir: Path,
931+
) -> None:
932+
object.__setattr__(self, "args", tuple(args))
933+
object.__setattr__(self, "plugins", plugins)
934+
object.__setattr__(self, "dir", dir)
935+
929936
class ArgsSource(enum.Enum):
930937
"""Indicates the source of the test arguments.
931938

0 commit comments

Comments
 (0)