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

Commit a9a73f9

Browse files
thejcannonsambhav
andauthored
Add ignore-self-only-init option (#560)
* add `ignore-self-only-init` option * docs * callable_args * fix * Update release_notes.rst * Update release_notes.rst Co-authored-by: Sambhav Kothari <[email protected]>
1 parent d395b01 commit a9a73f9

File tree

8 files changed

+86
-17
lines changed

8 files changed

+86
-17
lines changed

docs/release_notes.rst

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ Release Notes
44
**pydocstyle** version numbers follow the
55
`Semantic Versioning <http://semver.org/>`_ specification.
66

7+
6.3.0 - January 17th, 2023
8+
--------------------------
9+
10+
New Features
11+
12+
* Add `ignore-self-only-init` config (#560).
13+
714
6.2.3 - January 8th, 2023
815
---------------------------
916

docs/snippets/config.rst

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Available options are:
4545
* ``match_dir``
4646
* ``ignore_decorators``
4747
* ``property_decorators``
48+
* ``ignore_self_only_init``
4849

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

src/pydocstyle/checker.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,12 @@ def check_source(
136136
ignore_decorators=None,
137137
property_decorators=None,
138138
ignore_inline_noqa=False,
139+
ignore_self_only_init=False,
139140
):
140141
self.property_decorators = (
141142
{} if property_decorators is None else property_decorators
142143
)
144+
self.ignore_self_only_init = ignore_self_only_init
143145
module = parse(StringIO(source), filename)
144146
for definition in module:
145147
for this_check in self.checks:
@@ -199,22 +201,27 @@ def check_docstring_missing(self, definition, docstring):
199201
with a single underscore.
200202
201203
"""
204+
205+
def method_violation():
206+
if definition.is_magic:
207+
return violations.D105()
208+
if definition.is_init:
209+
if (
210+
self.ignore_self_only_init
211+
and len(definition.param_names) == 1
212+
):
213+
return None
214+
return violations.D107()
215+
if not definition.is_overload:
216+
return violations.D102()
217+
return None
218+
202219
if not docstring and definition.is_public:
203220
codes = {
204221
Module: violations.D100,
205222
Class: violations.D101,
206223
NestedClass: violations.D106,
207-
Method: lambda: violations.D105()
208-
if definition.is_magic
209-
else (
210-
violations.D107()
211-
if definition.is_init
212-
else (
213-
violations.D102()
214-
if not definition.is_overload
215-
else None
216-
)
217-
),
224+
Method: method_violation,
218225
NestedFunction: violations.D103,
219226
Function: (
220227
lambda: violations.D103()
@@ -1102,6 +1109,7 @@ def check(
11021109
ignore_decorators=None,
11031110
property_decorators=None,
11041111
ignore_inline_noqa=False,
1112+
ignore_self_only_init=False,
11051113
):
11061114
"""Generate docstring errors that exist in `filenames` iterable.
11071115
@@ -1121,6 +1129,8 @@ def check(
11211129
11221130
`ignore_inline_noqa` controls if `# noqa` comments are respected or not.
11231131
1132+
`ignore_self_only_init` controls if D107 is reported on __init__ only containing `self`.
1133+
11241134
Examples
11251135
---------
11261136
>>> check(['pydocstyle.py'])
@@ -1158,6 +1168,7 @@ def check(
11581168
ignore_decorators,
11591169
property_decorators,
11601170
ignore_inline_noqa,
1171+
ignore_self_only_init,
11611172
):
11621173
code = getattr(error, 'code', None)
11631174
if code in checked_codes:

src/pydocstyle/cli.py

+2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ def run_pydocstyle():
4343
checked_codes,
4444
ignore_decorators,
4545
property_decorators,
46+
ignore_self_only_init,
4647
) in conf.get_files_to_check():
4748
errors.extend(
4849
check(
4950
(filename,),
5051
select=checked_codes,
5152
ignore_decorators=ignore_decorators,
5253
property_decorators=property_decorators,
54+
ignore_self_only_init=ignore_self_only_init,
5355
)
5456
)
5557
except IllegalConfiguration as error:

src/pydocstyle/config.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ class ConfigurationParser:
185185
'match',
186186
'match-dir',
187187
'ignore-decorators',
188+
'ignore-self-only-init',
188189
)
189190
BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention')
190191

@@ -195,6 +196,7 @@ class ConfigurationParser:
195196
"property,cached_property,functools.cached_property"
196197
)
197198
DEFAULT_CONVENTION = conventions.pep257
199+
DEFAULT_IGNORE_SELF_ONLY_INIT = False
198200

199201
PROJECT_CONFIG_FILES = (
200202
'setup.cfg',
@@ -301,6 +303,7 @@ def _get_property_decorators(conf):
301303
list(config.checked_codes),
302304
ignore_decorators,
303305
property_decorators,
306+
config.ignore_self_only_init,
304307
)
305308
else:
306309
config = self._get_config(os.path.abspath(name))
@@ -313,6 +316,7 @@ def _get_property_decorators(conf):
313316
list(config.checked_codes),
314317
ignore_decorators,
315318
property_decorators,
319+
config.ignore_self_only_init,
316320
)
317321

