Skip to content
This repository was 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 all 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
7 changes: 7 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Release Notes
**pydocstyle** version numbers follow the
`Semantic Versioning <http://semver.org/>`_ specification.

6.3.0 - January 17th, 2023
--------------------------

New Features

* Add `ignore-self-only-init` config (#560).

6.2.3 - January 8th, 2023
---------------------------

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 @@ -136,10 +136,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 @@ -199,22 +201,27 @@ 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:
codes = {
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 @@ -1102,6 +1109,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 @@ -1121,6 +1129,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 @@ -1158,6 +1168,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 @@ -185,6 +185,7 @@ class ConfigurationParser:
'match',
'match-dir',
'ignore-decorators',
'ignore-self-only-init',
)
BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention')

Expand All @@ -195,6 +196,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 @@ -301,6 +303,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 @@ -313,6 +316,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 @@ -514,9 +518,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 @@ -553,6 +561,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 @@ -849,6 +858,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 @@ -916,6 +931,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',
'callable_args',
'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.callable_args


class NestedFunction(Function):
"""A Python source code nested function."""
Expand Down Expand Up @@ -666,8 +672,10 @@ def parse_definition(self, class_):
name = self.current.value
self.log.debug("parsing %s '%s'", class_.__name__, name)
self.stream.move()
callable_args = []
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 @@ -676,6 +684,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
):
callable_args.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 @@ -712,7 +729,8 @@ def parse_definition(self, class_):
decorators,
docstring,
children,
None,
callable_args,
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 @@ -182,10 +182,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
16 changes: 15 additions & 1 deletion src/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,20 @@ def test_comment_with_noqa_plus_docstring_file(env):
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

def test_match_considers_basenames_for_path_args(env):
"""Test that `match` option only considers basenames for path arguments.

Expand All @@ -1570,4 +1584,4 @@ def test_match_considers_basenames_for_path_args(env):
# env.invoke calls pydocstyle with full path to test_a.py
out, _, code = env.invoke(target='test_a.py')
assert '' == out
assert code == 0
assert code == 0