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

Allow per-function skips #203

Merged
merged 5 commits into from
Sep 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ New Features
* Added D404 - First word of the docstring should not be "This". It is turned
off by default (#183).

* Added the ability to ignore specific function and method docstrings with
inline comments:

1. "# noqa" skips all checks.

2. "# noqa: D102,D203" can be used to skip specific checks.

Bug Fixes

* Fixed an issue where file paths were printed in lower case (#179, #181).
Expand Down
15 changes: 15 additions & 0 deletions docs/snippets/in_file.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
``pydocstyle`` supports inline commenting to skip specific checks on
specific functions or methods. The supported comments that can be added are:

1. ``"# noqa"`` skips all checks.

2. ``"# noqa: D102,D203"`` can be used to skip specific checks. Note that
this is compatible with skips from `flake8 <http://flake8.pycqa.org/>`_,
e.g. ``# noqa: D102,E501,D203``.

For example, this will skip the check for a period at the end of a function
docstring::

>>> def bad_function(): # noqa: D400
... """Omit a period in the docstring as an exception"""
... pass
6 changes: 6 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ Configuration Files
^^^^^^^^^^^^^^^^^^^

.. include:: snippets/config.rst


In-file configuration
^^^^^^^^^^^^^^^^^^^^^

.. include:: snippets/in_file.rst
116 changes: 72 additions & 44 deletions src/pydocstyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ def leading_space(string):
class Value(object):

def __init__(self, *args):
if len(self._fields) != len(args):
raise ValueError('got {0} arguments for {1} fields for {2}: {3}'
.format(len(args), len(self._fields),
self.__class__.__name__, self._fields))
vars(self).update(zip(self._fields, args))

def __hash__(self):
Expand All @@ -112,7 +116,7 @@ def __repr__(self):
class Definition(Value):

_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
'children', 'parent')
'children', 'parent', 'skipped_error_codes')

_human = property(lambda self: humanize(type(self).__name__))
kind = property(lambda self: self._human.split()[-1])
Expand Down Expand Up @@ -140,13 +144,18 @@ def is_empty_or_comment(line):
return ''.join(reversed(list(filtered_src)))

def __str__(self):
return 'in %s %s `%s`' % (self._publicity, self._human, self.name)
out = 'in {0} {1} `{2}`'.format(self._publicity, self._human,
self.name)
if self.skipped_error_codes:
out += ' (skipping {0})'.format(self.skipped_error_codes)
return out


class Module(Definition):

_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
'children', 'parent', '_all', 'future_imports')
'children', 'parent', '_all', 'future_imports',
'skipped_error_codes')
is_public = True
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
module = property(lambda self: self)
Expand Down Expand Up @@ -387,17 +396,17 @@ def parse_all(self):
if self.current.value not in '([':
raise AllError('Could not evaluate contents of __all__. ')
if self.current.value == '[':
msg = ("%s WARNING: __all__ is defined as a list, this means "
"pydocstyle cannot reliably detect contents of the __all__ "
"variable, because it can be mutated. Change __all__ to be "
"an (immutable) tuple, to remove this warning. Note, "
"pydocstyle uses __all__ to detect which definitions are "
"public, to warn if public definitions are missing "
"docstrings. If __all__ is a (mutable) list, pydocstyle "
"cannot reliably assume its contents. pydocstyle will "
"proceed assuming __all__ is not mutated.\n"
% self.filename)
sys.stderr.write(msg)
sys.stderr.write(
"{0} WARNING: __all__ is defined as a list, this means "
"pydocstyle cannot reliably detect contents of the __all__ "
"variable, because it can be mutated. Change __all__ to be "
"an (immutable) tuple, to remove this warning. Note, "
"pydocstyle uses __all__ to detect which definitions are "
"public, to warn if public definitions are missing "
"docstrings. If __all__ is a (mutable) list, pydocstyle "
"cannot reliably assume its contents. pydocstyle will "
"proceed assuming __all__ is not mutated.\n"
.format(self.filename))
self.consume(tk.OP)

self.all = []
Expand All @@ -409,17 +418,17 @@ def parse_all(self):
self.current.value == ','):
all_content += self.current.value
else:
raise AllError('Unexpected token kind in __all__: %r. ' %
self.current.kind)
raise AllError('Unexpected token kind in __all__: {0!r}. '
.format(self.current.kind))
self.stream.move()
self.consume(tk.OP)
all_content += ")"
try:
self.all = eval(all_content, {})
except BaseException as e:
raise AllError('Could not evaluate contents of __all__.'
'\bThe value was %s. The exception was:\n%s'
% (all_content, e))
'\bThe value was {0}. The exception was:\n{1}'
.format(all_content, e))

