Skip to content

Commit d1e443b

Browse files
committed
PyCQA#129 - Über merge of doom.
1 parent 8925df3 commit d1e443b

File tree

2 files changed

+169
-4
lines changed

2 files changed

+169
-4
lines changed

src/pydocstyle/checker.py

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ class PEP257Checker(object):
4343
4444
"""
4545

46+
ALL_NUMPY_SECTIONS = ['Short Summary',
47+
'Extended Summary',
48+
'Parameters',
49+
'Returns',
50+
'Yields',
51+
'Other Parameters',
52+
'Raises',
53+
'See Also',
54+
'Notes',
55+
'References',
56+
'Examples',
57+
'Attributes',
58+
'Methods']
59+
4660
def check_source(self, source, filename, ignore_decorators):
4761
module = parse(StringIO(source), filename)
4862
for definition in module:
@@ -54,7 +68,7 @@ def check_source(self, source, filename, ignore_decorators):
5468
len(ignore_decorators.findall(dec.name)) > 0
5569
for dec in definition.decorators)
5670
if not skipping_all and not decorator_skip:
57-
error = this_check(None, definition,
71+
error = this_check(self, definition,
5872
definition.docstring)
5973
else:
6074
error = None
@@ -190,6 +204,13 @@ def check_blank_after_summary(self, definition, docstring):
190204
if blanks_count != 1:
191205
return violations.D205(blanks_count)
192206

207+
@staticmethod
208+
def _get_docstring_indent(definition, docstring):
209+
"""Return the indentation of the docstring's opening quotes."""
210+
before_docstring, _, _ = definition.source.partition(docstring)
211+
_, _, indent = before_docstring.rpartition('\n')
212+
return indent
213+
193214
@check_for(Definition)
194215
def check_indent(self, definition, docstring):
195216
"""D20{6,7,8}: The entire docstring should be indented same as code.
@@ -199,8 +220,7 @@ def check_indent(self, definition, docstring):
199220
200221
"""
201222
if docstring:
202-
before_docstring, _, _ = definition.source.partition(docstring)
203-
_, _, indent = before_docstring.rpartition('\n')
223+
indent = self._get_docstring_indent(definition, docstring)
204224
lines = docstring.split('\n')
205225
if len(lines) > 1:
206226
lines = lines[1:] # First line does not need indent.
@@ -390,6 +410,144 @@ def check_starts_with_this(self, function, docstring):
390410
if first_word.lower() == 'this':
391411
return violations.D404()
392412

