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

Commit dcff6aa

Browse files
committed
Merge branch 'master' into splitfiles
# Conflicts: # src/pydocstyle.py # src/pydocstyle/tests/test_decorators.py
2 parents cab6da3 + 2ce9e41 commit dcff6aa

File tree

13 files changed

+154
-66
lines changed

13 files changed

+154
-66
lines changed

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# If extensions (or modules to document with autodoc) are in another directory,
1919
# add these directories to sys.path here. If the directory is relative to the
2020
# documentation root, use os.path.abspath to make it absolute, like shown here.
21-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
21+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
2222

2323
# -- General configuration ------------------------------------------------
2424

docs/release_notes.rst

+9
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@ 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

28+
* Fixed an issue where file paths were printed in lower case (#179, #181).
29+
2130
* The error code D300 is now also being reported if a docstring has
2231
uppercase literals (``R`` or ``U``) as prefix (#176).
2332

docs/snippets/in_file.rst

+15
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

+6
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

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
-r requirements/docs.txt
22
-r requirements/tests.txt
3+
-r requirements/test_env.txt
34
-r requirements/runtime.txt

requirements/tests.txt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pytest==2.7.3
2-
pytest-pep8
3-
mock
1+
pytest==3.0.2
2+
pytest-pep8==1.0.6
3+
mock==2.0.0
44
pathlib

src/pydocstyle/checker.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ def check_source(self, source, filename):
4949
for check in self.checks:
5050
terminate = False
5151
if isinstance(definition, check._check_for):
52-
error = check(None, definition, definition.docstring)
52+
if definition.skipped_error_codes != 'all':
53+
error = check(None, definition, definition.docstring)
54+
else:
55+
error = None
5356
errors = error if hasattr(error, '__iter__') else [error]
5457
for error in errors:
55-
if error is not None:
58+
if error is not None and error.code not in \
59+
definition.skipped_error_codes:
5660
partition = check.__doc__.partition('.\n')
5761
message, _, explanation = partition
5862
error.set_context(explanation=explanation,

src/pydocstyle/parser.py

+43-20
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class Value(object):
4141
"""A generic object with a list of preset fields."""
4242

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

4650
def __hash__(self):
@@ -59,7 +63,7 @@ class Definition(Value):
5963
"""A Python source code definition (could be class, function, etc)."""
6064

6165
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
62-
'children', 'parent')
66+
'children', 'parent', 'skipped_error_codes')
6367

6468
_human = property(lambda self: humanize(type(self).__name__))
6569
kind = property(lambda self: self._human.split()[-1])
@@ -87,14 +91,19 @@ def is_empty_or_comment(line):
8791
return ''.join(reversed(list(filtered_src)))
8892

8993
def __str__(self):
90-
return 'in %s %s `%s`' % (self._publicity, self._human, self.name)
94+
out = 'in {0} {1} `{2}`'.format(self._publicity, self._human,
95+
self.name)
96+
if self.skipped_error_codes:
97+
out += ' (skipping {0})'.format(self.skipped_error_codes)
98+
return out
9199

92100

93101
class Module(Definition):
94102
"""A Python source code module."""
95103

96104
_fields = ('name', '_source', 'start', 'end', 'decorators', 'docstring',
97-
'children', 'parent', '_all', 'future_imports')
105+
'children', 'parent', '_all', 'future_imports',
106+
'skipped_error_codes')
98107
is_public = True
99108
_nest = staticmethod(lambda s: {'def': Function, 'class': Class}[s])
100109
module = property(lambda self: self)
@@ -364,17 +373,17 @@ def parse_all(self):
364373
if self.current.value not in '([':
365374
raise AllError('Could not evaluate contents of __all__. ')
366375
if self.current.value == '[':
367-
msg = ("%s WARNING: __all__ is defined as a list, this means "
368-
"pydocstyle cannot reliably detect contents of the __all__ "
369-
"variable, because it can be mutated. Change __all__ to be "
370-
"an (immutable) tuple, to remove this warning. Note, "
371-
"pydocstyle uses __all__ to detect which definitions are "
372-
"public, to warn if public definitions are missing "
373-
"docstrings. If __all__ is a (mutable) list, pydocstyle "
374-
"cannot reliably assume its contents. pydocstyle will "
375-
"proceed assuming __all__ is not mutated.\n"
376-
% self.filename)
377-
sys.stderr.write(msg)
376+
sys.stderr.write(
377+
"{0} WARNING: __all__ is defined as a list, this means "
378+
"pydocstyle cannot reliably detect contents of the __all__ "
379+
"variable, because it can be mutated. Change __all__ to be "
380+
"an (immutable) tuple, to remove this warning. Note, "
381+
"pydocstyle uses __all__ to detect which definitions are "
382+
"public, to warn if public definitions are missing "
383+
"docstrings. If __all__ is a (mutable) list, pydocstyle "
384+
"cannot reliably assume its contents. pydocstyle will "
385+
"proceed assuming __all__ is not mutated.\n"
386+
.format(self.filename))
378387
self.consume(tk.OP)
379388