def parse_module(self):
"""Parse a module (and its children) and return a Module object."""
Expand All @@ -433,7 +442,7 @@ def parse_module(self):
if self.filename.endswith('__init__.py'):
cls = Package
module = cls(self.filename, self.source, start, end,
[], docstring, children, None, self.all)
[], docstring, children, None, self.all, None, '')
for child in module.children:
child.parent = module
module.future_imports = self.future_imports
Expand Down Expand Up @@ -463,6 +472,7 @@ def parse_definition(self, class_):
else:
self.consume(tk.OP)
if self.current.kind in (tk.NEWLINE, tk.COMMENT):
skipped_error_codes = self.parse_skip_comment()
self.leapfrog(tk.INDENT)
assert self.current.kind != tk.INDENT
docstring = self.parse_docstring()
Expand All @@ -473,20 +483,33 @@ def parse_definition(self, class_):
log.debug("finished parsing nested definitions for '%s'", name)
end = self.line - 1
else: # one-liner definition
skipped_error_codes = ''
docstring = self.parse_docstring()
decorators = [] # TODO
children = []
end = self.line
self.leapfrog(tk.NEWLINE)
definition = class_(name, self.source, start, end,
decorators, docstring, children, None)
decorators, docstring, children, None,
skipped_error_codes)
for child in definition.children:
child.parent = definition
log.debug("finished parsing %s '%s'. Next token is %r (%s)",
class_.__name__, name, self.current.kind,
self.current.value)
return definition

def parse_skip_comment(self):
"""Parse a definition comment for noqa skips."""
skipped_error_codes = ''
if self.current.kind == tk.COMMENT:
if 'noqa: ' in self.current.value:
skipped_error_codes = ''.join(
self.current.value.split('noqa: ')[1:])
elif self.current.value.startswith('# noqa'):
skipped_error_codes = 'all'
return skipped_error_codes

