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

Commit 7d3f471

Browse files
committed
Allow per-function skips
Comments like "#noqa" can be added to the end of function and method definitions to skip checks.
1 parent 0e32548 commit 7d3f471

File tree

7 files changed

+133
-49
lines changed

7 files changed

+133
-49
lines changed

docs/release_notes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ New Features
1616
* Added D404 - First word of the docstring should not be "This". It is turned
1717
off by default (#183).
1818

19+
* Added the ability to ignore specific function and method doctstrings with
20+
comments:
21+
22+
1. "# noqa" or "# pydocstyle: noqa" skips all checks.
23+
24+
2. "# pydocstyle: D102,D203" can be used to skip specific checks.
25+
1926
Bug Fixes
2027

2128
* Fixed an issue where file paths were printed in lower case (#179, #181).

docs/snippets/per_file.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
``pydocstyle`` inline commenting to skip specific checks on specific
2+
functions or methods. The supported comments that can be added are:
3+
4+
1. ``"# noqa"`` or ``"# pydocstyle: noqa"`` skips all checks.
5+
6+
2. ``"# pydocstyle: D102,D203"`` can be used to skip specific checks.
7+
8+
For example, this will skip the check for a period at the end of a function
9+
docstring::
10+
11+
>>> def bad_function(): # pydocstyle: D400
12+
... """Omit a period in the docstring as an exception"""
13+
... pass

docs/usage.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ Configuration Files
1717
^^^^^^^^^^^^^^^^^^^
1818

1919
.. include:: snippets/config.rst
20+
21+
22+
Per-file configuration
23+
^^^^^^^^^^^^^^^^^^^^^^
24+
25+
.. include:: snippets/per_file.rst

src/pydocstyle.py

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ def leading_space(string):
9595
class Value(object):
9696

9797
def __init__(self, *args):
98+
if len(self._fields) != len(args):
99+
raise ValueError('got %s arguments for %s fields for %s: %s'
100+
% (len(args), len(self._fields),
101+
self.__class__.__name__, self._fields))
98102
vars(self).update(zip(self._fields, args))
99103

100104
def __hash__(self):
@@ -112,7 +116,7 @@ def __repr__(self):
112116
class Definition(Value):
113117

114118
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
115-
'children', 'parent')
119+
'children', 'parent', 'skips')
116120

117121
_human = property(lambda self: humanize(type(self).__name__))
118122
kind = property(lambda self: self._human.split()[-1])
@@ -140,13 +144,16 @@ def is_empty_or_comment(line):
140144
return ''.join(reversed(list(filtered_src)))
141145

142146
def __str__(self):
143-
return 'in %s %s `%s`' % (self._publicity, self._human, self.name)
147+
out = 'in %s %s `%s`' % (self._publicity, self._human, self.name)
148+
if self.skips:
149+
out += ' (skipping %s)' % self.skips
150+
return out
144151

145152

146153
class Module(Definition):
147154

