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

Add ignore-self-only-init option #560

Merged
merged 9 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ New Features

* Add support for `property_decorators` config to ignore D401.
* Add support for Python 3.10 (#554).
* Add `ignore-self-only-init` config (#560).
thejcannon marked this conversation as resolved.
Show resolved Hide resolved

6.1.1 - May 17th, 2021
---------------------------
Expand Down
1 change: 1 addition & 0 deletions docs/snippets/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Available options are:
* ``match_dir``
* ``ignore_decorators``
* ``property_decorators``
* ``ignore_self_only_init``

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

Expand Down
33 changes: 22 additions & 11 deletions src/pydocstyle/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,12 @@ def check_source(
ignore_decorators=None,
property_decorators=None,
ignore_inline_noqa=False,
ignore_self_only_init=False,
):
self.property_decorators = (
{} if property_decorators is None else property_decorators
)
self.ignore_self_only_init = ignore_self_only_init
module = parse(StringIO(source), filename)
for definition in module:
for this_check in self.checks:
Expand Down Expand Up @@ -196,6 +198,21 @@ def check_docstring_missing(self, definition, docstring):
with a single underscore.

"""

def method_violation():
if definition.is_magic:
return violations.D105()
if definition.is_init:
if (
self.ignore_self_only_init
and len(definition.param_names) == 1
):
return None
return violations.D107()
if not definition.is_overload:
return violations.D102()
return None

if (
not docstring
and definition.is_public
Expand All @@ -206,17 +223,7 @@ def check_docstring_missing(self, definition, docstring):
Module: violations.D100,
Class: violations.D101,
NestedClass: violations.D106,
Method: lambda: violations.D105()
if definition.is_magic
else (
violations.D107()
if definition.is_init
else (
violations.D102()
if not definition.is_overload
else None
)
),
Method: method_violation,
NestedFunction: violations.D103,
Function: (
lambda: violations.D103()
Expand Down Expand Up @@ -1050,6 +1057,7 @@ def check(
ignore_decorators=None,
property_decorators=None,
ignore_inline_noqa=False,
ignore_self_only_init=False,
):
"""Generate docstring errors that exist in `filenames` iterable.

Expand All @@ -1069,6 +1077,8 @@ def check(

`ignore_inline_noqa` controls if `# noqa` comments are respected or not.

`ignore_self_only_init` controls if D107 is reported on __init__ only containing `self`.

Examples
---------
>>> check(['pydocstyle.py'])
Expand Down Expand Up @@ -1106,6 +1116,7 @@ def check(
ignore_decorators,
property_decorators,
ignore_inline_noqa,
ignore_self_only_init,
):
code = getattr(error, 'code', None)
if code in checked_codes:
Expand Down
2 changes: 2 additions & 0 deletions src/pydocstyle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ def run_pydocstyle():
checked_codes,
ignore_decorators,
property_decorators,
ignore_self_only_init,
) in conf.get_files_to_check():
errors.extend(
check(
(filename,),
select=checked_codes,
ignore_decorators=ignore_decorators,
property_decorators=property_decorators,
ignore_self_only_init=ignore_self_only_init,
)
)
except IllegalConfiguration as error:
Expand Down
20 changes: 18 additions & 2 deletions src/pydocstyle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class ConfigurationParser:
'match',
'match-dir',
'ignore-decorators',
'ignore-self-only-init',
)
BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention')

Expand All @@ -190,6 +191,7 @@ class ConfigurationParser:
"property,cached_property,functools.cached_property"
)
DEFAULT_CONVENTION = conventions.pep257
DEFAULT_IGNORE_SELF_ONLY_INIT = False

PROJECT_CONFIG_FILES = (
'setup.cfg',
Expand Down Expand Up @@ -296,6 +298,7 @@ def _get_property_decorators(conf):
list(config.checked_codes),
ignore_decorators,
property_decorators,
config.ignore_self_only_init,
)
else:
config = self._get_config(os.path.abspath(name))
Expand All @@ -308,6 +311,7 @@ def _get_property_decorators(conf):
list(config.checked_codes),
ignore_decorators,
property_decorators,
config.ignore_self_only_init,
)

