From 6637beeacd311ff8b9cc5866836c4106b96ea3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar=20Rubio?= Date: Tue, 13 Sep 2022 20:14:00 +0200 Subject: [PATCH 1/2] Allow to run with all Python optimizations enabled --- src/pydocstyle/checker.py | 271 +++++++++++++++++----------------- src/tests/test_integration.py | 28 ++++ 2 files changed, 163 insertions(+), 136 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 0b45644..871d3ea 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -2,6 +2,7 @@ import ast import string +import sys import tokenize as tk from collections import namedtuple from itertools import chain, takewhile @@ -36,6 +37,126 @@ __all__ = ('check',) +CHECKS_EXPLANATIONS = { + "check_docstring_missing": """D10{0,1,2,3}: Public definitions should have docstrings. + +All modules should normally have docstrings. [...] all functions and +classes exported by a module should also have docstrings. Public +methods (including the __init__ constructor) should also have +docstrings. + +Note: Public (exported) definitions are either those with names listed + in __all__ variable (if present), or those that do not start + with a single underscore. +""", + "check_docstring_empty": """D419: Docstring is empty. + +If the user provided a docstring but it was empty, it is like they never provided one. + +NOTE: This used to report as D10X errors. +""", + "check_one_liners": """D200: One-liner docstrings should fit on one line with quotes. + +The closing quotes are on the same line as the opening quotes. +This looks better for one-liners. +""", + "check_no_blank_before": """D20{1,2}: No blank lines allowed around function/method docstring. + +There's no blank line either before or after the docstring unless directly +followed by an inner function or class. +""", + "check_blank_before_after_class": """D20{3,4}: Class docstring should have 1 blank line around them. + +Insert a blank line before and after all docstrings (one-line or +multi-line) that document a class -- generally speaking, the class's +methods are separated from each other by a single blank line, and the +docstring needs to be offset from the first method by a blank line; +for symmetry, put a blank line between the class header and the +docstring. +""", + "check_blank_after_summary": """D205: Put one blank line between summary line and description. + +Multi-line docstrings consist of a summary line just like a one-line +docstring, followed by a blank line, followed by a more elaborate +description. The summary line may be used by automatic indexing tools; +it is important that it fits on one line and is separated from the +rest of the docstring by a blank line. +""", + "check_indent": """D20{6,7,8}: The entire docstring should be indented same as code. + +The entire docstring is indented the same as the quotes at its +first line. +""", + "check_newline_after_last_paragraph": """D209: Put multi-line docstring closing quotes on separate line. + +Unless the entire docstring fits on a line, place the closing +quotes on a line by themselves. +""", + "check_surrounding_whitespaces": "D210: No whitespaces allowed surrounding docstring text.", + "check_multi_line_summary_start": """D21{2,3}: Multi-line docstring summary style check. + +A multi-line docstring summary should start either at the first, +or separately at the second line of a docstring. +""", + "check_triple_double_quotes": r'''D300: Use """triple double quotes""". + +For consistency, always use """triple double quotes""" around +docstrings. Use r"""raw triple double quotes""" if you use any +backslashes in your docstrings. For Unicode docstrings, use +u"""Unicode triple-quoted strings""". + +Note: Exception to this is made if the docstring contains + """ quotes in its body. +''', + "check_backslashes": r'''D301: Use r""" if any backslashes in a docstring. + +Use r"""raw triple double quotes""" if you use any backslashes +(\) in your docstrings. + +Exceptions are backslashes for line-continuation and unicode escape +sequences \N... and \u... These are considered intended unescaped +content in docstrings. +''', + "check_ends_with_period": """D400: First line should end with a period. + +The [first line of a] docstring is a phrase ending in a period. +""", + "check_ends_with_punctuation": """D415: should end with proper punctuation. + +The [first line of a] docstring is a phrase ending in a period, +question mark, or exclamation point + +""", + "check_imperative_mood": """D401: First line should be in imperative mood: 'Do', not 'Does'. + +[Docstring] prescribes the function or method's effect as a command: +("Do this", "Return that"), not as a description; e.g. don't write +"Returns the pathname ...". +""", + "check_no_signature": """D402: First line should not be function's or method's "signature". + +The one-line docstring should NOT be a "signature" reiterating the +function/method parameters (which can be obtained by introspection). +""", + "check_capitalized": """D403: First word of the first line should be properly capitalized. + +The [first line of a] docstring is a phrase ending in a period. +""", + "check_if_needed": """D418: Function decorated with @overload shouldn't contain a docstring. + +Functions that are decorated with @overload are definitions, +and are for the benefit of the type checker only, +since they will be overwritten by the non-@overload-decorated definition. +""", + "check_starts_with_this": """D404: First word of the docstring should not be `This`. + +Docstrings should use short, simple language. They should not begin +with "This class is [..]" or "This module contains [..]". +""", + "check_docstring_sections": "Check for docstring sections.", +} + + def check_for(kind, terminal=False): def decorator(f): f._check_for = kind @@ -142,7 +263,7 @@ def check_source( ) module = parse(StringIO(source), filename) for definition in module: - for this_check in self.checks: + for this_check in self.checks(): terminate = False if isinstance(definition, this_check._check_for): skipping_all = definition.skipped_error_codes == 'all' @@ -164,7 +285,10 @@ def check_source( ignore_inline_noqa or error.code not in definition.skipped_error_codes ): - partition = this_check.__doc__.partition('.\n') + this_check_doc = CHECKS_EXPLANATIONS[ + this_check.__name__ + ] + partition = this_check_doc.partition('.\n') message, _, explanation = partition error.set_context( explanation=explanation, definition=definition @@ -176,29 +300,17 @@ def check_source( if terminate: break - @property - def checks(self): + @classmethod + def checks(cls): all = [ this_check - for this_check in vars(type(self)).values() + for this_check in vars(cls).values() if hasattr(this_check, '_check_for') ] return sorted(all, key=lambda this_check: not this_check._terminal) @check_for(Definition, terminal=True) def check_docstring_missing(self, definition, docstring): - """D10{0,1,2,3}: Public definitions should have docstrings. - - All modules should normally have docstrings. [...] all functions and - classes exported by a module should also have docstrings. Public - methods (including the __init__ constructor) should also have - docstrings. - - Note: Public (exported) definitions are either those with names listed - in __all__ variable (if present), or those that do not start - with a single underscore. - - """ if not docstring and definition.is_public: codes = { Module: violations.D100, @@ -227,24 +339,11 @@ def check_docstring_missing(self, definition, docstring): @check_for(Definition, terminal=True) def check_docstring_empty(self, definition, docstring): - """D419: Docstring is empty. - - If the user provided a docstring but it was empty, it is like they never provided one. - - NOTE: This used to report as D10X errors. - - """ if docstring and is_blank(ast.literal_eval(docstring)): return violations.D419() @check_for(Definition) def check_one_liners(self, definition, docstring): - """D200: One-liner docstrings should fit on one line with quotes. - - The closing quotes are on the same line as the opening quotes. - This looks better for one-liners. - - """ if docstring: lines = ast.literal_eval(docstring).split('\n') if len(lines) > 1: @@ -254,11 +353,6 @@ def check_one_liners(self, definition, docstring): @check_for(Function) def check_no_blank_before(self, function, docstring): # def - """D20{1,2}: No blank lines allowed around function/method docstring. - - There's no blank line either before or after the docstring unless directly - followed by an inner function or class. - """ if docstring: before, _, after = function.source.partition(docstring) blanks_before = list(map(is_blank, before.split('\n')[:-1])) @@ -279,16 +373,6 @@ def check_no_blank_before(self, function, docstring): # def @check_for(Class) def check_blank_before_after_class(self, class_, docstring): - """D20{3,4}: Class docstring should have 1 blank line around them. - - Insert a blank line before and after all docstrings (one-line or - multi-line) that document a class -- generally speaking, the class's - methods are separated from each other by a single blank line, and the - docstring needs to be offset from the first method by a blank line; - for symmetry, put a blank line between the class header and the - docstring. - - """ # NOTE: this gives false-positive in this case # class Foo: # @@ -312,15 +396,6 @@ def check_blank_before_after_class(self, class_, docstring): @check_for(Definition) def check_blank_after_summary(self, definition, docstring): - """D205: Put one blank line between summary line and description. - - Multi-line docstrings consist of a summary line just like a one-line - docstring, followed by a blank line, followed by a more elaborate - description. The summary line may be used by automatic indexing tools; - it is important that it fits on one line and is separated from the - rest of the docstring by a blank line. - - """ if docstring: lines = ast.literal_eval(docstring).strip().split('\n') if len(lines) > 1: @@ -338,12 +413,6 @@ def _get_docstring_indent(definition, docstring): @check_for(Definition) def check_indent(self, definition, docstring): - """D20{6,7,8}: The entire docstring should be indented same as code. - - The entire docstring is indented the same as the quotes at its - first line. - - """ if docstring: indent = self._get_docstring_indent(definition, docstring) lines = docstring.split('\n') @@ -366,12 +435,6 @@ def check_indent(self, definition, docstring): @check_for(Definition) def check_newline_after_last_paragraph(self, definition, docstring): - """D209: Put multi-line docstring closing quotes on separate line. - - Unless the entire docstring fits on a line, place the closing - quotes on a line by themselves. - - """ if docstring: lines = [ l @@ -384,7 +447,6 @@ def check_newline_after_last_paragraph(self, definition, docstring): @check_for(Definition) def check_surrounding_whitespaces(self, definition, docstring): - """D210: No whitespaces allowed surrounding docstring text.""" if docstring: lines = ast.literal_eval(docstring).split('\n') if ( @@ -396,12 +458,6 @@ def check_surrounding_whitespaces(self, definition, docstring): @check_for(Definition) def check_multi_line_summary_start(self, definition, docstring): - """D21{2,3}: Multi-line docstring summary style check. - - A multi-line docstring summary should start either at the first, - or separately at the second line of a docstring. - - """ if docstring: start_triple = [ '"""', @@ -424,17 +480,6 @@ def check_multi_line_summary_start(self, definition, docstring): @check_for(Definition) def check_triple_double_quotes(self, definition, docstring): - r'''D300: Use """triple double quotes""". - - For consistency, always use """triple double quotes""" around - docstrings. Use r"""raw triple double quotes""" if you use any - backslashes in your docstrings. For Unicode docstrings, use - u"""Unicode triple-quoted strings""". - - Note: Exception to this is made if the docstring contains - """ quotes in its body. - - ''' if docstring: if '"""' in ast.literal_eval(docstring): # Allow ''' quotes if docstring contains """, because @@ -451,15 +496,6 @@ def check_triple_double_quotes(self, definition, docstring): @check_for(Definition) def check_backslashes(self, definition, docstring): - r'''D301: Use r""" if any backslashes in a docstring. - - Use r"""raw triple double quotes""" if you use any backslashes - (\) in your docstrings. - - Exceptions are backslashes for line-continuation and unicode escape - sequences \N... and \u... These are considered intended unescaped - content in docstrings. - ''' # Just check that docstring is raw, check_triple_double_quotes # ensures the correct quotes. @@ -486,34 +522,16 @@ def _check_ends_with(docstring, chars, violation): @check_for(Definition) def check_ends_with_period(self, definition, docstring): - """D400: First line should end with a period. - - The [first line of a] docstring is a phrase ending in a period. - - """ return self._check_ends_with(docstring, '.', violations.D400) @check_for(Definition) def check_ends_with_punctuation(self, definition, docstring): - """D415: should end with proper punctuation. - - The [first line of a] docstring is a phrase ending in a period, - question mark, or exclamation point - - """ return self._check_ends_with( docstring, ('.', '!', '?'), violations.D415 ) @check_for(Function) def check_imperative_mood(self, function, docstring): # def context - """D401: First line should be in imperative mood: 'Do', not 'Does'. - - [Docstring] prescribes the function or method's effect as a command: - ("Do this", "Return that"), not as a description; e.g. don't write - "Returns the pathname ...". - - """ if ( docstring and not function.is_test @@ -538,12 +556,6 @@ def check_imperative_mood(self, function, docstring): # def context @check_for(Function) def check_no_signature(self, function, docstring): # def context - """D402: First line should not be function's or method's "signature". - - The one-line docstring should NOT be a "signature" reiterating the - function/method parameters (which can be obtained by introspection). - - """ if docstring: first_line = ast.literal_eval(docstring).strip().split('\n')[0] if function.name + '(' in first_line.replace(' ', ''): @@ -551,11 +563,6 @@ def check_no_signature(self, function, docstring): # def context @check_for(Function) def check_capitalized(self, function, docstring): - """D403: First word of the first line should be properly capitalized. - - The [first line of a] docstring is a phrase ending in a period. - - """ if docstring: first_word = ast.literal_eval(docstring).split()[0] if first_word == first_word.upper(): @@ -568,24 +575,11 @@ def check_capitalized(self, function, docstring): @check_for(Function) def check_if_needed(self, function, docstring): - """D418: Function decorated with @overload shouldn't contain a docstring. - - Functions that are decorated with @overload are definitions, - and are for the benefit of the type checker only, - since they will be overwritten by the non-@overload-decorated definition. - - """ if docstring and function.is_overload: return violations.D418() @check_for(Definition) def check_starts_with_this(self, function, docstring): - """D404: First word of the docstring should not be `This`. - - Docstrings should use short, simple language. They should not begin - with "This class is [..]" or "This module contains [..]". - - """ if not docstring: return @@ -1061,7 +1055,6 @@ def _check_google_sections(self, lines, definition, docstring): @check_for(Definition) def check_docstring_sections(self, definition, docstring): - """Check for docstring sections.""" if not docstring: return @@ -1078,6 +1071,12 @@ def check_docstring_sections(self, definition, docstring): ) +if sys.flags.optimize < 2: + # not using PYTHONOPTIMIZE=2, set checker methods docstrings + for this_check in ConventionChecker.checks(): + this_check.__doc__ = CHECKS_EXPLANATIONS[this_check.__name__] + + parse = Parser() diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index dbc6d31..1cc797c 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -270,6 +270,34 @@ def test_run_as_named_module(): assert p.returncode == 0, out.decode('utf-8') + err.decode('utf-8') +def test_run_with_python_optimizations_enabled(env): + """Test that pydocstyle can be run with the environment variable PYTHONOPTIMIZE=2. + + This means that the following should run pydocstyle: + + python -OO -m pydocstyle + + """ + test_content = textwrap.dedent("""\ + def foo(): + pass + """) + + with env.open('foo.py', 'wt') as test: + test.write(test_content) + + cmd = [sys.executable, "-OO", "-m", "pydocstyle", "foo.py"] + p = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=env.tempdir) + out, err = p.communicate() + + assert b"Missing docstring in public module" in out + assert b"Missing docstring in public function" in out + assert p.returncode == 1, out.decode('utf-8') + err.decode('utf-8') + + def test_config_file(env): """Test that options are correctly loaded from a config file. From 819f66af48d3439afe8e6f6570bfdd86c678de7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Mond=C3=A9jar=20Rubio?= Date: Tue, 13 Sep 2022 20:18:53 +0200 Subject: [PATCH 2/2] Add changelog entry --- docs/release_notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index a9060ae..2f33081 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -18,6 +18,7 @@ New Features Bug Fixes * Fix ``--match`` option to only consider filename when matching full paths (#550). +* Fix error raised when running with `PYTHONOPTIMIZE=2` environment variable (#607). 6.1.1 - May 17th, 2021 ---------------------------