diff --git a/docs/release_notes.rst b/docs/release_notes.rst index a9060ae..b3c490e 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -14,6 +14,7 @@ New Features * Add support for `property_decorators` config to ignore D401. * Add support for Python 3.10 (#554). * Replace D10X errors with D419 if docstring exists but is empty (#559). +* Add `ignore-functions` option to allow some functions to skip docstring checking (#587). Bug Fixes diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 0b45644..b735d35 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -136,6 +136,7 @@ def check_source( ignore_decorators=None, property_decorators=None, ignore_inline_noqa=False, + ignore_functions=None, ): self.property_decorators = ( {} if property_decorators is None else property_decorators @@ -150,9 +151,19 @@ def check_source( len(ignore_decorators.findall(dec.name)) > 0 for dec in definition.decorators ) + # Only skip checking the docstring if absent. + # If the docstring is present, then check it as normal + name_skip = ( + ignore_functions is not None + and not definition.docstring + and bool(ignore_functions.findall(definition.name)) + ) + if ( - ignore_inline_noqa or not skipping_all - ) and not decorator_skip: + (ignore_inline_noqa or not skipping_all) + and not decorator_skip + and not name_skip + ): error = this_check( self, definition, definition.docstring ) @@ -1088,6 +1099,7 @@ def check( ignore_decorators=None, property_decorators=None, ignore_inline_noqa=False, + ignore_functions=None, ): """Generate docstring errors that exist in `filenames` iterable. @@ -1141,9 +1153,10 @@ def check( for error in ConventionChecker().check_source( source, filename, - ignore_decorators, - property_decorators, - ignore_inline_noqa, + ignore_decorators=ignore_decorators, + property_decorators=property_decorators, + ignore_inline_noqa=ignore_inline_noqa, + ignore_functions=ignore_functions, ): code = getattr(error, 'code', None) if code in checked_codes: diff --git a/src/pydocstyle/cli.py b/src/pydocstyle/cli.py index 241894f..9024922 100644 --- a/src/pydocstyle/cli.py +++ b/src/pydocstyle/cli.py @@ -43,6 +43,7 @@ def run_pydocstyle(): checked_codes, ignore_decorators, property_decorators, + ignore_functions, ) in conf.get_files_to_check(): errors.extend( check( @@ -50,6 +51,7 @@ def run_pydocstyle(): select=checked_codes, ignore_decorators=ignore_decorators, property_decorators=property_decorators, + ignore_functions=ignore_functions, ) ) except IllegalConfiguration as error: diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index ed00c87..3e80c1b 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -180,6 +180,7 @@ class ConfigurationParser: 'match', 'match-dir', 'ignore-decorators', + 'ignore-functions', ) BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention') @@ -189,6 +190,7 @@ class ConfigurationParser: DEFAULT_PROPERTY_DECORATORS = ( "property,cached_property,functools.cached_property" ) + DEFAULT_IGNORE_FUNCTIONS_RE = '' DEFAULT_CONVENTION = conventions.pep257 PROJECT_CONFIG_FILES = ( @@ -263,6 +265,10 @@ def _get_matches(conf): match_dir_func = re(conf.match_dir + '$').match return match_func, match_dir_func + def _get_ignore_functions(conf): + """Return the `ignore_functions` as None or regex.""" + return re(conf.ignore_functions) if conf.ignore_functions else None + def _get_ignore_decorators(conf): """Return the `ignore_decorators` as None or regex.""" return ( @@ -284,6 +290,7 @@ def _get_property_decorators(conf): match, match_dir = _get_matches(config) ignore_decorators = _get_ignore_decorators(config) property_decorators = _get_property_decorators(config) + ignore_functions = _get_ignore_functions(config) # Skip any dirs that do not match match_dir dirs[:] = [d for d in dirs if match_dir(d)] @@ -296,18 +303,22 @@ def _get_property_decorators(conf): list(config.checked_codes), ignore_decorators, property_decorators, + ignore_functions, ) else: config = self._get_config(os.path.abspath(name)) match, _ = _get_matches(config) ignore_decorators = _get_ignore_decorators(config) property_decorators = _get_property_decorators(config) + ignore_functions = _get_ignore_functions(config) + if match(os.path.basename(name)): yield ( name, list(config.checked_codes), ignore_decorators, property_decorators, + ignore_functions, ) # --------------------------- Private Methods ----------------------------- @@ -509,6 +520,7 @@ def _merge_configuration(self, parent_config, child_options): 'match_dir', 'ignore_decorators', 'property_decorators', + 'ignore_functions', ): kwargs[key] = getattr(child_options, key) or getattr( parent_config, key @@ -548,6 +560,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_functions': "IGNORE_FUNCTIONS_RE", } for key, default in defaults.items(): kwargs[key] = ( @@ -780,9 +793,10 @@ def _create_option_parser(cls): OptionGroup( parser, 'Note', - 'When using --match, --match-dir or --ignore-decorators consider ' - 'whether you should use a single quote (\') or a double quote ("), ' - 'depending on your OS, Shell, etc.', + 'When using --match, --match-dir, --ignore-decorators or ' + '--ignore-functions consider whether you should use a single ' + 'quote (\') or a double quote ("), depending on your OS, ' + 'Shell, etc.', ) ) @@ -899,6 +913,19 @@ def _create_option_parser(cls): ), ) + # Function selection + option( + '--ignore-functions', + metavar='', + default=None, + help=( + "ignore any functions or methods whose names fit " + "the regular expression; default is " + "--ignore-functions='{}' which does not ignore any " + "functions.".format(cls.DEFAULT_IGNORE_DECORATORS_RE) + ), + ) + return parser @@ -911,6 +938,7 @@ def _create_option_parser(cls): 'match_dir', 'ignore_decorators', 'property_decorators', + 'ignore_functions', ), ) diff --git a/src/tests/test_cases/functions.py b/src/tests/test_cases/functions.py index db7b9fa..5b03094 100644 --- a/src/tests/test_cases/functions.py +++ b/src/tests/test_cases/functions.py @@ -75,3 +75,21 @@ class inner(): def func_with_weird_backslash(): """Test a function with a weird backslash.\ """ + + +def ignored_function(): + # This function does not need a docstring + # because the name matches "ignored_function.*" + pass + + +def ignored_function_with_suffix(): + # This function does not need a docstring + # because the name matches "ignored_function.*" + pass + + +@expect('D103: Missing docstring in public function') +def missing_docstring_function(): + # This function should have a docstring. + pass diff --git a/src/tests/test_definitions.py b/src/tests/test_definitions.py index c23192f..a26fbde 100644 --- a/src/tests/test_definitions.py +++ b/src/tests/test_definitions.py @@ -44,6 +44,7 @@ def test_complex_file(test_case): select=set(ErrorRegistry.get_error_codes()), ignore_decorators=re.compile('wraps|ignored_decorator'), property_decorators=DEFAULT_PROPERTY_DECORATORS, + ignore_functions=re.compile("ignored_function.*"), ) ) for error in results: