5
5
import sys
6
6
from concurrent .futures import ThreadPoolExecutor
7
7
8
- from .constants import SUBPROCESS_ARGS
8
+ from flake8_markdown .constants import SUBPROCESS_ARGS
9
9
10
- __version__ = '0.1.1'
11
-
12
-
13
- def non_matching_lookbehind (pattern ):
14
- return r'(?<={})' .format (pattern )
10
+ __version__ = '0.2.0'
15
11
16
12
17
13
def non_matching_lookahead (pattern ):
@@ -26,13 +22,30 @@ def non_matching_group(pattern):
26
22
return r'(?:{})' .format (pattern )
27
23
28
24
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
+
29
42
ONE_OR_MORE_LINES_NOT_GREEDY = r'(?:.*\n)+?'
30
43
31
44
regex_rule = '' .join ([
32
45
# Use non-matching group instead of a lookbehind because the code
33
46
# block may have line highlighting hints. See:
34
47
# https://python-markdown.github.io/extensions/fenced_code_blocks/#emphasized-lines
35
- non_matching_group ('^```python.*$' ),
48
+ non_matching_group ('^```( python|pycon) .*$' ),
36
49
matching_group (ONE_OR_MORE_LINES_NOT_GREEDY ),
37
50
non_matching_lookahead ('^```' )
38
51
])
@@ -44,28 +57,50 @@ def lint_markdown_file(markdown_file_path):
44
57
linting_errors = []
45
58
markdown_content = open (markdown_file_path , 'r' ).read ()
46
59
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' ):
49
63
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 ()
53
72
flake8_process = subprocess .run (
54
73
['flake8' , '-' ],
55
74
input = match_text ,
56
75
** SUBPROCESS_ARGS ,
57
76
)
58
77
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
60
96
# Replace reference to stdin line number with file line number
61
97
flake8_output = re .sub (
62
98
r'stdin:[0-9]+' ,
63
99
'{}:{}' .format (markdown_file_path , markdown_line_number ),
64
100
flake8_output
65
101
)
66
- stripped_output = flake8_output .strip ()
67
- if stripped_output :
68
- linting_errors .append (stripped_output )
102
+ linting_errors .append (flake8_output )
103
+
69
104
if linting_errors :
70
105
linting_error_output = '\n ' .join (linting_errors )
71
106
print (linting_error_output )
0 commit comments