Skip to content

Commit df44cc9

Browse files
committed
PyCQA#129 - Lookin good under the hood.
1 parent df5033d commit df44cc9

File tree

2 files changed

+135
-91
lines changed

2 files changed

+135
-91
lines changed

src/pydocstyle/checker.py

Lines changed: 121 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pyparsing as pr
88
from itertools import takewhile
99
from re import compile as re
10+
from collections import namedtuple
1011

1112
from . import violations
1213
from .config import IllegalConfiguration
@@ -411,44 +412,21 @@ def check_starts_with_this(self, function, docstring):
411412
if first_word.lower() == 'this':
412413
return violations.D404()
413414

414-
@check_for(Definition)
415-
def check_numpy_content(self, definition, docstring):
416-
"""Check the content of the docstring for numpy conventions."""
417-
pass
418-
419-
def check_numpy_parameters(self, section, content, definition, docstring):
420-
print "LALALAL"
421-
yield
422-
423-
def _check_numpy_section(self, section, content, definition, docstring):
424-
"""Check the content of the docstring for numpy conventions."""
425-
method_name = "check_numpy_%s" % section
426-
if hasattr(self, method_name):
427-
gen_func = getattr(self, method_name)
428-
429-
for err in gen_func(section, content, definition, docstring):
430-
yield err
431-
else:
432-
print "Now checking numpy section %s" % section
433-
for l in content:
434-
print "##", l
435-
436415
@staticmethod
437-
def _find_line_indices(lines, predicate):
438-
"""Return a list of indices of lines that match `predicate`."""
439-
return [i for i, line in enumerate(lines) if predicate(line)]
416+
def _get_leading_words(line):
417+
"""Return any leading set of words from `line`.
440418
441-
@staticmethod
442-
def _rstrip_non_alpha(line):
419+
For example, if `line` is " Hello world!!!", returns "Hello world".
420+
"""
443421
result = re("[A-Za-z ]+").match(line.strip())
444422
if result is not None:
445423
return result.group()
446424

