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

Commit 8ea53ea

Browse files
committed
Allow skipping decorated functions
1 parent d0d5bec commit 8ea53ea

File tree

6 files changed

+72
-39
lines changed

6 files changed

+72
-39
lines changed

docs/snippets/config.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Available options are:
3232
* ``add_ignore``
3333
* ``match``
3434
* ``match_dir``
35+
* ``ignore_decorators``
3536

3637
See the :ref:`cli_usage` section for more information.
3738

src/pydocstyle/checker.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from . import violations
1111
from .config import IllegalConfiguration
1212
# TODO: handle
13-
from .parser import *
13+
from .parser import (Package, Module, Class, NestedClass, Definition, AllError,
14+
Method, Function, NestedFunction, Parser, StringIO)
1415
from .utils import log, is_blank
1516

1617

@@ -43,36 +44,40 @@ class PEP257Checker(object):
4344
4445
"""
4546

46-
def check_source(self, source, filename):
47+
def check_source(self, source, filename, ignore_decorators):
4748
module = parse(StringIO(source), filename)
4849
for definition in module:
49-
for check in self.checks:
50+
for this_check in self.checks:
5051
terminate = False
51-
if isinstance(definition, check._check_for):
52-
if definition.skipped_error_codes != 'all':
53-
error = check(None, definition, definition.docstring)
52+
if isinstance(definition, this_check._check_for):
53+
if definition.skipped_error_codes != 'all' and \
54+
not any(ignore_decorators is not None and
55+
len(ignore_decorators.findall(dec.name))
56+
for dec in definition.decorators):
57+
error = this_check(None, definition,
58+
definition.docstring)
5459
else:
5560
error = None
5661
errors = error if hasattr(error, '__iter__') else [error]
5762
for error in errors:
5863
if error is not None and error.code not in \
5964
definition.skipped_error_codes:
60-
partition = check.__doc__.partition('.\n')
65+
partition = this_check.__doc__.partition('.\n')
6166
message, _, explanation = partition
6267
error.set_context(explanation=explanation,
6368
definition=definition)
6469
yield error
65-
if check._terminal:
70+
if this_check._terminal:
6671
terminate = True
6772
break
6873
if terminate:
6974
break
7075

7176
@property
7277
def checks(self):
73-
all = [check for check in vars(type(self)).values()
74-
if hasattr(check, '_check_for')]
75-
return sorted(all, key=lambda check: not check._terminal)
78+
all = [this_check for this_check in vars(type(self)).values()
79+
if hasattr(this_check, '_check_for')]
80+
return sorted(all, key=lambda this_check: not this_check._terminal)
7681

7782
@check_for(Definition, terminal=True)
7883
def check_docstring_missing(self, definition, docstring):
@@ -387,7 +392,7 @@ def check_starts_with_this(self, function, docstring):
387392
parse = Parser()
388393

389394

390-
def check(filenames, select=None, ignore=None):
395+
def check(filenames, select=None, ignore=None, ignore_decorators=None):
391396
"""Generate docstring errors that exist in `filenames` iterable.
392397
393398
By default, the PEP-257 convention is checked. To specifically define the
@@ -432,7 +437,8 @@ def check(filenames, select=None, ignore=None):
432437
try:
433438
with tokenize_open(filename) as file:
434439
source = file.read()
435-
for error in PEP257Checker().check_source(source, filename):
440+
for error in PEP257Checker().check_source(source, filename,
441+
ignore_decorators):
436442
code = getattr(error, 'code', None)
437443
if code in checked_codes:
438444
yield error

src/pydocstyle/cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ def run_pydocstyle(use_pep257=False):
4545

4646
errors = []
4747
try:
48-
for filename, checked_codes in conf.get_files_to_check():
49-
errors.extend(check((filename,), select=checked_codes))
48+
for filename, checked_codes, ignore_decorators in \
49+
conf.get_files_to_check():
50+
errors.extend(check((filename,), select=checked_codes,
51+
ignore_decorators=ignore_decorators))
5052
except IllegalConfiguration:
5153
# An illegal configuration file was found during file generation.
5254
return ReturnCode.invalid_options

src/pydocstyle/config.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ class ConfigurationParser(object):
6464
"""
6565

6666
CONFIG_FILE_OPTIONS = ('convention', 'select', 'ignore', 'add-select',
67-
'add-ignore', 'match', 'match-dir')
67+
'add-ignore', 'match', 'match-dir',
68+
'ignore-decorators')
6869
BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention')
6970

7071
DEFAULT_MATCH_RE = '(?!test_).*\.py'
7172
DEFAULT_MATCH_DIR_RE = '[^\.].*'
73+
DEFAULT_IGNORE_DECORATORS_RE = ''
7274
DEFAULT_CONVENTION = conventions.pep257
7375

7476
PROJECT_CONFIG_FILES = (
@@ -139,31 +141,42 @@ def _get_matches(config):
139141
match_dir_func = re(config.match_dir + '$').match
140142
return match_func, match_dir_func
141143

144+
def _get_ignore_dec(config):
145+
"""Return the `ignore_decorators` as None or regex."""
146+
if config.ignore_decorators: # not None and not ''
147+
ignore_decorators = re(config.ignore_decorators)
148+
else:
149+
ignore_decorators = None
150+
return ignore_decorators
151+
142152
for name in self._arguments:
143153
if os.path.isdir(name):
144154
for root, dirs, filenames in os.walk(name):
145155
config = self._get_config(root)
146156
match, match_dir = _get_matches(config)
157+
ignore_dec = _get_ignore_dec(config)
147158

148159
# Skip any dirs that do not match match_dir
149160
dirs[:] = [dir for dir in dirs if match_dir(dir)]
150161

151162
for filename in filenames:
152163
if match(filename):
153164
full_path = os.path.join(root, filename)
154-
yield full_path, list(config.checked_codes)
165+
yield (full_path, list(config.checked_codes),
166+
ignore_dec)
155167
else:
156168
config = self._get_config(name)
157169
match, _ = _get_matches(config)
170+
ignore_dec = _get_ignore_dec(config)
158171
if match(name):
159-
yield name, list(config.checked_codes)
172+
yield (name, list(config.checked_codes), ignore_dec)
160173

161174
# --------------------------- Private Methods -----------------------------
162175

163176
def _get_config(self, node):
164177
"""Get and cache the run configuration for `node`.
165178
166-
If no configuration exists (not local and not for the parend node),
179+
If no configuration exists (not local and not for the parent node),
167180
returns and caches a default configuration.
168181
169182
The algorithm:
@@ -298,14 +311,12 @@ def _merge_configuration(self, parent_config, child_options):
298311

