-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathpytest_darker.py
125 lines (100 loc) · 4 KB
/
pytest_darker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import re
import subprocess
import sys
from typing import Any, Optional, Tuple, Union, cast
import py
import pytest
import toml
from _pytest._code.code import ExceptionInfo, TerminalRepr
from _pytest.cacheprovider import Cache
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from darker.__main__ import main
HISTKEY = "darker/mtimes"
class DarkerError(Exception):
pass
class DarkerItem(pytest.Item, pytest.File):
def __init__(self, fspath: py.path.local, parent: pytest.Session):
super(DarkerItem, self).__init__(fspath, parent)
self._nodeid += "::DARKER"
self.add_marker("darker")
try:
with open("pyproject.toml") as toml_file:
self.pyproject = toml.load(toml_file)["tool"]["darker"]
except Exception:
self.pyproject = {}
@classmethod
def from_parent( # type: ignore[override]
cls, parent: pytest.Session, *, fspath: py.path.local, **kw: Any
) -> "DarkerItem":
"""The public constructor"""
return cast(DarkerItem, super().from_parent(parent=parent, fspath=fspath, **kw))
def setup(self) -> None:
pytest.importorskip("darker")
mtimes = getattr(self.config, "_darkermtimes", {})
self._darkermtime = self.fspath.mtime()
old = mtimes.get(str(self.fspath), 0)
if self._darkermtime == old:
pytest.skip("file(s) previously passed darker format checks")
if self._skip_test():
pytest.skip("file(s) excluded by pyproject.toml")
def runtest(self) -> None:
retval = main(
[
"--check",
"--diff",
"--quiet",
str(self.fspath),
]
)
if retval != 0:
raise DarkerError("Edited lines are not Black formatted")
mtimes = getattr(self.config, "_darkermtimes", {})
mtimes[str(self.fspath)] = self._darkermtime
def repr_failure( # type: ignore[override]
self, excinfo: ExceptionInfo[BaseException]
) -> Union[str, TerminalRepr]:
if excinfo.errisinstance(DarkerError):
return cast(TerminalRepr, excinfo.value)
return super(DarkerItem, self).repr_failure(excinfo)
def reportinfo(self) -> Tuple[Union[py.path.local, str], Optional[int], str]:
return self.fspath, -1, "Darker format check"
def _skip_test(self) -> bool:
return self._excluded() or (not self._included())
def _included(self) -> bool:
if "include" not in self.pyproject:
return True
return bool(re.search(self.pyproject["include"], str(self.fspath)))
def _excluded(self) -> bool:
if "exclude" not in self.pyproject:
return False
return bool(re.search(self.pyproject["exclude"], str(self.fspath)))
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group.addoption(
"--darker", action="store_true", help="enable format checking with darker"
)
def pytest_collect_file(
path: py.path.local, parent: pytest.Session
) -> Optional[DarkerItem]:
config = parent.config
if config.option.darker and path.ext == ".py":
if hasattr(DarkerItem, "from_parent"):
return DarkerItem.from_parent(parent, fspath=path)
else:
return DarkerItem(path, parent)
else:
return None
def pytest_configure(config: Config) -> None:
# load cached mtimes at session startup
if config.option.darker and hasattr(config, "cache"):
assert isinstance(config.cache, Cache)
config._darkermtimes = config.cache.get( # type: ignore[attr-defined]
HISTKEY, {}
)
config.addinivalue_line("markers", "darker: enable format checking with darker")
def pytest_unconfigure(config: Config) -> None:
# save cached mtimes at end of session
if hasattr(config, "_darkermtimes"):
assert isinstance(config.cache, Cache)
config.cache.set(HISTKEY, config._darkermtimes) # type: ignore[attr-defined]