447425
@staticmethod
448-
def _is_real_section(section, line, prev_line):
449-
"""Check if the suspected line is a real section name or not.
426+
def _is_a_docstring_section(context):
427+
"""Check if the suspected line is really a section.
450428
451-
This is done by checking the next conditions:
429+
This is done by checking the following conditions:
452430
* Does the current line has a suffix after the suspected section name?
453431
* Is the previous line not empty?
454432
* Does the previous line end with a punctuation mark?
@@ -460,85 +438,146 @@ def _is_real_section(section, line, prev_line):
460438
returns. <----- Not a real section name.
461439
'''
462440
"""
441+
section_name_suffix = context.line.lstrip(context.section_name).strip()
442+
463443
punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
464444
prev_line_ends_with_punctuation = \
465-
any(prev_line.endswith(x) for x in punctuation)
466-
prev_line_is_empty = prev_line == ''
445+
any(context.previous_line.strip().endswith(x) for x in punctuation)
467446

468-
suffix = line.lstrip(section).strip()
469-
470-
# If there's a suffix to our suspected section name, and the previous
471-
# line is not empty and ends with a punctuation mark, this is probably
472-
# a false-positive.
473-
return (suffix and not
474-
prev_line_ends_with_punctuation and not
475-
prev_line_is_empty)
447+
return not (section_name_suffix != '' and not
448+
prev_line_ends_with_punctuation and not
449+
context.previous_line.strip() == '')
476450

477451
@classmethod
478-
def _check_section(cls, line_index, dashes_indices, lines):
479-
line = lines[line_index].strip()
480-
prev_line = lines[line_index - 1].strip()
481-
section = cls._rstrip_non_alpha(line)
482-
capitalized_section = section.title()
452+
def _check_section_underline(cls, section_name, context, indentation):
453+
"""D4{07,08,09,10}, D215: Section underline checks.
454+
455+
Check for correct formatting for docstring sections. Checks that:
456+
* The line that follows the section name contains dashes (D40{7,8}).
457+
* The amount of dashes is equal to the length of the section
458+
name (D409).
459+
* The line that follows the section header (with or without dashes)
460+
is empty (D410).
461+
* The indentation of the dashed line is equal to the docstring's
462+
indentation (D215).
463+
"""
464+
dash_line_found = False
465+
next_non_empty_line_offset = 0
466+
467+
for line in context.following_lines:
468+
line_set = ''.join(set(line.strip()))
469+
if line_set != '':
470+
dash_line_found = line_set == '-'
471+
break
472+
next_non_empty_line_offset += 1
473+
474+
if not dash_line_found:
475+
yield violations.D407(section_name)
476+
if next_non_empty_line_offset == 0:
477+
yield violations.D410(section_name)
478+
else:
479+
if next_non_empty_line_offset > 0:
480+
yield violations.D408(section_name)
483481

484-
if cls._is_real_section(section, line, prev_line):
485-
return
482+
dash_line = context.following_lines[next_non_empty_line_offset]
483+
if dash_line.strip() != "-" * len(section_name):
484+
yield violations.D409(section_name,
485+
len(section_name),
486+
len(dash_line.strip()))
486487

487-
suffix = line.lstrip(section).strip()
488-
if suffix:
489-
yield violations.D406(capitalized_section, suffix)
488+
line_after_dashes = \
489+
context.following_lines[next_non_empty_line_offset + 1]
490+
if line_after_dashes.strip() != '':
491+
yield violations.D410(section_name)
490492

491-
if prev_line != '':
492-
yield violations.D410(capitalized_section) # Missing blank line
493+
if leading_space(dash_line) > indentation:
494+
yield violations.D215(section_name)
493495

494-
if (section not in cls.SECTION_NAMES and
496+
@classmethod
497+
def _check_section(cls, docstring, definition, context):
498+
"""D4{05,06,11}, D214: Section name checks.
499+
500+
Check for valid section names. Checks that:
501+
* The section name is properly capitalized (D405).
502+
* The section is not over-indented (D214).
503+
* The section name has no superfluous suffix to it (D406).
504+
* There's a blank line before the section (D411).
505+
506+
Also yields all the errors from `_check_section_underline`.
507+
"""
508+
capitalized_section = context.section_name.title()
509+
indentation = cls._get_docstring_indent(definition, docstring)
510+
511+
if (context.section_name not in cls.SECTION_NAMES and
495512
capitalized_section in cls.SECTION_NAMES):
496-
yield violations.D405(capitalized_section, section)
513+
yield violations.D405(capitalized_section, context.section_name)
497514

498-
next_line_index = line_index + 1
499-
if next_line_index not in dashes_indices:
500-
yield violations.D407(capitalized_section)
501-
else:
502-
if lines[next_line_index].strip() != "-" * len(section):
503-
# The length of the underlining dashes does not
504-
# match the length of the section name.
505-
yield violations.D408(section, len(section))
515+
if leading_space(context.line) > indentation:
516+
yield violations.D214(capitalized_section)
517+
518+
suffix = context.line.strip().lstrip(context.section_name)
519+
if suffix != '':
520+
yield violations.D406(capitalized_section, suffix)
506521

507-
# If there are no dashes - the next line after the section name
508-
# should be empty. Otherwise, it's the next line after the dashes.
509-
# This is why we increment the line index by 1 here.
510-
next_line_index += 1
522+
if context.previous_line.strip() != '':
523+
yield violations.D411(capitalized_section)
511524

512-
if lines[next_line_index].strip():
513-
yield violations.D409(capitalized_section)
525+
for err in cls._check_section_underline(capitalized_section,
526+
context,
527+
indentation):
528+
yield err
514529

515530
@check_for(Definition)
516-
def check_docstring_internal_structure(self, definition, docstring):
517-
"""Parse the general structure of a numpy docstring and check it."""
531+
def check_docstring_sections(self, definition, docstring):
532+
"""D21{4,5}, D4{05,06,07,08,09,10}: Docstring sections checks.
533+
534+
Check the general format of a sectioned docstring:
535+
'''This is my one-liner.
536+
537+
Short Summary
538+
-------------
539+
540+
This is my summary.
541+
542+
Returns
543+
-------
544+
545+
None.
546+
'''
547+
548+
Section names appear in `SECTION_NAMES`.
549+
"""
518550
if not docstring:
519551
return
520552

521553
lines = docstring.split("\n")
522554
if len(lines) < 2:
523-
# It's not a multiple lined docstring
524555
return
525556

526557
lower_section_names = [s.lower() for s in self.SECTION_NAMES]
527558

528-
def _suspected_as_section(line):
529-
result = self._rstrip_non_alpha(line.lower())
559+
def _suspected_as_section(_line):
560+
result = self._get_leading_words(_line.lower())
530561
return result in lower_section_names
531562

532-
def _contains_only_dashes(line):
533-
return ''.join(set(line.strip())) == '-'
534-
535563
# Finding our suspects.
536-
section_indices = self._find_line_indices(lines, _suspected_as_section)
537-
dashes_indices = self._find_line_indices(lines, _contains_only_dashes)
538-
539-
for i in section_indices:
540-
for err in self._check_section(i, dashes_indices, lines):
541-
yield err
564+
suspected_section_indices = [i for i, line in enumerate(lines) if
565+
_suspected_as_section(line)]
566+
567+
context = namedtuple('SectionContext', ('section_name',
568+
'previous_line',
569+
'line',
570+
'following_lines'))
571+
572+
contexts = (context(self._get_leading_words(lines[i].strip()),
573+
lines[i - 1],
574+
lines[i],
575+
lines[i + 1:]) for i in suspected_section_indices)
576+
577+
for ctx in contexts:
578+
if self._is_a_docstring_section(ctx):
579+
for err in self._check_section(docstring, definition, ctx):
580+
yield err
542581

543582

544583
parse = Parser()

src/pydocstyle/violations.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,9 @@ def to_rst(cls):
188188
'at the first line')
189189
D213 = D2xx.create_error('D213', 'Multi-line docstring summary should start '
190190
'at the second line')
191-
D214 = D2xx.create_error('D214', 'Section or section underline is '
192-
'over-indented', 'in section {0!r}')
191+
D214 = D2xx.create_error('D214', 'Section is over-indented', '{0!r}')
192+
D215 = D2xx.create_error('D215', 'Section underline is over-indented',
193+
'in section {0!r}')
193194

194195
D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues')
195196
D300 = D3xx.create_error('D300', 'Use """triple double quotes"""',
@@ -212,14 +213,16 @@ def to_rst(cls):
212213
'{0!r}, not {1!r}')
213214
D406 = D4xx.create_error('D406', 'Section name should end with a newline',
214215
'{0!r}, found {1!r}')
215-
D407 = D4xx.create_error('D407', 'Missing dashed underline after section name',
216-
'section={0!r}')
217-
D408 = D4xx.create_error('D408', 'Section underline should match the length '
218-
'of the section\'s name', 'len({0!r}) == {1!r}')
219-
D409 = D4xx.create_error('D409', 'Missing blank line after section name',
216+
D407 = D4xx.create_error('D407', 'Missing dashed underline after section',
220217
'{0!r}')
221-
D410 = D4xx.create_error('D410', 'Missing blank line before section name',
218+
D408 = D4xx.create_error('D408', 'Section underline should be in the line '
219+
'following the section\'s name',
222220
'{0!r}')
221+
D409 = D4xx.create_error('D409', 'Section underline should match the length '
222+
'of its name',
223+
'len({0!r}) == {1!r}, got {2!r} dashes')
224+
D410 = D4xx.create_error('D410', 'Missing blank line after section', '{0!r}')
225+
D411 = D4xx.create_error('D411', 'Missing blank line before section', '{0!r}')
223226

224227

225228
class AttrDict(dict):
@@ -229,6 +232,8 @@ def __getattr__(self, item):
229232
all_errors = set(ErrorRegistry.get_error_codes())
230233

231234
conventions = AttrDict({
232-
'pep257': all_errors - {'D203', 'D212', 'D213', 'D404'},
235+
'pep257': all_errors - {'D203', 'D212', 'D213', 'D214', 'D215', 'D404',
236+
'D405', 'D406', 'D407', 'D408', 'D409', 'D410',
237+
'D411'},
233238
'numpy': all_errors - {'D203', 'D212', 'D213', 'D402'}
234239
})

0 commit comments

Comments
 (0)