299312
self._set_add_options(error_codes, child_options)
300313

301-
match = child_options.match \
302-
if child_options.match is not None else parent_config.match
303-
match_dir = child_options.match_dir \
304-
if child_options.match_dir is not None else parent_config.match_dir
305-
306-
return CheckConfiguration(checked_codes=error_codes,
307-
match=match,
308-
match_dir=match_dir)
314+
kwargs = dict(checked_codes=error_codes)
315+
for key in ('match', 'match_dir', 'ignore_decorators'):
316+
kwargs[key] = getattr(child_options, key) \
317+
if getattr(child_options, key) is not None \
318+
else getattr(parent_config, key)
319+
return CheckConfiguration(**kwargs)
309320

310321
def _parse_args(self, args=None, values=None):
311322
"""Parse the options using `self._parser` and reformat the options."""
@@ -328,21 +339,17 @@ def _create_check_config(cls, options, use_dafaults=True):
328339
set for the checked codes.
329340
330341
"""
331-
match = cls.DEFAULT_MATCH_RE \
332-
if options.match is None and use_dafaults \
333-
else options.match
334-
335-
match_dir = cls.DEFAULT_MATCH_DIR_RE \
336-
if options.match_dir is None and use_dafaults \
337-
else options.match_dir
338-
339342
checked_codes = None
340343

341344
if cls._has_exclusive_option(options) or use_dafaults:
342345
checked_codes = cls._get_checked_errors(options)
343346

344-
return CheckConfiguration(checked_codes=checked_codes,
345-
match=match, match_dir=match_dir)
347+
kwargs = dict(checked_codes=checked_codes)
348+
for key in ('match', 'match_dir', 'ignore_decorators'):
349+
kwargs[key] = getattr(cls, 'DEFAULT_{0}_RE'.format(key.upper())) \
350+
if getattr(options, key) is None and use_dafaults \
351+
else getattr(options, key)
352+
return CheckConfiguration(**kwargs)
346353

347354
@classmethod
348355
def _get_section_name(cls, parser):
@@ -518,12 +525,20 @@ def _create_option_parser(cls):
518525
"matches all dirs that don't start with "
519526
"a dot").format(cls.DEFAULT_MATCH_DIR_RE))
520527

528+
# Decorators
529+
option('--ignore-decorators', metavar='<decorators>', default=None,
530+
help=("ignore any functions or methods that are decorated "
531+
"by a function with a name fitting the <decorators> "
532+
"regular expression; default is --ignore-decorators='{0}'"
533+
"which does not ignore any decorated functions."
534+
.format(cls.DEFAULT_IGNORE_DECORATORS_RE)))
521535
return parser
522536

523537

524538
# Check configuration - used by the ConfigurationParser class.
525539
CheckConfiguration = namedtuple('CheckConfiguration',
526-
('checked_codes', 'match', 'match_dir'))
540+
('checked_codes', 'match', 'match_dir',
541+
'ignore_decorators'))
527542

528543

529544
class IllegalConfiguration(Exception):

src/tests/test_cases/test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# encoding: utf-8
22
# No docstring, so we can test D100
3+
from functools import wraps
34
import os
45
import sys
56
from .expected import Expectation
@@ -363,5 +364,11 @@ def docstring_ignore_violations_of_pydocstyle_D400_and_PEP8_E501_but_catch_D401(
363364
"""Runs something"""
364365
pass
365366

367+
368+
@wraps(docstring_bad_ignore_one)
369+
def bad_decorated_function():
370+
"""Bad (E501) but decorated"""
371+
pass
372+
366373
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
367374
'D100: Missing docstring in public module')

src/tests/test_definitions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Old parser tests."""
22

33
import os
4+
import re
45
import pytest
56
from pydocstyle.violations import Error, ErrorRegistry
67
from pydocstyle.checker import check
@@ -278,7 +279,8 @@ def test_pep257(test_case):
278279
'test_cases',
279280
test_case + '.py')
280281
results = list(check([test_case_file],
281-
select=set(ErrorRegistry.get_error_codes())))
282+
select=set(ErrorRegistry.get_error_codes()),
283+
ignore_decorators=re.compile('wraps')))
282284
for error in results:
283285
assert isinstance(error, Error)
284286
results = set([(e.definition.name, e.message) for e in results])

0 commit comments

Comments
 (0)