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

Commit d4d9d63

Browse files
TomFryerssambhav
andauthored
Exempt properties from D401 (#546)
* Exempt properties from D401 * Add test and release note * Add property_decorators configuration option * Fix failing test * Clarify Function is_overload and is_property * Fix crash * Fix typo * Fix test * Also count functools.cached_property as property * Add cached_property tests * Make line less then 80 characters long * Blacken * Revert "Add cached_property tests" This reverts commit caf8f42. * Update docs/release_notes.rst Co-authored-by: Sambhav Kothari <[email protected]>
1 parent 0bc57cd commit d4d9d63

File tree

8 files changed

+101
-15
lines changed

8 files changed

+101
-15
lines changed

docs/release_notes.rst

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Release Notes
88
Current Development Version
99
---------------------------
1010

11+
New Features
12+
13+
* Add support for `property_decorators` config to ignore D401
1114

1215
6.1.1 - May 17th, 2021
1316
---------------------------

docs/snippets/config.rst

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Available options are:
4444
* ``match``
4545
* ``match_dir``
4646
* ``ignore_decorators``
47+
* ``property_decorators``
4748

4849
See the :ref:`cli_usage` section for more information.
4950

src/pydocstyle/checker.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,12 @@ def check_source(
131131
source,
132132
filename,
133133
ignore_decorators=None,
134+
property_decorators=None,
134135
ignore_inline_noqa=False,
135136
):
137+
self.property_decorators = (
138+
{} if property_decorators is None else property_decorators
139+
)
136140
module = parse(StringIO(source), filename)
137141
for definition in module:
138142
for this_check in self.checks:
@@ -500,7 +504,11 @@ def check_imperative_mood(self, function, docstring): # def context
500504
"Returns the pathname ...".
501505
502506
"""
503-
if docstring and not function.is_test:
507+
if (
508+
docstring
509+
and not function.is_test
510+
and not function.is_property(self.property_decorators)
511+
):
504512
stripped = ast.literal_eval(docstring).strip()
505513
if stripped:
506514
first_word = strip_non_alphanumeric(stripped.split()[0])
@@ -1040,6 +1048,7 @@ def check(
10401048
select=None,
10411049
ignore=None,
10421050
ignore_decorators=None,
1051+
property_decorators=None,
10431052
ignore_inline_noqa=False,
10441053
):
10451054
"""Generate docstring errors that exist in `filenames` iterable.
@@ -1092,7 +1101,11 @@ def check(
10921101
with tk.open(filename) as file:
10931102
source = file.read()
10941103
for error in ConventionChecker().check_source(
1095-
source, filename, ignore_decorators, ignore_inline_noqa
1104+
source,
1105+
filename,
1106+
ignore_decorators,
1107+
property_decorators,
1108+
ignore_inline_noqa,
10961109
):
10971110
code = getattr(error, 'code', None)
10981111
if code in checked_codes:

src/pydocstyle/cli.py

+2
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ def run_pydocstyle():
4242
filename,
4343
checked_codes,
4444
ignore_decorators,
45+
property_decorators,
4546
) in conf.get_files_to_check():
4647
errors.extend(
4748
check(
4849
(filename,),
4950
select=checked_codes,
5051
ignore_decorators=ignore_decorators,
52+
property_decorators=property_decorators,
5153
)
5254
)
5355
except IllegalConfiguration as error:

src/pydocstyle/config.py

+54-5
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ class ConfigurationParser:
186186
DEFAULT_MATCH_RE = r'(?!test_).*\.py'
187187
DEFAULT_MATCH_DIR_RE = r'[^\.].*'
188188
DEFAULT_IGNORE_DECORATORS_RE = ''
189+
DEFAULT_PROPERTY_DECORATORS = (
190+
"property,cached_property,functools.cached_property"
191+
)
189192
DEFAULT_CONVENTION = conventions.pep257
190193

191194
PROJECT_CONFIG_FILES = (
@@ -266,12 +269,21 @@ def _get_ignore_decorators(conf):
266269
re(conf.ignore_decorators) if conf.ignore_decorators else None
267270
)
268271

272+
def _get_property_decorators(conf):
273+
"""Return the `property_decorators` as None or set."""
274+
return (
275+
set(conf.property_decorators.split(","))
276+
if conf.property_decorators
277+
else None
278+
)
279+
269280
for name in self._arguments:
270281
if os.path.isdir(name):
271282
for root, dirs, filenames in os.walk(name):
272283
config = self._get_config(os.path.abspath(root))
273284
match, match_dir = _get_matches(config)
274285
ignore_decorators = _get_ignore_decorators(config)
286+
property_decorators = _get_property_decorators(config)
275287

276288
# Skip any dirs that do not match match_dir
277289
dirs[:] = [d for d in dirs if match_dir(d)]
@@ -283,13 +295,20 @@ def _get_ignore_decorators(conf):
283295
full_path,
284296
list(config.checked_codes),
285297
ignore_decorators,
298+
property_decorators,
286299
)
287300
else:
288301
config = self._get_config(os.path.abspath(name))
289302
match, _ = _get_matches(config)
290303
ignore_decorators = _get_ignore_decorators(config)
304+
property_decorators = _get_property_decorators(config)
291305
if match(name):
292-
yield (name, list(config.checked_codes), ignore_decorators)
306+
yield (
307+
name,
308+
list(config.checked_codes),
309+
ignore_decorators,
310+
property_decorators,
311+
)
293312

