Skip to content

Commit df5033d

Browse files
committed
PyCQA#129 - Disgusting code is now prettier.
1 parent 8831109 commit df5033d

File tree

2 files changed

+67
-141
lines changed

2 files changed

+67
-141
lines changed

src/pydocstyle/checker.py

Lines changed: 63 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,19 @@ class PEP257Checker(object):
4444
4545
"""
4646

47-
ALL_NUMPY_SECTIONS = ['Short Summary',
48-
'Extended Summary',
49-
'Parameters',
50-
'Returns',
51-
'Yields',
52-
'Other Parameters',
53-
'Raises',
54-
'See Also',
55-
'Notes',
56-
'References',
57-
'Examples',
58-
'Attributes',
59-
'Methods']
47+
SECTION_NAMES = ['Short Summary',
48+
'Extended Summary',
49+
'Parameters',
50+
'Returns',
51+
'Yields',
52+
'Other Parameters',
53+
'Raises',
54+
'See Also',
55+
'Notes',
56+
'References',
57+
'Examples',
58+
'Attributes',
59+
'Methods']
6060

6161
def check_source(self, source, filename, ignore_decorators):
6262
module = parse(StringIO(source), filename)
@@ -444,35 +444,73 @@ def _rstrip_non_alpha(line):
444444
if result is not None:
445445
return result.group()
446446

447+
@staticmethod
448+
def _is_real_section(section, line, prev_line):
449+
"""Check if the suspected line is a real section name or not.
450+
451+
This is done by checking the next conditions:
452+
* Does the current line has a suffix after the suspected section name?
453+
* Is the previous line not empty?
454+
* Does the previous line end with a punctuation mark?
455+
456+
If so, this is probably not a real section name. For example:
457+
'''Title.
458+
459+
Some part of the docstring that specifies what the function
460+
returns. <----- Not a real section name.
461+
'''
462+
"""
463+
punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
464+
prev_line_ends_with_punctuation = \
465+
any(prev_line.endswith(x) for x in punctuation)
466+
prev_line_is_empty = prev_line == ''
467+
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)
476+
447477
@classmethod
448478
def _check_section(cls, line_index, dashes_indices, lines):
449479
line = lines[line_index].strip()
480+
prev_line = lines[line_index - 1].strip()
450481
section = cls._rstrip_non_alpha(line)
451-
actual_section = section.title()
482+
capitalized_section = section.title()
452483

453-
if (section not in cls.ALL_NUMPY_SECTIONS and
454-
section.title() in cls.ALL_NUMPY_SECTIONS):
455-
# The capitalized section string is not in the line,
456-
# meaning that the words appear there but not properly capitalized.
457-
yield violations.D405(actual_section, section)
484+
if cls._is_real_section(section, line, prev_line):
485+
return
458486

459487
suffix = line.lstrip(section).strip()
460488
if suffix:
461-
# The section name should end with a newline.
462-
yield violations.D406(actual_section, suffix)
489+
yield violations.D406(capitalized_section, suffix)
490+
491+
if prev_line != '':
492+
yield violations.D410(capitalized_section) # Missing blank line
493+
494+
if (section not in cls.SECTION_NAMES and
495+
capitalized_section in cls.SECTION_NAMES):
496+
yield violations.D405(capitalized_section, section)
463497

464498
next_line_index = line_index + 1
465499
if next_line_index not in dashes_indices:
466-
yield violations.D407(actual_section)
500+
yield violations.D407(capitalized_section)
467501
else:
468-
next_line_index += 1
469502
if lines[next_line_index].strip() != "-" * len(section):
470503
# The length of the underlining dashes does not
471504
# match the length of the section name.
472505
yield violations.D408(section, len(section))
473506

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
511+
474512
if lines[next_line_index].strip():
475-
yield violations.D409(actual_section)
513+
yield violations.D409(capitalized_section)
476514

477515
@check_for(Definition)
478516
def check_docstring_internal_structure(self, definition, docstring):
@@ -485,7 +523,7 @@ def check_docstring_internal_structure(self, definition, docstring):
485523
# It's not a multiple lined docstring
486524
return
487525

488-
lower_section_names = [s.lower() for s in self.ALL_NUMPY_SECTIONS]
526+
lower_section_names = [s.lower() for s in self.SECTION_NAMES]
489527

490528
def _suspected_as_section(line):
491529
result = self._rstrip_non_alpha(line.lower())
@@ -502,121 +540,6 @@ def _contains_only_dashes(line):
502540
for err in self._check_section(i, dashes_indices, lines):
503541
yield err
504542

505-
def SKIP_check_numpy(self, definition, docstring):
506-
"""Parse the general structure of a numpy docstring and check it."""
507-
if not docstring:
508-
return
509-
510-
lines = docstring.split("\n")
511-
if len(lines) < 2:
512-
# It's not a multiple lined docstring
513-
return
514-
515-
lines_generator = ScrollableIterator(lines[1:]) # Skipping first line
516-
indent = self._get_docstring_indent(definition, docstring)
517-
518-
current_section = None
519-
curr_section_lines = []
520-
start_collecting_lines = False
521-
522-
for line in lines_generator:
523-
for section in self.ALL_NUMPY_SECTIONS:
524-
with_colon = section.lower() + ':'
525-
if line.strip().lower() in [section.lower(), with_colon]:
526-
# There's a chance that this line is a numpy section
527-
try:
528-
next_line = lines_generator.next()
529-
except StopIteration:
530-
# It probably isn't :)
531-
return
532-
533-
if ''.join(set(next_line.strip())) == '-':
534-
# The next line contains only dashes, there's a good
535-
# chance that it's a numpy section
536-
537-
if (leading_space(line) > indent or
538-
leading_space(next_line) > indent):
539-
yield violations.D214(section)
540-
541-
if section not in line:
542-
# The capitalized section string is not in the line,
543-
# meaning that the word appears there but not
544-
# properly capitalized.
545-
yield violations.D405(section, line.strip())
546-
elif line.strip().lower() == with_colon:
547-
# The section name should not end with a colon.
548-
yield violations.D406(section, line.strip())
549-
550-
if next_line.strip() != "-" * len(section):
551-
# The length of the underlining dashes does not
552-
# match the length of the section name.
553-
yield violations.D407(section, len(section))
554-
555-
# At this point, we're done with the structured part of
556-
# the section and its underline.
557-
# We will now collect the content of each section and
558-
# let section handlers deal with it.
559-
560-
if current_section is not None:
561-
for err in self._check_numpy_section(
562-
current_section,
563-
curr_section_lines,
564-
definition,
565-
docstring):
566-
yield err
567-
568-
start_collecting_lines = True
569-
current_section = section.lower()
570-
curr_section_lines = []
571-
else:
572-
# The next line does not contain only dashes, so it's
573-
# not likely to be a section header.
574-
lines_generator.scroll_back()
575-
576-
if current_section is not None:
577-
if start_collecting_lines:
578-
start_collecting_lines = False
579-
else:
580-
curr_section_lines.append(line)
581-
582-
if current_section is not None:
583-
for err in self._check_numpy_section(current_section,
584-
curr_section_lines,
585-
definition,
586-
docstring):
587-
yield err
588-
589-
590-
class ScrollableIterator(object):
591-
"""An iterator over an iterable that can be moved back during iteration."""
592-
593-
def __init__(self, iterable):
594-
self._iterable = iterable
595-
self._index = 0
596-
597-
def __iter__(self):
598-
return self
599-
600-
def next(self):
601-
"""Generate the next item or raise StopIteration."""
602-
try:
603-
return self._iterable[self._index]
604-
except IndexError:
605-
raise StopIteration()
606-
finally:
607-
self._index += 1
608-
609-
def scroll_back(self, num=1):
610-
"""Move the generator `num` items backwards."""
611-
if num < 0:
612-
raise ValueError('num cannot be a negative number')
613-
self._index = max(0, self._index - num)
614-
615-
def clone(self):
616-
"""Return a copy of the generator set to the same item index."""
617-
obj_copy = self.__class__(self._iterable)
618-
obj_copy._index = self._index
619-
620543

621544
parse = Parser()
622545

src/pydocstyle/violations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,10 @@ def to_rst(cls):
216216
'section={0!r}')
217217
D408 = D4xx.create_error('D408', 'Section underline should match the length '
218218
'of the section\'s name', 'len({0!r}) == {1!r}')
219-
D409 = D4xx.create_error('D409', 'Missing blank line after section', '{0!r}')
219+
D409 = D4xx.create_error('D409', 'Missing blank line after section name',
220+
'{0!r}')
221+
D410 = D4xx.create_error('D410', 'Missing blank line before section name',
222+
'{0!r}')
220223

221224

222225
class AttrDict(dict):

0 commit comments

Comments
 (0)