Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit 633031b

Browse files
authored
Merge pull request #226 from farmersez/numpy-conventions
Numpy conventions
2 parents e416156 + a80de49 commit 633031b

File tree

7 files changed

+426
-11
lines changed

7 files changed

+426
-11
lines changed

docs/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ New Features
2020
* Decorator-based skipping via ``--ignore-decorators`` has been added (#204).
2121
* Support for using pycodestyle style wildcards has been added (#72, #209).
2222
* Superfluous opening quotes are now reported as part of D300 (#166, #225).
23+
* Support for ``numpy`` conventions verification has been added (#129).
2324

2425
Bug Fixes
2526

src/pydocstyle/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
from .utils import __version__
44

55
# Temporary hotfix for flake8-docstrings
6-
from .checker import PEP257Checker, tokenize_open
6+
from .checker import ConventionChecker, tokenize_open
77
from .parser import AllError

src/pydocstyle/checker.py

Lines changed: 210 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import tokenize as tk
77
from itertools import takewhile
88
from re import compile as re
9+
from collections import namedtuple
910

1011
from . import violations
1112
from .config import IllegalConfiguration
@@ -33,8 +34,8 @@ def decorator(f):
3334
return decorator
3435

3536

36-
class PEP257Checker(object):
37-
"""Checker for PEP 257.
37+
class ConventionChecker(object):
38+
"""Checker for PEP 257 and numpy conventions.
3839
3940
D10x: Missing docstrings
4041
D20x: Whitespace issues
@@ -43,6 +44,20 @@ class PEP257Checker(object):
4344
4445
"""
4546

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']
60+
4661
def check_source(self, source, filename, ignore_decorators):
4762
module = parse(StringIO(source), filename)
4863
for definition in module:
@@ -54,7 +69,7 @@ def check_source(self, source, filename, ignore_decorators):
5469
len(ignore_decorators.findall(dec.name)) > 0
5570
for dec in definition.decorators)
5671
if not skipping_all and not decorator_skip:
57-
error = this_check(None, definition,
72+
error = this_check(self, definition,
5873
definition.docstring)
5974
else:
6075
error = None
@@ -190,6 +205,13 @@ def check_blank_after_summary(self, definition, docstring):
190205
if blanks_count != 1:
191206
return violations.D205(blanks_count)
192207

208+
@staticmethod
209+
def _get_docstring_indent(definition, docstring):
210+
"""Return the indentation of the docstring's opening quotes."""
211+
before_docstring, _, _ = definition.source.partition(docstring)
212+
_, _, indent = before_docstring.rpartition('\n')
213+
return indent
214+
193215
@check_for(Definition)
194216
def check_indent(self, definition, docstring):
195217
"""D20{6,7,8}: The entire docstring should be indented same as code.
@@ -199,8 +221,7 @@ def check_indent(self, definition, docstring):
199221
200222
"""
201223
if docstring:
202-
before_docstring, _, _ = definition.source.partition(docstring)
203-
_, _, indent = before_docstring.rpartition('\n')
224+
indent = self._get_docstring_indent(definition, docstring)
204225
lines = docstring.split('\n')
205226
if len(lines) > 1:
206227
lines = lines[1:] # First line does not need indent.
@@ -390,6 +411,188 @@ def check_starts_with_this(self, function, docstring):
390411
if first_word.lower() == 'this':
391412
return violations.D404()
392413

414+
@staticmethod
415+
def _get_leading_words(line):
416+
"""Return any leading set of words from `line`.
417+
418+
For example, if `line` is " Hello world!!!", returns "Hello world".
419+
"""
420+
result = re("[A-Za-z ]+").match(line.strip())
421+
if result is not None:
422+
return result.group()
423+
424+
@staticmethod
425+
def _is_a_docstring_section(context):
426+
"""Check if the suspected context is really a section header.
427+
428+
Lets have a look at the following example docstring:
429+
'''Title.
430+
431+
Some part of the docstring that specifies what the function
432+
returns. <----- Not a real section name. It has a suffix and the
433+
previous line is not empty and does not end with
434+
a punctuation sign.
435+
436+
This is another line in the docstring. It describes stuff,
437+
but we forgot to add a blank line between it and the section name.
438+
Returns <----- A real section name. The previous line ends with
439+
------- a period, therefore it is in a new
440+
grammatical context.
441+
Bla.
442+
443+
'''
444+
445+
To make sure this is really a section we check these conditions:
446+
* There's no suffix to the section name.
447+
* The previous line ends with punctuation.
448+
* The previous line is empty.
449+
450+
If one of the conditions is true, we will consider the line as
451+
a section name.
452+
"""
453+
section_name_suffix = context.line.lstrip(context.section_name).strip()
454+
455+
punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
456+
prev_line_ends_with_punctuation = \
457+
any(context.previous_line.strip().endswith(x) for x in punctuation)
458+
459+
return (is_blank(section_name_suffix) or
460+
prev_line_ends_with_punctuation or
461+
is_blank(context.previous_line))
462+
463+
@classmethod
464+
def _check_section_underline(cls, section_name, context, indentation):
465+
"""D4{07,08,09,10}, D215: Section underline checks.
466+
467+
Check for correct formatting for docstring sections. Checks that:
468+
* The line that follows the section name contains
469+
dashes (D40{7,8}).
470+
* The amount of dashes is equal to the length of the section
471+
name (D409).
472+
* The line that follows the section header (with or without dashes)
473+
is empty (D410).
474+
* The indentation of the dashed line is equal to the docstring's
475+
indentation (D215).
476+
"""
477+
dash_line_found = False
478+
next_non_empty_line_offset = 0
479+
480+
for line in context.following_lines:
481+
line_set = ''.join(set(line.strip()))
482+
if not is_blank(line_set):
483+
dash_line_found = line_set == '-'
484+
break
485+
next_non_empty_line_offset += 1
486+
487+
if not dash_line_found:
488+
yield violations.D407(section_name)
489+
if next_non_empty_line_offset == 0:
490+
yield violations.D410(section_name)
491+
else:
492+
if next_non_empty_line_offset > 0:
493+
yield violations.D408(section_name)
494+
495+
dash_line = context.following_lines[next_non_empty_line_offset]
496+
if dash_line.strip() != "-" * len(section_name):
497+
yield violations.D409(len(section_name),
498+
section_name,
499+
len(dash_line.strip()))
500+
501+
line_after_dashes = \
502+
context.following_lines[next_non_empty_line_offset + 1]
503+
if not is_blank(line_after_dashes):
504+
yield violations.D410(section_name)
505+
506+
if leading_space(dash_line) > indentation:
507+
yield violations.D215(section_name)
508+
509+
@classmethod
510+
def _check_section(cls, docstring, definition, context):
511+
"""D4{05,06,11}, D214: Section name checks.
512+
513+
Check for valid section names. Checks that:
514+
* The section name is properly capitalized (D405).
515+
* The section is not over-indented (D214).
516+
* The section name has no superfluous suffix to it (D406).
517+
* There's a blank line before the section (D411).
518+
519+
Also yields all the errors from `_check_section_underline`.
520+
"""
521+
capitalized_section = context.section_name.title()
522+
indentation = cls._get_docstring_indent(definition, docstring)
523+
524+
if (context.section_name not in cls.SECTION_NAMES and
525+
capitalized_section in cls.SECTION_NAMES):
526+
yield violations.D405(capitalized_section, context.section_name)
527+
528+
if leading_space(context.line) > indentation:
529+
yield violations.D214(capitalized_section)
530+
531+
suffix = context.line.strip().lstrip(context.section_name)
532+
if suffix:
533+
yield violations.D406(capitalized_section, context.line.strip())
534+
535+
if not is_blank(context.previous_line):
536+
yield violations.D411(capitalized_section)
537+
538+
for err in cls._check_section_underline(capitalized_section,
539+
context,
540+
indentation):
541+
yield err
542+
543+
@check_for(Definition)
544+
def check_docstring_sections(self, definition, docstring):
545+
"""D21{4,5}, D4{05,06,07,08,09,10}: Docstring sections checks.
546+
547+
Check the general format of a sectioned docstring:
548+
'''This is my one-liner.
549+
550+
Short Summary
551+
-------------
552+
553+
This is my summary.
554+
555+
Returns
556+
-------
557+
558+
None.
559+
'''
560+
561+
Section names appear in `SECTION_NAMES`.
562+
"""
563+
if not docstring:
564+
return
565+
566+
lines = docstring.split("\n")
567+
if len(lines) < 2:
568+
return
569+
570+
lower_section_names = [s.lower() for s in self.SECTION_NAMES]
571+
572+
def _suspected_as_section(_line):
573+
result = self._get_leading_words(_line.lower())
574+
return result in lower_section_names
575+
576+
# Finding our suspects.
577+
suspected_section_indices = [i for i, line in enumerate(lines) if
578+
_suspected_as_section(line)]
579+
580+
SectionContext = namedtuple('SectionContext', ('section_name',
581+
'previous_line',
582+
'line',
583+
'following_lines'))
584+
585+
contexts = (SectionContext(self._get_leading_words(lines[i].strip()),
586+
lines[i - 1],
587+
lines[i],
588+
lines[i + 1:])
589+
for i in suspected_section_indices)
590+
591+
for ctx in contexts:
592+
if self._is_a_docstring_section(ctx):
593+
for err in self._check_section(docstring, definition, ctx):
594+
yield err
595+
393596

394597
parse = Parser()
395598

@@ -439,8 +642,8 @@ def check(filenames, select=None, ignore=None, ignore_decorators=None):
439642
try:
440643
with tokenize_open(filename) as file:
441644
source = file.read()
442-
for error in PEP257Checker().check_source(source, filename,
443-
ignore_decorators):
645+
for error in ConventionChecker().check_source(source, filename,
646+
ignore_decorators):
444647
code = getattr(error, 'code', None)
445648
if code in checked_codes:
446649
yield error

src/pydocstyle/violations.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +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 is over-indented', '{0!r}')
192+
D215 = D2xx.create_error('D215', 'Section underline is over-indented',
193+
'in section {0!r}')
191194

192195
D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues')
193196
D300 = D3xx.create_error('D300', 'Use """triple double quotes"""',
@@ -206,6 +209,20 @@ def to_rst(cls):
206209
'properly capitalized', '{0!r}, not {1!r}')
207210
D404 = D4xx.create_error('D404', 'First word of the docstring should not '
208211
'be `This`')
212+
D405 = D4xx.create_error('D405', 'Section name should be properly capitalized',
213+
'{0!r}, not {1!r}')
214+
D406 = D4xx.create_error('D406', 'Section name should end with a newline',
215+
'{0!r}, not {1!r}')
216+
D407 = D4xx.create_error('D407', 'Missing dashed underline after section',
217+
'{0!r}')
218+
D408 = D4xx.create_error('D408', 'Section underline should be in the line '
219+
'following the section\'s name',
220+
'{0!r}')
221+
D409 = D4xx.create_error('D409', 'Section underline should match the length '
222+
'of its name',
223+
'Expected {0!r} dashes in section {1!r}, got {2!r}')
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}')
209226

210227

211228
class AttrDict(dict):
@@ -215,5 +232,8 @@ def __getattr__(self, item):
215232
all_errors = set(ErrorRegistry.get_error_codes())
216233

217234
conventions = AttrDict({
218-
'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'},
238+
'numpy': all_errors - {'D203', 'D212', 'D213', 'D402'}
219239
})

0 commit comments

Comments
 (0)