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

Commit 2ce9e41

Browse files
authored
Merge pull request #203 from Eric89GXL/skips
Allow per-function skips
2 parents 0e32548 + dbaec42 commit 2ce9e41

File tree

7 files changed

+141
-61
lines changed

7 files changed

+141
-61
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 docstrings with
20+
inline comments:
21+
22+
1. "# noqa" skips all checks.
23+
24+
2. "# noqa: 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/in_file.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
``pydocstyle`` supports inline commenting to skip specific checks on
2+
specific functions or methods. The supported comments that can be added are:
3+
4+
1. ``"# noqa"`` skips all checks.
5+
6+
2. ``"# noqa: D102,D203"`` can be used to skip specific checks. Note that
7+
this is compatible with skips from `flake8 <http://flake8.pycqa.org/>`_,
8+
e.g. ``# noqa: D102,E501,D203``.
9+
10+
For example, this will skip the check for a period at the end of a function
11+
docstring::
12+
13+
>>> def bad_function(): # noqa: D400
14+
... """Omit a period in the docstring as an exception"""
15+
... 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+
In-file configuration
23+
^^^^^^^^^^^^^^^^^^^^^
24+
25+
.. include:: snippets/in_file.rst

src/pydocstyle.py

Lines changed: 72 additions & 44 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 {0} arguments for {1} fields for {2}: {3}'
100+
.format(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', 'skipped_error_codes')
116120

117121
_human = property(lambda self: humanize(type(self).__name__))
118122
kind = property(lambda self: self._human.split()[-1])
@@ -140,13 +144,18 @@ 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 {0} {1} `{2}`'.format(self._publicity, self._human,
148+
self.name)
149+
if self.skipped_error_codes:
150+
out += ' (skipping {0})'.format(self.skipped_error_codes)
151+
return out
144152

145153

146154
class Module(Definition):
147155

148156
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
149-
'children', 'parent', '_all', 'future_imports')
157+
'children', 'parent', '_all', 'future_imports',
158+
'skipped_error_codes')
150159
is_public = True
151160
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
152161
module = property(lambda self: self)
@@ -387,17 +396,17 @@ def parse_all(self):
387396
if self.current.value not in '([':
388397
raise AllError('Could not evaluate contents of __all__. ')
389398
if self.current.value == '[':
390-
msg = ("%s WARNING: __all__ is defined as a list, this means "
391-
"pydocstyle cannot reliably detect contents of the __all__ "
392-
"variable, because it can be mutated. Change __all__ to be "
393-
"an (immutable) tuple, to remove this warning. Note, "
394-
"pydocstyle uses __all__ to detect which definitions are "
395-
"public, to warn if public definitions are missing "
396-
"docstrings. If __all__ is a (mutable) list, pydocstyle "
397-
"cannot reliably assume its contents. pydocstyle will "
398-
"proceed assuming __all__ is not mutated.\n"
399-
% self.filename)
400-
sys.stderr.write(msg)
399+
sys.stderr.write(
400+
"{0} WARNING: __all__ is defined as a list, this means "
401+
"pydocstyle cannot reliably detect contents of the __all__ "
402+
"variable, because it can be mutated. Change __all__ to be "
403+
"an (immutable) tuple, to remove this warning. Note, "
404+
"pydocstyle uses __all__ to detect which definitions are "
405+
"public, to warn if public definitions are missing "
406+
"docstrings. If __all__ is a (mutable) list, pydocstyle "
407+
"cannot reliably assume its contents. pydocstyle will "
408+
"proceed assuming __all__ is not mutated.\n"
409+
.format(self.filename))
401410
self.consume(tk.OP)
402411

403412
self.all = []
@@ -409,17 +418,17 @@ def parse_all(self):
409418
self.current.value == ','):
410419
all_content += self.current.value
411420
else:
412-
raise AllError('Unexpected token kind in __all__: %r. ' %
413-
self.current.kind)
421+
raise AllError('Unexpected token kind in __all__: {0!r}. '
422+
.format(self.current.kind))
414423
self.stream.move()
415424
self.consume(tk.OP)
416425
all_content += ")"
417426
try:
418427
self.all = eval(all_content, {})
419428
except BaseException as e:
420429
raise AllError('Could not evaluate contents of __all__.'
421-
'\bThe value was %s. The exception was:\n%s'
422-
% (all_content, e))
430+
'\bThe value was {0}. The exception was:\n{1}'
431+
.format(all_content, e))
423432