148155
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
149-
'children', 'parent', '_all', 'future_imports')
156+
'children', 'parent', '_all', 'future_imports', 'skips')
150157
is_public = True
151158
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
152159
module = property(lambda self: self)
@@ -433,7 +440,7 @@ def parse_module(self):
433440
if self.filename.endswith('__init__.py'):
434441
cls = Package
435442
module = cls(self.filename, self.source, start, end,
436-
[], docstring, children, None, self.all)
443+
[], docstring, children, None, self.all, None, '')
437444
for child in module.children:
438445
child.parent = module
439446
module.future_imports = self.future_imports
@@ -462,7 +469,15 @@ def parse_definition(self, class_):
462469
self.leapfrog(tk.OP, value=":")
463470
else:
464471
self.consume(tk.OP)
472+
skips = ''
465473
if self.current.kind in (tk.NEWLINE, tk.COMMENT):
474+
if self.current.kind == tk.COMMENT:
475+
if self.current.value.startswith('# noqa') or \
476+
'pydocstyle: noqa' in self.current.value:
477+
skips = 'all'
478+
elif 'pydocstyle: ' in self.current.value:
479+
skips = ''.join(self.current.value.split(
480+
'pydocstyle: ')[1:])
466481
self.leapfrog(tk.INDENT)
467482
assert self.current.kind != tk.INDENT
468483
docstring = self.parse_docstring()
@@ -479,7 +494,7 @@ def parse_definition(self, class_):
479494
end = self.line
480495
self.leapfrog(tk.NEWLINE)
481496
definition = class_(name, self.source, start, end,
482-
decorators, docstring, children, None)
497+
decorators, docstring, children, None, skips)
483498
for child in definition.children:
484499
child.parent = definition
485500
log.debug("finished parsing %s '%s'. Next token is %r (%s)",
@@ -1400,7 +1415,10 @@ def check_source(self, source, filename):
14001415
for check in self.checks:
14011416
terminate = False
14021417
if isinstance(definition, check._check_for):
1403-
error = check(None, definition, definition.docstring)
1418+
if definition.skips != 'all':
1419+
error = check(None, definition, definition.docstring)
1420+
else:
1421+
error = None
14041422
errors = error if hasattr(error, '__iter__') else [error]
14051423
for error in errors:
14061424
if error is not None:
@@ -1441,7 +1459,10 @@ def check_docstring_missing(self, definition, docstring):
14411459
Method: (lambda: D105() if is_magic(definition.name)
14421460
else D102()),
14431461
Function: D103, NestedFunction: D103, Package: D104}
1444-
return codes[type(definition)]()
1462+
code = codes[type(definition)]
1463+
if code.__name__ in definition.skips:
1464+
return
1465+
return code()
14451466

14461467
@check_for(Definition)
14471468
def check_one_liners(self, definition, docstring):
@@ -1451,6 +1472,8 @@ def check_one_liners(self, definition, docstring):
14511472
This looks better for one-liners.
14521473
14531474
"""
1475+
if 'D200' in definition.skips:
1476+
return
14541477
if docstring:
14551478
lines = ast.literal_eval(docstring).split('\n')
14561479
if len(lines) > 1:
@@ -1472,9 +1495,11 @@ def check_no_blank_before(self, function, docstring): # def
14721495
blanks_before_count = sum(takewhile(bool, reversed(blanks_before)))
14731496
blanks_after_count = sum(takewhile(bool, blanks_after))
14741497
if blanks_before_count != 0:
1475-
yield D201(blanks_before_count)
1498+
if 'D201' not in function.skips:
1499+
yield D201(blanks_before_count)
14761500
if not all(blanks_after) and blanks_after_count != 0:
1477-
yield D202(blanks_after_count)
1501+
if 'D202' not in function.skips:
1502+
yield D202(blanks_after_count)
14781503

14791504
@check_for(Class)
14801505
def check_blank_before_after_class(self, class_, docstring):
@@ -1502,11 +1527,12 @@ def check_blank_before_after_class(self, class_, docstring):
15021527
blanks_after = list(map(is_blank, after.split('\n')[1:]))
15031528
blanks_before_count = sum(takewhile(bool, reversed(blanks_before)))
15041529
blanks_after_count = sum(takewhile(bool, blanks_after))
1505-
if blanks_before_count != 0:
1530+
if 'D211' not in class_.skips and blanks_before_count != 0:
15061531
yield D211(blanks_before_count)
1507-
if blanks_before_count != 1:
1532+
if 'D203' not in class_.skips and blanks_before_count != 1:
15081533
yield D203(blanks_before_count)
1509-
if not all(blanks_after) and blanks_after_count != 1:
1534+
if 'D204' not in class_.skips and (not all(blanks_after) and
1535+
blanks_after_count != 1):
15101536
yield D204(blanks_after_count)
15111537

15121538
@check_for(Definition)
@@ -1521,6 +1547,8 @@ def check_blank_after_summary(self, definition, docstring):
15211547
15221548
"""
15231549
if docstring:
1550+
if 'D205' in definition.skips:
1551+
return
15241552
lines = ast.literal_eval(docstring).strip().split('\n')
15251553
if len(lines) > 1:
15261554
post_summary_blanks = list(map(is_blank, lines[1:]))
@@ -1543,13 +1571,16 @@ def check_indent(self, definition, docstring):
15431571
if len(lines) > 1:
15441572
lines = lines[1:] # First line does not need indent.
15451573
indents = [leading_space(l) for l in lines if not is_blank(l)]
1546-
if set(' \t') == set(''.join(indents) + indent):
1547-
yield D206()
1548-
if (len(indents) > 1 and min(indents[:-1]) > indent or
1549-
indents[-1] > indent):
1550-
yield D208()
1551-
if min(indents) < indent:
1552-
yield D207()
1574+
if 'D206' not in definition.skips:
1575+
if set(' \t') == set(''.join(indents) + indent):
1576+
yield D206()
1577+
if 'D208' not in definition.skips:
1578+
if (len(indents) > 1 and min(indents[:-1]) > indent or
1579+
indents[-1] > indent):
1580+
yield D208()
1581+
if 'D207' not in definition.skips:
1582+
if min(indents) < indent:
1583+
yield D207()
15531584