380389
self.all = []
@@ -386,17 +395,17 @@ def parse_all(self):
386395
self.current.value == ','):
387396
all_content += self.current.value
388397
else:
389-
raise AllError('Unexpected token kind in __all__: %r. ' %
390-
self.current.kind)
398+
raise AllError('Unexpected token kind in __all__: {0!r}. '
399+
.format(self.current.kind))
391400
self.stream.move()
392401
self.consume(tk.OP)
393402
all_content += ")"
394403
try:
395404
self.all = eval(all_content, {})
396405
except BaseException as e:
397406
raise AllError('Could not evaluate contents of __all__.'
398-
'\bThe value was %s. The exception was:\n%s'
399-
% (all_content, e))
407+
'\bThe value was {0}. The exception was:\n{1}'
408+
.format(all_content, e))
400409

401410
def parse_module(self):
402411
"""Parse a module (and its children) and return a Module object."""
@@ -410,7 +419,7 @@ def parse_module(self):
410419
if self.filename.endswith('__init__.py'):
411420
cls = Package
412421
module = cls(self.filename, self.source, start, end,
413-
[], docstring, children, None, self.all)
422+
[], docstring, children, None, self.all, None, '')
414423
for child in module.children:
415424
child.parent = module
416425
module.future_imports = self.future_imports
@@ -440,6 +449,7 @@ def parse_definition(self, class_):
440449
else:
441450
self.consume(tk.OP)
442451
if self.current.kind in (tk.NEWLINE, tk.COMMENT):
452+
skipped_error_codes = self.parse_skip_comment()
443453
self.leapfrog(tk.INDENT)
444454
assert self.current.kind != tk.INDENT
445455
docstring = self.parse_docstring()
@@ -451,20 +461,33 @@ def parse_definition(self, class_):
451461
name)
452462
end = self.line - 1
453463
else: # one-liner definition
464+
skipped_error_codes = ''
454465
docstring = self.parse_docstring()
455466
decorators = [] # TODO
456467
children = []
457468
end = self.line
458469
self.leapfrog(tk.NEWLINE)
459470
definition = class_(name, self.source, start, end,
460-
decorators, docstring, children, None)
471+
decorators, docstring, children, None,
472+
skipped_error_codes)
461473
for child in definition.children:
462474
child.parent = definition
463475
self.log.debug("finished parsing %s '%s'. Next token is %r (%s)",
464476
class_.__name__, name, self.current.kind,
465477
self.current.value)
466478
return definition
467479

480+
def parse_skip_comment(self):
481+
"""Parse a definition comment for noqa skips."""
482+
skipped_error_codes = ''
483+
if self.current.kind == tk.COMMENT:
484+
if 'noqa: ' in self.current.value:
485+
skipped_error_codes = ''.join(
486+
self.current.value.split('noqa: ')[1:])
487+
elif self.current.value.startswith('# noqa'):
488+
skipped_error_codes = 'all'
489+
return skipped_error_codes
490+
468491
def check_current(self, kind=None, value=None):
469492
"""Verify the current token is of type `kind` and equals `value`."""
470493
msg = textwrap.dedent("""

src/pydocstyle/tests/test_cases/test.py

+23
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/pydocstyle/tests/test_decorators.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,14 @@ def %s(self):
166166
""" % (name))
167167

168168
module = parser.Module('module_name', source, 0, 1, [],
169-
'Docstring for module', [], None, all)
169+
'Docstring for module', [], None,
170+
all, None, '')
170171