318322
# --------------------------- Private Methods -----------------------------
@@ -514,9 +518,13 @@ def _merge_configuration(self, parent_config, child_options):
514518
'match_dir',
515519
'ignore_decorators',
516520
'property_decorators',
521+
'ignore_self_only_init',
517522
):
518-
kwargs[key] = getattr(child_options, key) or getattr(
519-
parent_config, key
523+
child_value = getattr(child_options, key)
524+
kwargs[key] = (
525+
child_value
526+
if child_value is not None
527+
else getattr(parent_config, key)
520528
)
521529
return CheckConfiguration(**kwargs)
522530

@@ -553,6 +561,7 @@ def _create_check_config(cls, options, use_defaults=True):
553561
'match_dir': "MATCH_DIR_RE",
554562
'ignore_decorators': "IGNORE_DECORATORS_RE",
555563
'property_decorators': "PROPERTY_DECORATORS",
564+
'ignore_self_only_init': "IGNORE_SELF_ONLY_INIT",
556565
}
557566
for key, default in defaults.items():
558567
kwargs[key] = (
@@ -849,6 +858,12 @@ def _create_option_parser(cls):
849858
'basic list previously set by --select, --ignore '
850859
'or --convention.',
851860
)
861+
add_check(
862+
'--ignore-self-only-init',
863+
default=None,
864+
action='store_true',
865+
help='ignore __init__ methods which only have a self param.',
866+
)
852867

853868
parser.add_option_group(check_group)
854869

@@ -916,6 +931,7 @@ def _create_option_parser(cls):
916931
'match_dir',
917932
'ignore_decorators',
918933
'property_decorators',
934+
'ignore_self_only_init',
919935
),
920936
)
921937

src/pydocstyle/parser.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class Definition(Value):
8989
'decorators',
9090
'docstring',
9191
'children',
92+
'callable_args',
9293
'parent',
9394
'skipped_error_codes',
9495
) # type: Tuple[str, ...]
@@ -235,6 +236,11 @@ def is_test(self):
235236
"""
236237
return self.name.startswith('test') or self.name == 'runTest'
237238

239+
@property
240+
def param_names(self):
241+
"""Return the parameter names."""
242+
return self.callable_args
243+
238244

239245
class NestedFunction(Function):
240246
"""A Python source code nested function."""
@@ -666,8 +672,10 @@ def parse_definition(self, class_):
666672
name = self.current.value
667673
self.log.debug("parsing %s '%s'", class_.__name__, name)
668674
self.stream.move()
675+
callable_args = []
669676
if self.current.kind == tk.OP and self.current.value == '(':
670677
parenthesis_level = 0
678+
in_default_arg = False
671679
while True:
672680
if self.current.kind == tk.OP:
673681
if self.current.value == '(':
@@ -676,6 +684,15 @@ def parse_definition(self, class_):
676684
parenthesis_level -= 1
677685
if parenthesis_level == 0:
678686
break
687+
elif self.current.value == ',':
688+
in_default_arg = False
689+
elif (
690+
parenthesis_level == 1
691+
and self.current.kind == tk.NAME
692+
and not in_default_arg
693+
):
694+
callable_args.append(self.current.value)
695+
in_default_arg = True
679696
self.stream.move()
680697
if self.current.kind != tk.OP or self.current.value != ':':
681698
self.leapfrog(tk.OP, value=":")
@@ -712,7 +729,8 @@ def parse_definition(self, class_):
712729
decorators,
713730
docstring,
714731
children,
715-
None,
732+
callable_args,
733+
None, # parent
716734
skipped_error_codes,
717735
)
718736
for child in definition.children:

src/tests/test_decorators.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,10 @@ def %s(self):
182182
dunder_all, None, None, '')
183183

184184
cls = parser.Class('ClassName', source, 0, 1, [],
185-
'Docstring for class', children, module, '')
185+
'Docstring for class', children, [], module, '')
186186

187187
return parser.Method(name, source, 0, 1, [],
188-
'Docstring for method', children, cls, '')
188+
'Docstring for method', children, [], cls, '')
189189

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

src/tests/test_integration.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,20 @@ def test_comment_with_noqa_plus_docstring_file(env):
15511551
assert code == 0
15521552

15531553

1554+
def test_ignore_self_only_init(env):
1555+
"""Test that ignore_self_only_init works ignores __init__ with only self."""
1556+
with env.open('example.py', 'wt') as example:
1557+
example.write(textwrap.dedent("""\
1558+
class Foo:
1559+
def __init__(self):
1560+
pass
1561+
"""))
1562+
1563+
env.write_config(ignore_self_only_init=True, select="D107")
1564+
out, err, code = env.invoke()
1565+
assert '' == out
1566+
assert code == 0
1567+
15541568
def test_match_considers_basenames_for_path_args(env):
15551569
"""Test that `match` option only considers basenames for path arguments.
15561570
@@ -1570,4 +1584,4 @@ def test_match_considers_basenames_for_path_args(env):
15701584
# env.invoke calls pydocstyle with full path to test_a.py
15711585
out, _, code = env.invoke(target='test_a.py')
15721586
assert '' == out
1573-
assert code == 0
1587+
assert code == 0

0 commit comments

Comments
 (0)