15541585
@check_for(Definition)
15551586
def check_newline_after_last_paragraph(self, definition, docstring):
@@ -1559,7 +1590,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
15591590
quotes on a line by themselves.
15601591
15611592
"""
1562-
if docstring:
1593+
if docstring and 'D209' not in definition.skips:
15631594
lines = [l for l in ast.literal_eval(docstring).split('\n')
15641595
if not is_blank(l)]
15651596
if len(lines) > 1:
@@ -1569,7 +1600,7 @@ def check_newline_after_last_paragraph(self, definition, docstring):
15691600
@check_for(Definition)
15701601
def check_surrounding_whitespaces(self, definition, docstring):
15711602
"""D210: No whitespaces allowed surrounding docstring text."""
1572-
if docstring:
1603+
if docstring and 'D210' not in definition.skips:
15731604
lines = ast.literal_eval(docstring).split('\n')
15741605
if lines[0].startswith(' ') or \
15751606
len(lines) == 1 and lines[0].endswith(' '):
@@ -1595,9 +1626,11 @@ def check_multi_line_summary_start(self, definition, docstring):
15951626
if len(lines) > 1:
15961627
first = docstring.split("\n")[0].strip().lower()
15971628
if first in start_triple:
1598-
return D212()
1629+
if 'D212' not in definition.skips:
1630+
return D212()
15991631
else:
1600-
return D213()
1632+
if 'D213' not in definition.skips:
1633+
return D213()
16011634

16021635
@check_for(Definition)
16031636
def check_triple_double_quotes(self, definition, docstring):
@@ -1612,7 +1645,7 @@ def check_triple_double_quotes(self, definition, docstring):
16121645
""" quotes in its body.
16131646
16141647
'''
1615-
if docstring:
1648+
if docstring and 'D300' not in definition.skips:
16161649
opening = docstring[:5].lower()
16171650
if '"""' in ast.literal_eval(docstring) and opening.startswith(
16181651
("'''", "r'''", "u'''", "ur'''")):
@@ -1634,8 +1667,8 @@ def check_backslashes(self, definition, docstring):
16341667
'''
16351668
# Just check that docstring is raw, check_triple_double_quotes
16361669
# ensures the correct quotes.
1637-
if docstring and '\\' in docstring and not docstring.startswith(
1638-
('r', 'ur')):
1670+
if docstring and 'D301' not in definition.skips and \
1671+
'\\' in docstring and not docstring.startswith(('r', 'ur')):
16391672
return D301()
16401673

16411674
@check_for(Definition)
@@ -1650,7 +1683,8 @@ def check_unicode_docstring(self, definition, docstring):
16501683