294313
# --------------------------- Private Methods -----------------------------
295314

@@ -485,7 +504,12 @@ def _merge_configuration(self, parent_config, child_options):
485504
self._set_add_options(error_codes, child_options)
486505

487506
kwargs = dict(checked_codes=error_codes)
488-
for key in ('match', 'match_dir', 'ignore_decorators'):
507+
for key in (
508+
'match',
509+
'match_dir',
510+
'ignore_decorators',
511+
'property_decorators',
512+
):
489513
kwargs[key] = getattr(child_options, key) or getattr(
490514
parent_config, key
491515
)
@@ -519,9 +543,15 @@ def _create_check_config(cls, options, use_defaults=True):
519543
checked_codes = cls._get_checked_errors(options)
520544

521545
kwargs = dict(checked_codes=checked_codes)
522-
for key in ('match', 'match_dir', 'ignore_decorators'):
546+
defaults = {
547+
'match': "MATCH_RE",
548+
'match_dir': "MATCH_DIR_RE",
549+
'ignore_decorators': "IGNORE_DECORATORS_RE",
550+
'property_decorators': "PROPERTY_DECORATORS",
551+
}
552+
for key, default in defaults.items():
523553
kwargs[key] = (
524-
getattr(cls, f'DEFAULT_{key.upper()}_RE')
554+
getattr(cls, f"DEFAULT_{default}")
525555
if getattr(options, key) is None and use_defaults
526556
else getattr(options, key)
527557
)
@@ -855,14 +885,33 @@ def _create_option_parser(cls):
855885
)
856886
),
857887
)
888+
option(
889+
'--property-decorators',
890+
metavar='<property-decorators>',
891+
default=None,
892+
help=(
893+
"consider any method decorated with one of these "
894+
"decorators as a property, and consequently allow "
895+
"a docstring which is not in imperative mood; default "
896+
"is --property-decorators='{}'".format(
897+
cls.DEFAULT_PROPERTY_DECORATORS
898+
)
899+
),
900+
)
858901

859902
return parser
860903

861904

862905
# Check configuration - used by the ConfigurationParser class.
863906
CheckConfiguration = namedtuple(
864907
'CheckConfiguration',
865-
('checked_codes', 'match', 'match_dir', 'ignore_decorators'),
908+
(
909+
'checked_codes',
910+
'match',
911+
'match_dir',
912+
'ignore_decorators',
913+
'property_decorators',
914+
),
866915
)
867916

868917

src/pydocstyle/parser.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,16 @@ def is_public(self):
213213
@property
214214
def is_overload(self):
215215
"""Return True iff the method decorated with overload."""
216-
for decorator in self.decorators:
217-
if decorator.name == "overload":
218-
return True
219-
return False
216+
return any(
217+
decorator.name == "overload" for decorator in self.decorators
218+
)
219+
220+
def is_property(self, property_decorator_names):
221+
"""Return True if the method is decorated with any property decorator."""
222+
return any(
223+
decorator.name in property_decorator_names
224+
for decorator in self.decorators
225+
)
220226

221227
@property
222228
def is_test(self):

src/tests/test_cases/test.py

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def overloaded_method(a):
4242
"D418: Function/ Method decorated with @overload"
4343
" shouldn't contain a docstring")
4444

45+
@property
46+
def foo(self):
47+
"""The foo of the thing, which isn't in imperitive mood."""
48+
return "hello"
49+
4550
@expect('D102: Missing docstring in public method')
4651
def __new__(self=None):
4752
pass

src/tests/test_definitions.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import pytest
66
from pydocstyle.violations import Error, ErrorRegistry
77
from pydocstyle.checker import check
8+
from pydocstyle.config import ConfigurationParser
9+
10+
DEFAULT_PROPERTY_DECORATORS = ConfigurationParser.DEFAULT_PROPERTY_DECORATORS
811

912

1013
@pytest.mark.parametrize('test_case', [
@@ -35,10 +38,14 @@ def test_complex_file(test_case):
3538
test_case_file = os.path.join(test_case_dir,
3639
'test_cases',
3740
test_case + '.py')
38-
results = list(check([test_case_file],
39-
select=set(ErrorRegistry.get_error_codes()),
40-
ignore_decorators=re.compile(
41-
'wraps|ignored_decorator')))
41+
results = list(
42+
check(
43+
[test_case_file],
44+
select=set(ErrorRegistry.get_error_codes()),
45+
ignore_decorators=re.compile('wraps|ignored_decorator'),
46+
property_decorators=DEFAULT_PROPERTY_DECORATORS,
47+
)
48+
)
4249
for error in results:
4350
assert isinstance(error, Error)
4451
results = {(e.definition.name, e.message) for e in results}

0 commit comments

Comments
 (0)