413+
@check_for(Definition)
414+
def check_numpy_content(self, definition, docstring):
415+
"""Check the content of the docstring for numpy conventions."""
416+
pass
417+
418+
def check_numpy_parameters(self, section, content, definition, docstring):
419+
print "LALALAL"
420+
yield
421+
422+
def _check_numpy_section(self, section, content, definition, docstring):
423+
"""Check the content of the docstring for numpy conventions."""
424+
method_name = "check_numpy_%s" % section
425+
if hasattr(self, method_name):
426+
gen_func = getattr(self, method_name)
427+
428+
for err in gen_func(section, content, definition, docstring):
429+
yield err
430+
else:
431+
print "Now checking numpy section %s" % section
432+
for l in content:
433+
print "##", l
434+
435+
@check_for(Definition)
436+
def check_numpy(self, definition, docstring):
437+
"""Parse the general structure of a numpy docstring and check it."""
438+
if not docstring:
439+
return
440+
441+
lines = docstring.split("\n")
442+
if len(lines) < 2:
443+
# It's not a multiple lined docstring
444+
return
445+
446+
lines_generator = ScrollableGenerator(lines[1:]) # Skipping first line
447+
indent = self._get_docstring_indent(definition, docstring)
448+
449+
current_section = None
450+
curr_section_lines = []
451+
start_collecting_lines = False
452+
453+
for line in lines_generator:
454+
for section in self.ALL_NUMPY_SECTIONS:
455+
with_colon = section.lower() + ':'
456+
if line.strip().lower() in [section.lower(), with_colon]:
457+
# There's a chance that this line is a numpy section
458+
try:
459+
next_line = lines_generator.next()
460+
except StopIteration:
461+
# It probably isn't :)
462+
return
463+
464+
if ''.join(set(next_line.strip())) == '-':
465+
# The next line contains only dashes, there's a good
466+
# chance that it's a numpy section
467+
468+
if (leading_space(line) > indent or
469+
leading_space(next_line) > indent):
470+
yield violations.D214(section)
471+
472+
if section not in line:
473+
# The capitalized section string is not in the line,
474+
# meaning that the word appears there but not
475+
# properly capitalized.
476+
yield violations.D405(section, line.strip())
477+
elif line.strip().lower() == with_colon:
478+
# The section name should not end with a colon.
479+
yield violations.D406(section, line.strip())
480+
481+
if next_line.strip() != "-" * len(section):
482+
# The length of the underlining dashes does not
483+
# match the length of the section name.
484+
yield violations.D407(section, len(section))
485+
486+
# At this point, we're done with the structured part of
487+
# the section and its underline.
488+
# We will not collect the content of each section and
489+
# let section handlers deal with it.
490+
491+
if current_section is not None:
492+
for err in self._check_numpy_section(
493+
current_section,
494+
curr_section_lines,
495+
definition,
496+
docstring):
497+
yield err
498+
499+
start_collecting_lines = True
500+
current_section = section.lower()
501+
curr_section_lines = []
502+
else:
503+
# The next line does not contain only dashes, so it's
504+
# not likely to be a section header.
505+
lines_generator.scroll_back()
506+
507+
if current_section is not None:
508+
if start_collecting_lines:
509+
start_collecting_lines = False
510+
else:
511+
curr_section_lines.append(line)
512+
513+
if current_section is not None:
514+
for err in self._check_numpy_section(current_section,
515+
curr_section_lines,
516+
definition,
517+
docstring):
518+
yield err
519+
520+
521+
class ScrollableGenerator(object):
522+
"""A generator over a list that can be moved back during iteration."""
523+
524+
def __init__(self, list_like):
525+
self._list_like = list_like
526+
self._index = 0
527+
528+
def __iter__(self):
529+
return self
530+
531+
def next(self):
532+
"""Generate the next item or raise StopIteration."""
533+
try:
534+
return self._list_like[self._index]
535+
except IndexError:
536+
raise StopIteration()
537+
finally:
538+
self._index += 1
539+
540+
def scroll_back(self, num=1):
541+
"""Move the generator `num` items backwards."""
542+
if num < 0:
543+
raise ValueError('num cannot be a negative number')
544+
self._index = max(0, self._index - num)
545+
546+
def clone(self):
547+
"""Return a copy of the generator set to the same item index."""
548+
obj_copy = self.__class__(self._list_like)
549+
obj_copy._index = self._index
550+
393551

394552
parse = Parser()
395553

src/pydocstyle/violations.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,12 @@ def to_rst(cls):
206206
'properly capitalized', '{0!r}, not {1!r}')
207207
D404 = D4xx.create_error('D404', 'First word of the docstring should not '
208208
'be `This`')
209+
D405 = D4xx.create_error('D405', 'Section name should be properly capitalized',
210+
'{0!r}, not {1!r}')
211+
D406 = D4xx.create_error('D406', 'Section name should not end with a colon',
212+
'{0!r}, not {1!r}')
213+
D407 = D4xx.create_error('D407', 'Section underline should match the length '
214+
'of the section\'s name', 'len({0!r}) == {1!r}')
209215

210216

211217
class AttrDict(dict):
@@ -215,5 +221,6 @@ def __getattr__(self, item):
215221
all_errors = set(ErrorRegistry.get_error_codes())
216222

217223
conventions = AttrDict({
218-
'pep257': all_errors - {'D203', 'D212', 'D213', 'D404'}
224+
'pep257': all_errors - {'D203', 'D212', 'D213', 'D404'},
225+
'numpy': all_errors - {'D203', 'D212', 'D213', 'D402'}
219226
})

0 commit comments

Comments
 (0)