171172
cls = parser.Class('ClassName', source, 0, 1, [],
172-
'Docstring for class', children, module, all)
173+
'Docstring for class', children, module, '')
173174

174175
return parser.Method(name, source, 0, 1, [],
175-
'Docstring for method', children, cls, all)
176+
'Docstring for method', children, cls, '')
176177

177178
def test_is_public_normal(self):
178179
"""Test that methods are normally public, even if decorated."""

src/pydocstyle/tests/test_definitions.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -140,41 +140,41 @@ def test_parser():
140140
module = parse(StringIO(source), 'file.py')
141141
assert len(list(module)) == 8
142142
assert Module('file.py', _, 1, len(source.split('\n')),
143-
_, '"""Module."""', _, _, dunder_all, {}) == \
143+
_, '"""Module."""', _, _, dunder_all, {}, '') == \
144144
module
145145

146146
function, class_ = module.children
147147
assert Function('function', _, _, _, _, '"Function."', _,
148-
module) == function
149-
assert Class('class_', _, _, _, _, '"""Class."""', _, module) == class_
148+
module, '') == function
149+
assert Class('class_', _, _, _, _, '"""Class."""', _, module, '') == class_
150150

151151
nested_1, nested_2 = function.children
152152
assert NestedFunction('nested_1', _, _, _, _,
153-
'"""Nested."""', _, function) == nested_1
153+
'"""Nested."""', _, function, '') == nested_1
154154
assert NestedFunction('nested_2', _, _, _, _, None, _,
155-
function) == nested_2
155+
function, '') == nested_2
156156
assert nested_1.is_public is False
157157

158158
method_1, method_2 = class_.children
159159
assert method_1.parent == method_2.parent == class_
160160
assert Method('method_1', _, _, _, _, '"""Method."""', _,
161-
class_) == method_1
162-
assert Method('method_2', _, _, _, _, None, _, class_) == method_2
161+
class_, '') == method_1
162+
assert Method('method_2', _, _, _, _, None, _, class_, '') == method_2
163163

164164
nested_3, = method_2.children
165165
assert NestedFunction('nested_3', _, _, _, _,
166-
'"""Nested."""', _, method_2) == nested_3
166+
'"""Nested."""', _, method_2, '') == nested_3
167167
assert nested_3.module == module
168168
assert nested_3.all == dunder_all
169169

170170
module = parse(StringIO(source_alt), 'file_alt.py')
171171
assert Module('file_alt.py', _, 1, len(source_alt.split('\n')),
172-
_, None, _, _, dunder_all, {}) == module
172+
_, None, _, _, dunder_all, {}, '') == module
173173

174174
module = parse(StringIO(source_alt_nl_at_bracket), 'file_alt_nl.py')
175175
assert Module('file_alt_nl.py', _, 1,
176176
len(source_alt_nl_at_bracket.split('\n')), _, None, _, _,
177-
dunder_all, {}) == module
177+
dunder_all, {}, '') == module
178178

179179
with pytest.raises(AllError):
180180
parse(StringIO(source_complex_all), 'file_complex_all.py')
@@ -193,7 +193,7 @@ def test_import_parser():
193193

194194
assert Module('file_ucl{0}.py'.format(i), _, 1,
195195
_, _, None, _, _,
196-
_, {'unicode_literals': True}) == module
196+
_, {'unicode_literals': True}, '') == module
197197
assert module.future_imports['unicode_literals']
198198

199199
for i, source_mfi in enumerate((
@@ -208,8 +208,8 @@ def test_import_parser():
208208
module = parse(StringIO(source_mfi), 'file_mfi{0}.py'.format(i))
209209
assert Module('file_mfi{0}.py'.format(i), _, 1,
210210
_, _, None, _, _,
211-
_, {'unicode_literals': True, 'nested_scopes': True}) \
212-
== module
211+
_, {'unicode_literals': True, 'nested_scopes': True},
212+
'') == module
213213
assert module.future_imports['unicode_literals']
214214

215215
# These are invalid syntax, so there is no need to verify the result
@@ -227,7 +227,7 @@ def test_import_parser():
227227

228228
assert Module('file_invalid{0}.py'.format(i), _, 1,
229229
_, _, None, _, _,
230-
_, _) == module
230+
_, _, '') == module
231231

232232

233233
def _test_module():

0 commit comments

Comments
 (0)