Skip to content

Commit 048b082

Browse files
authored
Merge pull request #2 from johnfraney/lint-pycon-blocks
Add support for linting pycon code blocks
2 parents bfc6a95 + 55d77e0 commit 048b082

File tree

5 files changed

+113
-19
lines changed

5 files changed

+113
-19
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Flake8 Markdown lints [GitHub-style Python code blocks](https://help.github.com/
1111

1212
This package helps improve a Python project's documentation by ensuring that code samples are error-free.
1313

14+
## Features
15+
16+
- Lints code blocks containing regular Python and Python interpreter code ([`pycon`](http://pygments.org/docs/lexers/#pygments.lexers.python.PythonConsoleLexer))
17+
- [pre-commit](#pre-commit-hook) hook to lint on commit
18+
1419
## Installation
1520

1621
Flake8 Markdown can be installed from PyPI using `pip` or your package manager of choice:
@@ -46,7 +51,7 @@ To enable this hook in your local repository, add the following `repo` to your `
4651
# .pre-commit-config.yaml
4752
repos:
4853
- repo: https://github.com/johnfraney/flake8-markdown
49-
rev: v0.1.1
54+
rev: v0.2.0
5055
hooks:
5156
- id: flake8-markdown
5257
```
@@ -57,6 +62,12 @@ Everyone interacting in the project's codebases, issue trackers, chat rooms, and
5762
5863
## History
5964
65+
## [0.2.0] - 2019-06-14
66+
67+
### Added
68+
69+
- [`pycon`](http://pygments.org/docs/lexers/#pygments.lexers.python.PythonConsoleLexer) code block support
70+
6071
### [0.1.1] - 2019-05-19
6172

6273
#### Changed

flake8_markdown/__init__.py

+51-16
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
import sys
66
from concurrent.futures import ThreadPoolExecutor
77

8-
from .constants import SUBPROCESS_ARGS
8+
from flake8_markdown.constants import SUBPROCESS_ARGS
99

10-
__version__ = '0.1.1'
11-
12-
13-
def non_matching_lookbehind(pattern):
14-
return r'(?<={})'.format(pattern)
10+
__version__ = '0.2.0'
1511

1612

1713
def non_matching_lookahead(pattern):
@@ -26,13 +22,30 @@ def non_matching_group(pattern):
2622
return r'(?:{})'.format(pattern)
2723

2824

25+
def strip_repl_characters(code):
26+
"""Removes the first four characters from each REPL-style line.
27+
28+
>>> strip_repl_characters('>>> "banana"') == '"banana"'
29+
True
30+
>>> strip_repl_characters('... banana') == 'banana'
31+
True
32+
"""
33+
stripped_lines = []
34+
for line in code.splitlines():
35+
if line.startswith('>>> ') or line.startswith('... '):
36+
stripped_lines.append(line[4:])
37+
else:
38+
stripped_lines.append(line)
39+
return '\n'.join(stripped_lines)
40+
41+
2942
ONE_OR_MORE_LINES_NOT_GREEDY = r'(?:.*\n)+?'
3043

3144
regex_rule = ''.join([
3245
# Use non-matching group instead of a lookbehind because the code
3346
# block may have line highlighting hints. See:
3447
# https://python-markdown.github.io/extensions/fenced_code_blocks/#emphasized-lines
35-
non_matching_group('^```python.*$'),
48+
non_matching_group('^```(python|pycon).*$'),
3649
matching_group(ONE_OR_MORE_LINES_NOT_GREEDY),
3750
non_matching_lookahead('^```')
3851
])
@@ -44,28 +57,50 @@ def lint_markdown_file(markdown_file_path):
4457
linting_errors = []
4558
markdown_content = open(markdown_file_path, 'r').read()
4659
code_block_start_lines = []
47-
for line_no, line in enumerate(markdown_content.split('\n'), start=1):
48-
if line.startswith('```python'):
60+
for line_no, line in enumerate(markdown_content.splitlines(), start=1):
61+
# Match python and pycon
62+
if line.startswith('```py'):
4963
code_block_start_lines.append(line_no)
50-
matches = regex.findall(markdown_content)
51-
for match_number, match in enumerate(matches):
52-
match_text = match.lstrip()
64+
code_block_matches = regex.findall(markdown_content)
65+
for match_number, code_block_match in enumerate(code_block_matches):
66+
code_block_type = code_block_match[0]
67+
match_text = code_block_match[1]
68+
# pycon lines start with ">>> " or "... ", so strip those characters
69+
if code_block_type == 'pycon':
70+
match_text = strip_repl_characters(match_text)
71+
match_text = match_text.lstrip()
5372
flake8_process = subprocess.run(
5473
['flake8', '-'],
5574
input=match_text,
5675
**SUBPROCESS_ARGS,
5776
)
5877
flake8_output = flake8_process.stdout
59-
markdown_line_number = code_block_start_lines[match_number] + 1
78+
flake8_output = flake8_output.strip()
79+
# Skip empty lines
80+
if not flake8_output:
81+
continue
82+
flake8_output_split = flake8_output.split(':')
83+
line_number = int(flake8_output_split[1])
84+
column_number = int(flake8_output_split[2])
85+
markdown_line_number = (
86+
line_number + code_block_start_lines[match_number]
87+
)
88+
if code_block_type == 'pycon':
89+
match_lines = match_text.splitlines()
90+
line = match_lines[line_number - 1]
91+
if any([
92+
line.startswith('>>> '),
93+
line.startswith('... '),
94+
]):
95+
flake8_output_split[2] = column_number + 4
6096
# Replace reference to stdin line number with file line number
6197
flake8_output = re.sub(
6298
r'stdin:[0-9]+',
6399
'{}:{}'.format(markdown_file_path, markdown_line_number),
64100
flake8_output
65101
)
66-
stripped_output = flake8_output.strip()
67-
if stripped_output:
68-
linting_errors.append(stripped_output)
102+
linting_errors.append(flake8_output)
103+
69104
if linting_errors:
70105
linting_error_output = '\n'.join(linting_errors)
71106
print(linting_error_output)

pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "flake8-markdown"
3-
version = "0.1.1"
3+
version = "0.2.0"
44
description = "Lints Python code blocks in Markdown files using flake8"
55
authors = ["John Franey <johnfraney@gmail.com>"]
66
repository = "https://github.com/johnfraney/flake8-markdown"
@@ -17,7 +17,7 @@ classifiers = [
1717
"Topic :: Software Development :: Quality Assurance",
1818
]
1919
include = [
20-
"LICENCE",
20+
"LICENSE",
2121
]
2222

2323
[tool.poetry.dependencies]

tests/samples/pycon.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This file contains a Python console block to lint
2+
3+
This contains an unknown variable:
4+
5+
```pycon
6+
>>> print("Hello")
7+
'Hello'
8+
>>> banana = "banana"
9+
>>> for character in banana:
10+
... print(characterr)
11+
12+
```
13+
14+
This contains an EOL error:
15+
16+
```pycon
17+
>>> 'chocolate
18+
19+
```
20+
21+
This contains an undefined variable as a return:
22+
23+
```pycon
24+
>>> True
25+
false
26+
27+
```
28+
29+
This contains a valid code example:
30+
31+
```pycon
32+
>>> len([1, 2, 3])
33+
3
34+
35+
```

tests/test_flake8_markdown.py

+13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
FILE_WITH_ERRORS = 'tests/samples/basic.md'
88
FILE_WITHOUT_ERRORS = 'tests/samples/good.md'
99
FILE_WITH_EMPHASIZED_LINES = 'tests/samples/emphasized_lines.md'
10+
FILE_WITH_PYCON_BLOCKS = 'tests/samples/pycon.md'
1011

1112

1213
@pytest.fixture
@@ -75,6 +76,18 @@ def test_run_with_file_containing_emphasized_lines(run_flake8_markdown):
7576
assert "tests/samples/emphasized_lines.md:6:1: F821 undefined name 'emphasized_imaginary_function'" in output
7677

7778

79+
def test_run_with_file_containing_pycon_blocks(run_flake8_markdown):
80+
flake8_markdown_process = run_flake8_markdown(FILE_WITH_PYCON_BLOCKS)
81+
output = flake8_markdown_process.stdout
82+
print(output)
83+
assert flake8_markdown_process.returncode == 1
84+
error_count = len(output.splitlines())
85+
assert error_count == 3
86+
assert 'tests/samples/pycon.md:10:11: F821' in output
87+
assert 'tests/samples/pycon.md:17:10: E999' in output
88+
assert 'tests/samples/pycon.md:25:1: F821' in output
89+
90+
7891
def test_run_with_glob(run_flake8_markdown):
7992
flake8_markdown_process = run_flake8_markdown('tests/samples/*.md')
8093
assert flake8_markdown_process.returncode == 1

0 commit comments

Comments
 (0)