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

Commit

Permalink
Add ignore-self-only-init option (#560)
Browse files Browse the repository at this point in the history
* add `ignore-self-only-init` option

* docs

* callable_args

* fix

* Update release_notes.rst

* Update release_notes.rst

Co-authored-by: Sambhav Kothari <sambhavs.email@gmail.com>
  • Loading branch information
thejcannon and samj1912 committed Jan 17, 2023
1 parent d395b01 commit a9a73f9
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 17 deletions.
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

0 comments on commit a9a73f9

Please sign in to comment.