Skip to content

Drop attrs dependency, use dataclasses instead #10669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 20, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/10669.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
64 changes: 37 additions & 27 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ast
import dataclasses
import inspect
import os
import re
@@ -32,7 +33,6 @@
from typing import Union
from weakref import ref

import attr
import pluggy

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


@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 @@ def getrepr(
"""
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 @@ def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
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 @@ def repr_excinfo(
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 @@ def toterminal(self, tw: TerminalWriter) -> None:


# 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 @@ def toterminal(self, tw: TerminalWriter) -> None:
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 @@ def toterminal(self, tw: TerminalWriter) -> None:
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 @@ def toterminal(self, tw: TerminalWriter) -> None:
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 @@ def toterminal(self, tw: TerminalWriter) -> None:

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 @@ def toterminal(self, tw: TerminalWriter) -> None:
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 @@ def __str__(self) -> str:
)


@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 @@ def toterminal(self, tw: TerminalWriter) -> None:
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 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None:
tw.line(indent + line)


@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]]

11 changes: 6 additions & 5 deletions src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
@@ -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 Set
from typing import Union

import attr

from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
@@ -53,10 +52,12 @@


@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"
7 changes: 3 additions & 4 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Python version compatibility code."""
import dataclasses
import enum
import functools
import inspect
@@ -17,8 +18,6 @@
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):
21 changes: 14 additions & 7 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
@@ -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_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 InvocationParams:
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.
Loading