424433
def parse_module(self):
425434
"""Parse a module (and its children) and return a Module object."""
@@ -433,7 +442,7 @@ def parse_module(self):
433442
if self.filename.endswith('__init__.py'):
434443
cls = Package
435444
module = cls(self.filename, self.source, start, end,
436-
[], docstring, children, None, self.all)
445+
[], docstring, children, None, self.all, None, '')
437446
for child in module.children:
438447
child.parent = module
439448
module.future_imports = self.future_imports
@@ -463,6 +472,7 @@ def parse_definition(self, class_):
463472
else:
464473
self.consume(tk.OP)
465474
if self.current.kind in (tk.NEWLINE, tk.COMMENT):
475+
skipped_error_codes = self.parse_skip_comment()
466476
self.leapfrog(tk.INDENT)
467477
assert self.current.kind != tk.INDENT
468478
docstring = self.parse_docstring()
@@ -473,20 +483,33 @@ def parse_definition(self, class_):
473483
log.debug("finished parsing nested definitions for '%s'", name)
474484
end = self.line - 1
475485
else: # one-liner definition
486+
skipped_error_codes = ''
476487
docstring = self.parse_docstring()
477488
decorators = [] # TODO
478489
children = []
479490
end = self.line
480491
self.leapfrog(tk.NEWLINE)
481492
definition = class_(name, self.source, start, end,
482-
decorators, docstring, children, None)
493+
decorators, docstring, children, None,
494+
skipped_error_codes)
483495
for child in definition.children:
484496
child.parent = definition
485497
log.debug("finished parsing %s '%s'. Next token is %r (%s)",
486498
class_.__name__, name, self.current.kind,
487499
self.current.value)
488500
return definition
489501

