Skip to content

Commit 8371c77

Browse files
authored
Merge pull request #15 from damiondoesthings/master
Add support for linting Jupyter notebooks through nbQA
2 parents 6b55d66 + 2cef73e commit 8371c77

File tree

7 files changed

+72
-10
lines changed

7 files changed

+72
-10
lines changed

docs/sync.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@ mypy | mypy-baseline sync
99
The baseline file (`mypy-baseline.txt` by default) contains all errors that mypy spits out with line numbers replaced with zeros and colors removed. All errors recorded in the baseline will be filtered out from the future mypy runs by `mypy-baseline filter`.
1010

1111
The lines in the baseline come in the same order as mypy produeces them. This order is fragile, and the order of files analyzed (and so the lines in the baseline) might change as the dependency graph between modules changes and even be different on different machines. When syncing the changes with an existing baseline, we try to preserve the lines order there, but sometimes it cannot be done, and the lines may jump around. If that causes merge conflicts, simply re-run `sync` instead of trying to fix conflicts manually.
12+
13+
## Jupyter notebooks
14+
15+
All mypy-baseline commands support [nbQA](https://github.com/nbQA-dev/nbQA) for checking [Jupyter notebooks](https://jupyter.org/):
16+
17+
```bash
18+
python3 -m pip install nbqa
19+
nbqa mypy . | mypy-baseline sync
20+
```

mypy_baseline/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
from ._cli import entrypoint, main
55

66

7-
__version__ = '0.4.5'
7+
__version__ = '0.5.0'
88
__all__ = ['entrypoint', 'main']

mypy_baseline/_error.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,41 @@
1010
if TYPE_CHECKING:
1111
from ._config import Config
1212

13-
COLOR_PATTERN = '(\x1b\\[\\d*m?|\x0f)*'
14-
REX_COLOR = re.compile(COLOR_PATTERN)
15-
REX_LINE = re.compile(rf"""
13+
REX_COLOR = re.compile('(\x1b\\[\\d*m?|\x0f)*')
14+
REX_COLOR_NBQA = re.compile(r'\[\d*m|\x1b|\(B')
15+
REX_LINE = re.compile(r"""
1616
(?P<path>.+\.py):
1717
(?P<lineno>[0-9]+):\s
18-
{COLOR_PATTERN}(?P<severity>[a-z]+):{COLOR_PATTERN}\s
18+
(?P<severity>[a-z]+):\s
1919
(?P<message>.+?)
20-
(?:\s\s{COLOR_PATTERN}\[(?P<category>[a-z-]+)\]{COLOR_PATTERN})?
20+
(?:\s\s\[(?P<category>[a-z-]+)\])?
21+
\s*
22+
""", re.VERBOSE | re.MULTILINE)
23+
REX_LINE_NBQA = re.compile(r"""
24+
(?P<path>.+\.ipynb:cell_[0-9]+):
25+
(?P<lineno>[0-9]+):\s
26+
(?P<severity>[a-z]+):\s
27+
(?P<message>.+?)
28+
(?:\s\s\[(?P<category>[a-z-]+)\])?
2129
\s*
2230
""", re.VERBOSE | re.MULTILINE)
2331
REX_LINE_IN_MSG = re.compile(r'defined on line \d+')
2432

2533

34+
def _remove_color_codes(line: str) -> str:
35+
line = REX_COLOR.sub('', line)
36+
return REX_COLOR_NBQA.sub('', line)
37+
38+
2639
@dataclass
2740
class Error:
2841
raw_line: str
2942
_match: re.Match[str]
3043

3144
@classmethod
3245
def new(self, line: str) -> Error | None:
33-
match = REX_LINE.fullmatch(line)
46+
line = _remove_color_codes(line)
47+
match = REX_LINE.fullmatch(line) or REX_LINE_NBQA.fullmatch(line)
3448
if match is None:
3549
return None
3650
return Error(line, match)
@@ -61,6 +75,7 @@ def get_clean_line(self, config: Config) -> str:
6175
path = Path(*self.path.parts[:config.depth])
6276
pos = self.line_number if config.preserve_position else 0
6377
msg = REX_COLOR.sub('', self.message).strip()
78+
msg = REX_COLOR_NBQA.sub('', self.message).strip()
6479
msg = REX_LINE_IN_MSG.sub('defined on line 0', msg)
6580
line = f'{path}:{pos}: {self.severity}: {msg}'
6681
if self.category != 'note':

tests/test_commands/helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
LINE1 = 'views.py:69: error: Hello world [assignment]\r\n'
99
LINE2 = 'settings.py:42: error: How are you? [union-attr]\r\n'
1010

11+
NOTEBOOK_LINE1 = 'fail.ipynb:cell_1:2: \x1b[1m\x1b[31merror:\x1b(B\x1b[m Incompatible return value type (got \x1b(B\x1b[m\x1b[1m"int"\x1b(B\x1b[m, expected \x1b(B\x1b[m\x1b[1m"str"\x1b(B\x1b[m) \x1b(B\x1b[m\x1b[33m[return-value]\x1b(B\x1b[m\n' # noqa: E501
12+
1113

1214
def run(cmd: list[str], exit_code: int = 0) -> str:
1315
stdout = StringIO()

tests/test_commands/test_filter.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mypy_baseline import main
66

7-
from .helpers import LINE1, LINE2, run
7+
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1, run
88

99

1010
def test_filter():
@@ -25,6 +25,22 @@ def test_filter():
2525
assert 'Your changes introduced' in actual
2626

2727

28+
def test_filter_notebook():
29+
stdin = StringIO()
30+
stdin.write(NOTEBOOK_LINE1)
31+
32+
stdin.seek(0)
33+
stdout = StringIO()
34+
code = main(['filter'], stdin, stdout)
35+
assert code == 1
36+
stdout.seek(0)
37+
actual = stdout.read()
38+
assert NOTEBOOK_LINE1 in actual
39+
assert ' return-value ' in actual
40+
assert ' unresolved' in actual
41+
assert 'Your changes introduced' in actual
42+
43+
2844
def test_filter__empty_stdin():
2945
actual = run(['filter'])
3046
assert actual == ''

tests/test_commands/test_sync.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from mypy_baseline import main
77

8-
from .helpers import LINE1, LINE2
8+
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1
99

1010

1111
def test_sync(tmp_path: Path):
@@ -20,3 +20,15 @@ def test_sync(tmp_path: Path):
2020
line1, line2 = actual.splitlines()
2121
assert line1 == 'views.py:0: error: Hello world [assignment]'
2222
assert line2 == 'settings.py:0: error: How are you? [union-attr]'
23+
24+
25+
def test_sync_notebook(tmp_path: Path):
26+
blpath = tmp_path / 'bline.txt'
27+
stdin = StringIO()
28+
stdin.write(NOTEBOOK_LINE1)
29+
stdin.seek(0)
30+
code = main(['sync', '--baseline-path', str(blpath)], stdin, StringIO())
31+
assert code == 0
32+
actual = blpath.read_text()
33+
line1 = actual.splitlines()[0]
34+
assert line1 == 'fail.ipynb:cell_1:0: error: Incompatible return value type (got "int", expected "str") [return-value]' # noqa: E501

tests/test_commands/test_top_files.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pathlib import Path
44

5-
from .helpers import LINE1, LINE2, run
5+
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1, run
66

77

88
def test_top_files(tmp_path: Path):
@@ -12,3 +12,11 @@ def test_top_files(tmp_path: Path):
1212
actual = run(cmd)
1313
assert 'views.py' in actual
1414
assert 'settings.py' in actual
15+
16+
17+
def test_top_notebook(tmp_path: Path):
18+
blpath = tmp_path / 'bline.txt'
19+
blpath.write_text(f'{NOTEBOOK_LINE1}')
20+
cmd = ['top-files', '--baseline-path', str(blpath), '--no-color']
21+
actual = run(cmd)
22+
assert 'fail.ipynb' in actual

0 commit comments

Comments
 (0)