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

Commit ccfafc9

Browse files
authored
Merge pull request #204 from Eric89GXL/skip-dec
Allow skipping decorated functions
2 parents cce0cbb + daeda05 commit ccfafc9

File tree

8 files changed

+82
-44
lines changed

8 files changed

+82
-44
lines changed

docs/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Current Development Version
1010
Major Updates
1111

1212
* Support for Python 2.6 has been dropped (#206, #217).
13+
* Decorator-based skipping via ``--ignore-decorators`` has been added (#204).
1314

1415
Bug Fixes
1516

docs/snippets/cli.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ Usage
3636
search only dirs that exactly match <pattern> regular
3737
expression; default is --match-dir='[^\.].*', which
3838
matches all dirs that don't start with a dot
39+
--ignore-decorators=<decorators>
40+
ignore any functions or methods that are decorated by
41+
a function with a name fitting the <decorators>
42+
regular expression; default is --ignore-decorators=''
43+
which does not ignore any decorated functions.
3944
4045
4146
Return Code

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: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
from . import violations
1111
from .config import IllegalConfiguration
12-
# TODO: handle
13-
from .parser import *
12+
from .parser import (Package, Module, Class, NestedClass, Definition, AllError,
13+
Method, Function, NestedFunction, Parser, StringIO)
1414
from .utils import log, is_blank
1515

1616

@@ -43,36 +43,41 @@ class PEP257Checker(object):
4343
4444
"""
4545

46-
def check_source(self, source, filename):
46+
def check_source(self, source, filename, ignore_decorators):
4747
module = parse(StringIO(source), filename)
4848
for definition in module:
49-
for check in self.checks:
49+
for this_check in self.checks:
5050
terminate = False
51-
if isinstance(definition, check._check_for):
52-
if definition.skipped_error_codes != 'all':
53-
error = check(None, definition, definition.docstring)
51+
if isinstance(definition, this_check._check_for):
52+
skipping_all = (definition.skipped_error_codes == 'all')
53+
decorator_skip = ignore_decorators is not None and any(
54+
len(ignore_decorators.findall(dec.name)) > 0
55+
for dec in definition.decorators)
56+
if not skipping_all and not decorator_skip:
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: 41 additions & 27 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 = (
@@ -112,7 +114,7 @@ def parse(self):
112114

113115
self._run_conf = self._create_run_config(self._options)
114116

115-
config = self._create_check_config(self._options, use_dafaults=False)
117+
config = self._create_check_config(self._options, use_defaults=False)
116118
self._override_by_cli = config
117119

118120
@check_initialized
@@ -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_decorators(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_decorators = _get_ignore_decorators(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_decorators)
155167
else:
156168
config = self._get_config(name)
157169
match, _ = _get_matches(config)
170+
ignore_decorators = _get_ignore_decorators(config)
158171
if match(name):
159-
yield name, list(config.checked_codes)
172+
yield (name, list(config.checked_codes), ignore_decorators)
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,11 @@ 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] = \
317+
getattr(child_options, key) or getattr(parent_config, key)
318+
return CheckConfiguration(**kwargs)
309319

310320
def _parse_args(self, args=None, values=None):
311321
"""Parse the options using `self._parser` and reformat the options."""
@@ -320,29 +330,25 @@ def _create_run_config(options):
320330
return RunConfiguration(**values)
321331

322332
@classmethod
323-
def _create_check_config(cls, options, use_dafaults=True):
333+
def _create_check_config(cls, options, use_defaults=True):
324334
"""Create a `CheckConfiguration` object from `options`.
325335
326-
If `use_dafaults`, any of the match options that are `None` will
336+
If `use_defaults`, any of the match options that are `None` will
327337
be replaced with their default value and the default convention will be
328338
set for the checked codes.
329339
330340
"""
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-
339341
checked_codes = None
340342

341-
if cls._has_exclusive_option(options) or use_dafaults:
343+
if cls._has_exclusive_option(options) or use_defaults:
342344
checked_codes = cls._get_checked_errors(options)
343345

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

347353
@classmethod
348354
def _get_section_name(cls, parser):
@@ -518,12 +524,20 @@ def _create_option_parser(cls):
518524
"matches all dirs that don't start with "
519525
"a dot").format(cls.DEFAULT_MATCH_DIR_RE))
520526

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

523536

524537
# Check configuration - used by the ConfigurationParser class.
525538
CheckConfiguration = namedtuple('CheckConfiguration',
526-
('checked_codes', 'match', 'match_dir'))
539+
('checked_codes', 'match', 'match_dir',
540+
'ignore_decorators'))
527541

528542

529543
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
@@ -288,7 +289,8 @@ def test_pep257(test_case):
288289
'test_cases',
289290
test_case + '.py')
290291
results = list(check([test_case_file],
291-
select=set(ErrorRegistry.get_error_codes())))
292+
select=set(ErrorRegistry.get_error_codes()),
293+
ignore_decorators=re.compile('wraps')))
292294
for error in results:
293295
assert isinstance(error, Error)
294296
results = set([(e.definition.name, e.message) for e in results])

0 commit comments

Comments
 (0)