502+
def parse_skip_comment(self):
503+
"""Parse a definition comment for noqa skips."""
504+
skipped_error_codes = ''
505+
if self.current.kind == tk.COMMENT:
506+
if 'noqa: ' in self.current.value:
507+
skipped_error_codes = ''.join(
508+
self.current.value.split('noqa: ')[1:])
509+
elif self.current.value.startswith('# noqa'):
510+
skipped_error_codes = 'all'
511+
return skipped_error_codes
512+
490513
def check_current(self, kind=None, value=None):
491514
msg = textwrap.dedent("""
492515
Unexpected token at line {self.line}:
@@ -583,9 +606,9 @@ def set_context(self, definition, explanation):
583606

584607
@property
585608
def message(self):
586-
ret = '%s: %s' % (self.code, self.short_desc)
609+
ret = '{0}: {1}'.format(self.code, self.short_desc)
587610
if self.context is not None:
588-
ret += ' (' + self.context % self.parameters + ')'
611+
ret += ' (' + self.context.format(*self.parameters) + ')'
589612
return ret
590613

591614
@property
@@ -601,7 +624,8 @@ def lines(self):
601624
numbers_width = len(str(numbers_width))
602625
numbers_width = 6
603626
for n, line in enumerate(lines_stripped):
604-
source += '%*d: %s' % (numbers_width, n + offset, line)
627+
source += '{{0}}{0}: {{1}}'.format(numbers_width).format(
628+
n + offset, line)
605629
if n > 5:
606630
source += ' ...\n'
607631
break
@@ -610,16 +634,16 @@ def lines(self):
610634
def __str__(self):
611635
self.explanation = '\n'.join(l for l in self.explanation.split('\n')
612636
if not is_blank(l))
613-
template = '%(filename)s:%(line)s %(definition)s:\n %(message)s'
637+
template = '{filename}:{line} {definition}:\n {message}'
614638
if self.source and self.explain:
615-
template += '\n\n%(explanation)s\n\n%(lines)s\n'
639+
template += '\n\n{explanation}\n\n{lines}\n'
616640
elif self.source and not self.explain:
617-
template += '\n\n%(lines)s\n'
641+
template += '\n\n{lines}\n'
618642
elif self.explain and not self.source:
619-
template += '\n\n%(explanation)s\n\n'
620-
return template % dict((name, getattr(self, name)) for name in
643+
template += '\n\n{explanation}\n\n'
644+
return template.format(**dict((name, getattr(self, name)) for name in
621645
['filename', 'line', 'definition', 'message',
622-
'explanation', 'lines'])
646+
'explanation', 'lines']))
623647

624648
__repr__ = __str__
625649

@@ -668,7 +692,7 @@ def to_rst(cls):
668692
for group in cls.groups:
669693
table += sep_line
670694
table += blank_line
671-
table += '|' + ('**%s**' % group.name).center(78) + '|\n'
695+
table += '|' + '**{0}**'.format(group.name).center(78) + '|\n'
672696
table += blank_line
673697
for error in group.errors:
674698
table += sep_line
@@ -688,17 +712,17 @@ def to_rst(cls):
688712

689713
D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues')
690714
D200 = D2xx.create_error('D200', 'One-line docstring should fit on one line '
691-
'with quotes', 'found %s')
715+
'with quotes', 'found {0}')
692716
D201 = D2xx.create_error('D201', 'No blank lines allowed before function '
693-
'docstring', 'found %s')
717+
'docstring', 'found {0}')
694718
D202 = D2xx.create_error('D202', 'No blank lines allowed after function '
695-
'docstring', 'found %s')
719+
'docstring', 'found {0}')
696720
D203 = D2xx.create_error('D203', '1 blank line required before class '
697-
'docstring', 'found %s')
721+
'docstring', 'found {0}')
698722
D204 = D2xx.create_error('D204', '1 blank line required after class '
699-
'docstring', 'found %s')
723+
'docstring', 'found {0}')
700724
D205 = D2xx.create_error('D205', '1 blank line required between summary line '
701-
'and description', 'found %s')
725+
'and description', 'found {0}')
702726
D206 = D2xx.create_error('D206', 'Docstring should be indented with spaces, '
703727
'not tabs')
704728
D207 = D2xx.create_error('D207', 'Docstring is under-indented')
@@ -708,27 +732,27 @@ def to_rst(cls):
708732
D210 = D2xx.create_error('D210', 'No whitespaces allowed surrounding '
709733
'docstring text')
710734
D211 = D2xx.create_error('D211', 'No blank lines allowed before class '
711-
'docstring', 'found %s')
735+
'docstring', 'found {0}')
712736
D212 = D2xx.create_error('D212', 'Multi-line docstring summary should start '
713737
'at the first line')
714738
D213 = D2xx.create_error('D213', 'Multi-line docstring summary should start '
715739
'at the second line')
716740

717741
D3xx = ErrorRegistry.create_group('D3', 'Quotes Issues')
718742
D300 = D3xx.create_error('D300', 'Use """triple double quotes"""',
719-
'found %s-quotes')
743+
'found {0}-quotes')
720744
D301 = D3xx.create_error('D301', 'Use r""" if any backslashes in a docstring')
721745
D302 = D3xx.create_error('D302', 'Use u""" for Unicode docstrings')
722746

723747
D4xx = ErrorRegistry.create_group('D4', 'Docstring Content Issues')
724748
D400 = D4xx.create_error('D400', 'First line should end with a period',
725-
'not %r')
749+
'not {0!r}')
726750
D401 = D4xx.create_error('D401', 'First line should be in imperative mood',
727-
'%r, not %r')
751+
'{0!r}, not {1!r}')
728752
D402 = D4xx.create_error('D402', 'First line should not be the function\'s '
729753
'"signature"')
730754
D403 = D4xx.create_error('D403', 'First word of the first line should be '
731-
'properly capitalized', '%r, not %r')
755+
'properly capitalized', '{0!r}, not {1!r}')
732756
D404 = D4xx.create_error('D404', 'First word of the docstring should not '
733757
'be `This`')
734758

@@ -1365,7 +1389,7 @@ def run_pydocstyle(use_pep257=False):
13651389
code = ReturnCode.no_violations_found
13661390
count = 0
13671391
for error in errors:
1368-
sys.stderr.write('%s\n' % error)
1392+
sys.stderr.write('{0}\n'.format(error))
13691393
code = ReturnCode.violations_found
13701394
count += 1
13711395
if run_conf.count:
@@ -1400,10 +1424,14 @@ def check_source(self, source, filename):
14001424
for check in self.checks:
14011425
terminate = False
14021426
if isinstance(definition, check._check_for):
1403-
error = check(None, definition, definition.docstring)
1427+
if definition.skipped_error_codes != 'all':
1428+
error = check(None, definition, definition.docstring)
1429+
else:
1430+
error = None
14041431
errors = error if hasattr(error, '__iter__') else [error]
14051432
for error in errors:
1406-
if error is not None:
1433+
if error is not None and error.code not in \
1434+
definition.skipped_error_codes:
14071435
partition = check.__doc__.partition('.\n')
14081436
message, _, explanation = partition
14091437
error.set_context(explanation=explanation,

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_one(): # noqa: D400,D401
356+
"""Runs something"""
357+
pass
358+
359+
360+
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
361+
def docstring_ignore_violations_of_pydocstyle_D400_and_PEP8_E501_but_catch_D401(): # noqa: E501,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)