# --------------------------- Private Methods -----------------------------
Expand Down Expand Up @@ -509,9 +513,13 @@ def _merge_configuration(self, parent_config, child_options):
'match_dir',
'ignore_decorators',
'property_decorators',
'ignore_self_only_init',
):
kwargs[key] = getattr(child_options, key) or getattr(
parent_config, key
child_value = getattr(child_options, key)
kwargs[key] = (
child_value
if child_value is not None
else getattr(parent_config, key)
)
return CheckConfiguration(**kwargs)

Expand Down Expand Up @@ -548,6 +556,7 @@ def _create_check_config(cls, options, use_defaults=True):
'match_dir': "MATCH_DIR_RE",
'ignore_decorators': "IGNORE_DECORATORS_RE",
'property_decorators': "PROPERTY_DECORATORS",
'ignore_self_only_init': "IGNORE_SELF_ONLY_INIT",
}
for key, default in defaults.items():
kwargs[key] = (
Expand Down Expand Up @@ -844,6 +853,12 @@ def _create_option_parser(cls):
'basic list previously set by --select, --ignore '
'or --convention.',
)
add_check(
'--ignore-self-only-init',
default=None,
action='store_true',
help='ignore __init__ methods which only have a self param.',
)

parser.add_option_group(check_group)

Expand Down Expand Up @@ -911,6 +926,7 @@ def _create_option_parser(cls):
'match_dir',
'ignore_decorators',
'property_decorators',
'ignore_self_only_init',
),
)

Expand Down
20 changes: 19 additions & 1 deletion src/pydocstyle/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class Definition(Value):
'decorators',
'docstring',
'children',
'call_names',
'parent',
'skipped_error_codes',
) # type: Tuple[str, ...]
Expand Down Expand Up @@ -235,6 +236,11 @@ def is_test(self):
"""
return self.name.startswith('test') or self.name == 'runTest'

@property
def param_names(self):
"""Return the parameter names."""
return self.call_names


class NestedFunction(Function):
"""A Python source code nested function."""
Expand Down Expand Up @@ -665,8 +671,10 @@ def parse_definition(self, class_):
name = self.current.value
self.log.debug("parsing %s '%s'", class_.__name__, name)
self.stream.move()
call_names = []
thejcannon marked this conversation as resolved.
Show resolved Hide resolved
if self.current.kind == tk.OP and self.current.value == '(':
parenthesis_level = 0
in_default_arg = False
while True:
if self.current.kind == tk.OP:
if self.current.value == '(':
Expand All @@ -675,6 +683,15 @@ def parse_definition(self, class_):
parenthesis_level -= 1
if parenthesis_level == 0:
break
elif self.current.value == ',':
in_default_arg = False
elif (
parenthesis_level == 1
and self.current.kind == tk.NAME
and not in_default_arg
):
call_names.append(self.current.value)
in_default_arg = True
self.stream.move()
if self.current.kind != tk.OP or self.current.value != ':':
self.leapfrog(tk.OP, value=":")
Expand Down Expand Up @@ -711,7 +728,8 @@ def parse_definition(self, class_):
decorators,
docstring,
children,
None,
call_names,
None, # parent
skipped_error_codes,
)
for child in definition.children:
Expand Down
4 changes: 2 additions & 2 deletions src/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ def %s(self):
dunder_all, None, None, '')

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

return parser.Method(name, source, 0, 1, [],
'Docstring for method', children, cls, '')
'Docstring for method', children, [], cls, '')

def test_is_public_normal(self):
"""Test that methods are normally public, even if decorated."""
Expand Down
15 changes: 15 additions & 0 deletions src/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,3 +1489,18 @@ def test_comment_with_noqa_plus_docstring_file(env):
out, _, code = env.invoke()
assert '' == out
assert code == 0


def test_ignore_self_only_init(env):
"""Test that ignore_self_only_init works ignores __init__ with only self."""
with env.open('example.py', 'wt') as example:
example.write(textwrap.dedent("""\
class Foo:
def __init__(self):
pass
"""))

env.write_config(ignore_self_only_init=True, select="D107")
out, err, code = env.invoke()
assert '' == out
assert code == 0