Skip to content

Commit

Permalink
Merge pull request #15 from damiondoesthings/master
Browse files Browse the repository at this point in the history
Add support for linting Jupyter notebooks through nbQA
  • Loading branch information
orsinium authored Apr 14, 2023
2 parents 6b55d66 + 2cef73e commit 8371c77
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 10 deletions.
9 changes: 9 additions & 0 deletions docs/sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ mypy | mypy-baseline sync
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`.

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.

## Jupyter notebooks

All mypy-baseline commands support [nbQA](https://github.com/nbQA-dev/nbQA) for checking [Jupyter notebooks](https://jupyter.org/):

```bash
python3 -m pip install nbqa
nbqa mypy . | mypy-baseline sync
```
2 changes: 1 addition & 1 deletion mypy_baseline/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
from ._cli import entrypoint, main


__version__ = '0.4.5'
__version__ = '0.5.0'
__all__ = ['entrypoint', 'main']
27 changes: 21 additions & 6 deletions mypy_baseline/_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,41 @@
if TYPE_CHECKING:
from ._config import Config

COLOR_PATTERN = '(\x1b\\[\\d*m?|\x0f)*'
REX_COLOR = re.compile(COLOR_PATTERN)
REX_LINE = re.compile(rf"""
REX_COLOR = re.compile('(\x1b\\[\\d*m?|\x0f)*')
REX_COLOR_NBQA = re.compile(r'\[\d*m|\x1b|\(B')
REX_LINE = re.compile(r"""
(?P<path>.+\.py):
(?P<lineno>[0-9]+):\s
{COLOR_PATTERN}(?P<severity>[a-z]+):{COLOR_PATTERN}\s
(?P<severity>[a-z]+):\s
(?P<message>.+?)
(?:\s\s{COLOR_PATTERN}\[(?P<category>[a-z-]+)\]{COLOR_PATTERN})?
(?:\s\s\[(?P<category>[a-z-]+)\])?
\s*
""", re.VERBOSE | re.MULTILINE)
REX_LINE_NBQA = re.compile(r"""
(?P<path>.+\.ipynb:cell_[0-9]+):
(?P<lineno>[0-9]+):\s
(?P<severity>[a-z]+):\s
(?P<message>.+?)
(?:\s\s\[(?P<category>[a-z-]+)\])?
\s*
""", re.VERBOSE | re.MULTILINE)
REX_LINE_IN_MSG = re.compile(r'defined on line \d+')


def _remove_color_codes(line: str) -> str:
line = REX_COLOR.sub('', line)
return REX_COLOR_NBQA.sub('', line)


@dataclass
class Error:
raw_line: str
_match: re.Match[str]

@classmethod
def new(self, line: str) -> Error | None:
match = REX_LINE.fullmatch(line)
line = _remove_color_codes(line)
match = REX_LINE.fullmatch(line) or REX_LINE_NBQA.fullmatch(line)
if match is None:
return None
return Error(line, match)
Expand Down Expand Up @@ -61,6 +75,7 @@ def get_clean_line(self, config: Config) -> str:
path = Path(*self.path.parts[:config.depth])
pos = self.line_number if config.preserve_position else 0
msg = REX_COLOR.sub('', self.message).strip()
msg = REX_COLOR_NBQA.sub('', self.message).strip()
msg = REX_LINE_IN_MSG.sub('defined on line 0', msg)
line = f'{path}:{pos}: {self.severity}: {msg}'
if self.category != 'note':
Expand Down
2 changes: 2 additions & 0 deletions tests/test_commands/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
LINE1 = 'views.py:69: error: Hello world [assignment]\r\n'
LINE2 = 'settings.py:42: error: How are you? [union-attr]\r\n'

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


def run(cmd: list[str], exit_code: int = 0) -> str:
stdout = StringIO()
Expand Down
18 changes: 17 additions & 1 deletion tests/test_commands/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from mypy_baseline import main

from .helpers import LINE1, LINE2, run
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1, run


def test_filter():
Expand All @@ -25,6 +25,22 @@ def test_filter():
assert 'Your changes introduced' in actual


def test_filter_notebook():
stdin = StringIO()
stdin.write(NOTEBOOK_LINE1)

stdin.seek(0)
stdout = StringIO()
code = main(['filter'], stdin, stdout)
assert code == 1
stdout.seek(0)
actual = stdout.read()
assert NOTEBOOK_LINE1 in actual
assert ' return-value ' in actual
assert ' unresolved' in actual
assert 'Your changes introduced' in actual


def test_filter__empty_stdin():
actual = run(['filter'])
assert actual == ''
14 changes: 13 additions & 1 deletion tests/test_commands/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from mypy_baseline import main

from .helpers import LINE1, LINE2
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1


def test_sync(tmp_path: Path):
Expand All @@ -20,3 +20,15 @@ def test_sync(tmp_path: Path):
line1, line2 = actual.splitlines()
assert line1 == 'views.py:0: error: Hello world [assignment]'
assert line2 == 'settings.py:0: error: How are you? [union-attr]'


def test_sync_notebook(tmp_path: Path):
blpath = tmp_path / 'bline.txt'
stdin = StringIO()
stdin.write(NOTEBOOK_LINE1)
stdin.seek(0)
code = main(['sync', '--baseline-path', str(blpath)], stdin, StringIO())
assert code == 0
actual = blpath.read_text()
line1 = actual.splitlines()[0]
assert line1 == 'fail.ipynb:cell_1:0: error: Incompatible return value type (got "int", expected "str") [return-value]' # noqa: E501
10 changes: 9 additions & 1 deletion tests/test_commands/test_top_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pathlib import Path

from .helpers import LINE1, LINE2, run
from .helpers import LINE1, LINE2, NOTEBOOK_LINE1, run


def test_top_files(tmp_path: Path):
Expand All @@ -12,3 +12,11 @@ def test_top_files(tmp_path: Path):
actual = run(cmd)
assert 'views.py' in actual
assert 'settings.py' in actual


def test_top_notebook(tmp_path: Path):
blpath = tmp_path / 'bline.txt'
blpath.write_text(f'{NOTEBOOK_LINE1}')
cmd = ['top-files', '--baseline-path', str(blpath), '--no-color']
actual = run(cmd)
assert 'fail.ipynb' in actual

0 comments on commit 8371c77

Please sign in to comment.