def check_current(self, kind=None, value=None):
msg = textwrap.dedent("""
Unexpected token at line {self.line}:
Expand Down Expand Up @@ -583,9 +606,9 @@ def set_context(self, definition, explanation):

@property
def message(self):
ret = '%s: %s' % (self.code, self.short_desc)
ret = '{0}: {1}'.format(self.code, self.short_desc)
if self.context is not None:
ret += ' (' + self.context % self.parameters + ')'
ret += ' (' + self.context.format(*self.parameters) + ')'
return ret

@property
Expand All @@ -601,7 +624,8 @@ def lines(self):
numbers_width = len(str(numbers_width))
numbers_width = 6
for n, line in enumerate(lines_stripped):
source += '%*d: %s' % (numbers_width, n + offset, line)
source += '{{0}}{0}: {{1}}'.format(numbers_width).format(
n + offset, line)
if n > 5:
source += ' ...\n'
break
Expand All @@ -610,16 +634,16 @@ def lines(self):
def __str__(self):
self.explanation = '\n'.join(l for l in self.explanation.split('\n')
if not is_blank(l))
template = '%(filename)s:%(line)s %(definition)s:\n %(message)s'
template = '{filename}:{line} {definition}:\n {message}'
if self.source and self.explain:
template += '\n\n%(explanation)s\n\n%(lines)s\n'
template += '\n\n{explanation}\n\n{lines}\n'
elif self.source and not self.explain:
template += '\n\n%(lines)s\n'
template += '\n\n{lines}\n'
elif self.explain and not self.source:
template += '\n\n%(explanation)s\n\n'
return template % dict((name, getattr(self, name)) for name in
template += '\n\n{explanation}\n\n'
return template.format(**dict((name, getattr(self, name)) for name in
['filename', 'line', 'definition', 'message',
'explanation', 'lines'])
'explanation', 'lines']))

__repr__ = __str__

Expand Down Expand Up @@ -668,7 +692,7 @@ def to_rst(cls):
for group in cls.groups:
table += sep_line
table += blank_line
table += '|' + ('**%s**' % group.name).center(78) + '|\n'
table += '|' + '**{0}**'.format(group.name).center(78) + '|\n'
table += blank_line
for error in group.errors:
table += sep_line
Expand All @@ -688,17 +712,17 @@ def to_rst(cls):

D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues')
D200 = D2xx.create_error('D200', 'One-line docstring should fit on one line '
'with quotes', 'found %s')
'with quotes', 'found {0}')
D201 = D2xx.create_error('D201', 'No blank lines allowed before function '
'docstring', 'found %s')
'docstring', 'found {0}')
D202 = D2xx.create_error('D202', 'No blank lines allowed after function '
'docstring', 'found %s')
'docstring', 'found {0}')
D203 = D2xx.create_error('D203', '1 blank line required before class '
'docstring', 'found %s')
'docstring', 'found {0}')
D204 = D2xx.create_error('D204', '1 blank line required after class '
'docstring', 'found %s')
'docstring', 'found {0}')
D205 = D2xx.create_error('D205', '1 blank line required between summary line '
'and description', 'found %s')
'and description', 'found {0}')
D206 = D2xx.create_error('D206', 'Docstring should be indented with spaces, '
'not tabs')
D207 = D2xx.create_error('D207', 'Docstring is under-indented')
Expand All @@ -708,27 +732,27 @@ def to_rst(cls):
D210 = D2xx.create_error('D210', 'No whitespaces allowed surrounding '
'docstring text')
D211 = D2xx.create_error('D211', 'No blank lines allowed before class '
'docstring', 'found %s')
'docstring', 'found {0}')
D212 = D2xx.create_error('D212', 'Multi-line docstring summary should start '
'at the first line')
D213 = D2xx.create_error('D213', 'Multi-line docstring summary should start '
'at the second line')

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

D4xx = ErrorRegistry.create_group('D4', 'Docstring Content Issues')
D400 = D4xx.create_error('D400', 'First line should end with a period',
'not %r')
'not {0!r}')
D401 = D4xx.create_error('D401', 'First line should be in imperative mood',
'%r, not %r')
'{0!r}, not {1!r}')
D402 = D4xx.create_error('D402', 'First line should not be the function\'s '
'"signature"')
D403 = D4xx.create_error('D403', 'First word of the first line should be '
'properly capitalized', '%r, not %r')
'properly capitalized', '{0!r}, not {1!r}')
D404 = D4xx.create_error('D404', 'First word of the docstring should not '
'be `This`')

Expand Down Expand Up @@ -1365,7 +1389,7 @@ def run_pydocstyle(use_pep257=False):
code = ReturnCode.no_violations_found
count = 0
for error in errors:
sys.stderr.write('%s\n' % error)
sys.stderr.write('{0}\n'.format(error))
code = ReturnCode.violations_found
count += 1
if run_conf.count:
Expand Down Expand Up @@ -1400,10 +1424,14 @@ def check_source(self, source, filename):
for check in self.checks:
terminate = False
if isinstance(definition, check._check_for):
error = check(None, definition, definition.docstring)
if definition.skipped_error_codes != 'all':
error = check(None, definition, definition.docstring)
else:
error = None
errors = error if hasattr(error, '__iter__') else [error]
for error in errors:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check if error should be skipped here, instead of in each individual check function (it will save a lot of boilerplate code below).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes that's much cleaner

if error is not None:
if error is not None and error.code not in \
definition.skipped_error_codes:
partition = check.__doc__.partition('.\n')
message, _, explanation = partition
error.set_context(explanation=explanation,
Expand Down
23 changes: 23 additions & 0 deletions src/tests/test_cases/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,28 @@ def inner_function():
"""Do inner something."""
return 0


@expect("D400: First line should end with a period (not 'g')")
@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
def docstring_bad():
"""Runs something"""
pass


def docstring_bad_ignore_all(): # noqa
"""Runs something"""
pass


def docstring_bad_ignore_one(): # noqa: D400,D401
"""Runs something"""
pass


@expect("D401: First line should be in imperative mood ('Run', not 'Runs')")
def docstring_ignore_violations_of_pydocstyle_D400_and_PEP8_E501_but_catch_D401(): # noqa: E501,D400
"""Runs something"""
pass

expect(__file__ if __file__[-1] != 'c' else __file__[:-1],
'D100: Missing docstring in public module')
7 changes: 4 additions & 3 deletions src/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,14 @@ def %s(self):
""" % (name))

module = pydocstyle.Module('module_name', source, 0, 1, [],
'Docstring for module', [], None, all)
'Docstring for module', [], None,
all, None, '')

cls = pydocstyle.Class('ClassName', source, 0, 1, [],
'Docstring for class', children, module, all)
'Docstring for class', children, module, '')

return pydocstyle.Method(name, source, 0, 1, [],
'Docstring for method', children, cls, all)
'Docstring for method', children, cls, '')

def test_is_public_normal(self):
"""Methods are normally public, even if decorated."""
Expand Down
Loading