16511684
# Just check that docstring is unicode, check_triple_double_quotes
16521685
# ensures the correct quotes.
1653-
if docstring and sys.version_info[0] <= 2:
1686+
if docstring and sys.version_info[0] <= 2 and \
1687+
'D302' not in definition.skips:
16541688
if not is_ascii(docstring) and not docstring.startswith(
16551689
('u', 'ur')):
16561690
return D302()
@@ -1662,7 +1696,7 @@ def check_ends_with_period(self, definition, docstring):
16621696
The [first line of a] docstring is a phrase ending in a period.
16631697
16641698
"""
1665-
if docstring:
1699+
if docstring and 'D400' not in definition.skips:
16661700
summary_line = ast.literal_eval(docstring).strip().split('\n')[0]
16671701
if not summary_line.endswith('.'):
16681702
return D400(summary_line[-1])
@@ -1676,7 +1710,7 @@ def check_imperative_mood(self, function, docstring): # def context
16761710
"Returns the pathname ...".
16771711
16781712
"""
1679-
if docstring:
1713+
if docstring and 'D401' not in function.skips:
16801714
stripped = ast.literal_eval(docstring).strip()
16811715
if stripped:
16821716
first_word = stripped.split()[0]
@@ -1691,7 +1725,7 @@ def check_no_signature(self, function, docstring): # def context
16911725
function/method parameters (which can be obtained by introspection).
16921726
16931727
"""
1694-
if docstring:
1728+
if docstring and 'D402' not in function.skips:
16951729
first_line = ast.literal_eval(docstring).strip().split('\n')[0]
16961730
if function.name + '(' in first_line.replace(' ', ''):
16971731
return D402()
@@ -1703,7 +1737,7 @@ def check_capitalized(self, function, docstring):
17031737
The [first line of a] docstring is a phrase ending in a period.
17041738
17051739
"""
1706-
if docstring:
1740+
if docstring and 'D403' not in function.skips:
17071741
first_word = ast.literal_eval(docstring).split()[0]
17081742
if first_word == first_word.upper():
17091743
return
@@ -1721,7 +1755,7 @@ def check_starts_with_this(self, function, docstring):
17211755
with "This class is [..]" or "This module contains [..]".
17221756
17231757
"""
1724-
if docstring:
1758+
if docstring and 'D404' not in function.skips:
17251759
first_word = ast.literal_eval(docstring).split()[0]
17261760
if first_word.lower() == 'this':
17271761
return D404()

src/tests/test_cases/test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,5 +339,28 @@ def inner_function():
339339
"""Do inner something."""
340340
return 0
341341

342+
343+
@expect("D400: First line should end with a period (not 'g')")
344+
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
345+
def docstring_bad():
346+
"""Runs something"""
347+
pass
348+
349+
350+
def docstring_bad_ignore_all(): # noqa
351+
"""Runs something"""
352+
pass
353+
354+
355+
def docstring_bad_ignore_all_2(): # pydocstyle: noqa
356+
"""Runs something"""
357+
pass
358+
359+
360+
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
361+
def docstring_bad_ignore_one(): # pydocstyle: D400
362+
"""Runs something"""
363+
pass
364+
342365
expect(__file__ if __file__[-1] != 'c' else __file__[:-1],
343366
'D100: Missing docstring in public module')

src/tests/test_decorators.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,14 @@ def %s(self):
164164
""" % (name))
165165

166166
module = pydocstyle.Module('module_name', source, 0, 1, [],
167-
'Docstring for module', [], None, all)
167+
'Docstring for module', [], None,
168+
all, None, '')
168169

169170
cls = pydocstyle.Class('ClassName', source, 0, 1, [],
170-
'Docstring for class', children, module, all)
171+
'Docstring for class', children, module, '')
171172

172173
return pydocstyle.Method(name, source, 0, 1, [],
173-
'Docstring for method', children, cls, all)
174+
'Docstring for method', children, cls, '')
174175

175176
def test_is_public_normal(self):
176177
"""Methods are normally public, even if decorated."""

0 commit comments

Comments
 (0)