From d5a9841026122aec8f7dab0a5a4fe3d748cbe04d Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 15 Sep 2018 12:39:36 +0300 Subject: [PATCH] Version 0.0.13 release (#125) --- CHANGELOG.md | 16 +++ README.md | 4 +- pyproject.lock | 15 +-- pyproject.toml | 3 +- setup.cfg | 1 + tests/test_visitors/conftest.py | 22 ++--- .../test_jones/test_line_complexity.py | 98 +++++++++++++++++++ .../test_jones/test_module_complexity.py | 43 ++++++++ wemake_python_styleguide/checker.py | 5 + wemake_python_styleguide/errors/complexity.py | 77 ++++++++++++++- wemake_python_styleguide/logics/nodes.py | 26 +++++ wemake_python_styleguide/options/config.py | 25 ++++- wemake_python_styleguide/options/defaults.py | 6 ++ wemake_python_styleguide/types.py | 2 + .../visitors/ast/complexity/function.py | 6 +- .../visitors/ast/complexity/jones.py | 91 +++++++++++++++++ wemake_python_styleguide/visitors/base.py | 24 +++-- 17 files changed, 418 insertions(+), 46 deletions(-) create mode 100644 tests/test_visitors/test_ast/test_complexity/test_jones/test_line_complexity.py create mode 100644 tests/test_visitors/test_ast/test_complexity/test_jones/test_module_complexity.py create mode 100644 wemake_python_styleguide/logics/nodes.py create mode 100644 wemake_python_styleguide/visitors/ast/complexity/jones.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da02c2c2..7ad20e3b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ We follow Semantic Versions since the `0.1.0` release. We used to have incremental versioning before `0.1.0`. +## Version 0.0.13 aka The Jones Complexity + +This release is the last feature release before `0.1.0`. +However, there might be some supporting releases. + +### Features + +- Adds `jones` complexity checker +- Adds `--max-line-complexity` and `--max-jones-score` options + +### Misc + +- Improves docs: adds detailed installation instructions +- Removes `flake8-blind-except` plugin + + ## Version 0.0.12 This is just a supporting release. diff --git a/README.md b/README.md index 0f1b96547..aae10ae80 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ We are here not to: 1. Assume or check types, use `mypy` instead 2. Reformat code, since we believe that developers should do that -3. Suite everyone, this is *our* linter -4. Check for `SyntaxError`s or exceptions, write tests instead +3. Check for `SyntaxError`s or exceptions, write tests instead +4. Suite everyone, this is *our* linter ## Contributing diff --git a/pyproject.lock b/pyproject.lock index 0f85894d9..1e0356089 100644 --- a/pyproject.lock +++ b/pyproject.lock @@ -127,18 +127,6 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.0.0,<2.4.0" pyflakes = ">=1.5.0,<1.7.0" -[[package]] -category = "main" -description = "A flake8 extension that checks for blind except: statements" -name = "flake8-blind-except" -optional = false -platform = "*" -python-versions = "*" -version = "0.1.1" - -[package.dependencies] -setuptools = "*" - [[package]] category = "main" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." @@ -797,7 +785,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.23" [metadata] -content-hash = "98de8b21f5b2a1bcafe5a454c39f16a47e51980ebc85925b5c4e504eb73965bc" +content-hash = "e00b93239a7e2149bee5a8ed276c3f7972c68bb16426bbb9d25f3e613a89cf5d" platform = "*" python-versions = "^3.6 || ^3.7" @@ -814,7 +802,6 @@ coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] flake8 = ["7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"] -flake8-blind-except = ["0d7d1adb4cabf2268d6eebb815a7a5014bcb7e8419f7a74339c46d0b8847b858", "aca3356633825544cec51997260fe31a8f24a1a2795ce8e81696b9916745e599"] flake8-bugbear = ["07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83", "0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"] flake8-builtins = ["8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", "cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04"] flake8-coding = ["549c2b22c08711feda11795fb49f147a626305b602c547837bab405e7981f844", "f2ee7c3c8ae47f2d278111a2090655bcf170789c24ccfea519d93be2ede7571c"] diff --git a/pyproject.toml b/pyproject.toml index bfc02a95c..b32d435d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wemake-python-styleguide" -version = "0.0.12" +version = "0.0.13" description = "The most opinionated linter ever, used by wemake.services" license = "MIT" @@ -46,7 +46,6 @@ flake8-builtins = "^1.4" flake8-commas = "^2.0" flake8-quotes = "^1.0" flake8-comprehensions = "^1.4" -flake8-blind-except = "^0.1" flake8-docstrings = "^1.3" flake8-string-format = "^0.2" flake8-coding = "^1.3" diff --git a/setup.cfg b/setup.cfg index c5748103b..50904e550 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ filterwarnings = addopts = --doctest-modules --flake8 + --isort --cov=wemake_python_styleguide --cov-report=term:skip-covered --cov-report=html diff --git a/tests/test_visitors/conftest.py b/tests/test_visitors/conftest.py index c2ea882e1..9047c190c 100644 --- a/tests/test_visitors/conftest.py +++ b/tests/test_visitors/conftest.py @@ -7,11 +7,11 @@ import pytest -from wemake_python_styleguide.options import defaults +from wemake_python_styleguide.options.config import Configuration from wemake_python_styleguide.visitors.base import BaseNodeVisitor -def maybe_set_parent(tree: ast.AST) -> ast.AST: +def _maybe_set_parent(tree: ast.AST) -> ast.AST: """ Sets parents for all nodes that do not have this prop. @@ -31,11 +31,15 @@ def maybe_set_parent(tree: ast.AST) -> ast.AST: return tree +def _to_dest_option(long_option_name: str) -> str: + return long_option_name[2:].replace('-', '_') + + @pytest.fixture(scope='session') def parse_ast_tree(): """Helper function to convert code to ast.""" def factory(code: str) -> ast.AST: - return maybe_set_parent(ast.parse(dedent(code))) + return _maybe_set_parent(ast.parse(dedent(code))) return factory @@ -57,16 +61,8 @@ def factory(visitor: BaseNodeVisitor, errors: Sequence[str]): def options(): """Returns the options builder.""" default_values = { - 'max_arguments': defaults.MAX_ARGUMENTS, - 'max_expressions': defaults.MAX_EXPRESSIONS, - 'max_local_variables': defaults.MAX_LOCAL_VARIABLES, - 'max_returns': defaults.MAX_RETURNS, - 'min_variable_length': defaults.MIN_VARIABLE_LENGTH, - 'max_offset_blocks': defaults.MAX_OFFSET_BLOCKS, - 'max_elifs': defaults.MAX_ELIFS, - 'max_module_members': defaults.MAX_MODULE_MEMBERS, - 'max_methods': defaults.MAX_METHODS, - 'min_module_name_length': defaults.MIN_MODULE_NAME_LENGTH, + _to_dest_option(option.long_option_name): option.default + for option in Configuration.all_options() } Options = namedtuple('options', default_values.keys()) diff --git a/tests/test_visitors/test_ast/test_complexity/test_jones/test_line_complexity.py b/tests/test_visitors/test_ast/test_complexity/test_jones/test_line_complexity.py new file mode 100644 index 000000000..5bf601261 --- /dev/null +++ b/tests/test_visitors/test_ast/test_complexity/test_jones/test_line_complexity.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.ast.complexity.jones import ( + JonesComplexityVisitor, + LineComplexityViolation, +) + +line_simple = 'x = 2' +line_with_types = 'x: int = 2' +line_with_comprehension = 'x = [f for f in "abc"]' +line_with_math = 'x = y * 2 + 19 / 9.3' +line_inside_function = """ +def some(): + return 2 + 1 +""" + +line_inside_class = """ +class Some(): + field = 13 / 2 +""" + +function_declaration = 'def some(): ...' +class_declaration = 'class Some(object): ...' +empty_module = '' + + +@pytest.mark.parametrize('code', [ + line_simple, + line_with_types, + line_with_comprehension, + line_with_math, + line_inside_function, + line_inside_class, + function_declaration, + class_declaration, + empty_module, +]) +def test_regular_nodes(assert_errors, parse_ast_tree, code, default_options): + """Testing that regular nodes do not raise errors.""" + tree = parse_ast_tree(code) + + visitor = JonesComplexityVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('code', [ + line_simple, + line_inside_function, + line_inside_class, +]) +def test_complex_lines(assert_errors, parse_ast_tree, code, options): + """Testing that complex lines do raise errors.""" + tree = parse_ast_tree(code) + + option_values = options(max_line_complexity=1) + visitor = JonesComplexityVisitor(option_values, tree=tree) + visitor.run() + + assert_errors(visitor, [LineComplexityViolation]) + + +def test_same_complexity(parse_ast_tree, default_options): + """Ensures that complexity is counted correctly.""" + tree_without_types = parse_ast_tree(line_simple) + tree_with_types = parse_ast_tree(line_with_types) + + simple_visitor = JonesComplexityVisitor( + default_options, tree=tree_without_types, + ) + typed_visitor = JonesComplexityVisitor( + default_options, tree=tree_with_types, + ) + + simple_visitor.run() + typed_visitor.run() + + assert len(simple_visitor._lines) == 1 + assert len(simple_visitor._lines[1]) == 3 + assert len(simple_visitor._lines[1]) == len(typed_visitor._lines[1]) + + +@pytest.mark.parametrize('code, complexity', [ + (line_with_comprehension, 6), + (line_with_math, 9), +]) +def test_exact_complexity(parse_ast_tree, default_options, code, complexity): + """Ensures that complexity is counted correctly.""" + tree = parse_ast_tree(code) + + visitor = JonesComplexityVisitor(default_options, tree=tree) + visitor.run() + + assert len(visitor._lines) == 1 + assert len(visitor._lines[1]) == complexity diff --git a/tests/test_visitors/test_ast/test_complexity/test_jones/test_module_complexity.py b/tests/test_visitors/test_ast/test_complexity/test_jones/test_module_complexity.py new file mode 100644 index 000000000..6b8c05c6f --- /dev/null +++ b/tests/test_visitors/test_ast/test_complexity/test_jones/test_module_complexity.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.ast.complexity.jones import ( + JonesComplexityVisitor, + JonesScoreViolation, +) + +module_without_nodes = '' +module_with_nodes = """ +some = 1 + 2 +other = [number for number in range(0, some)] +""" + + +@pytest.mark.parametrize('code', [ + module_without_nodes, + module_with_nodes, +]) +def test_module_score(assert_errors, parse_ast_tree, code, default_options): + """Testing that regular nodes do not raise errors.""" + tree = parse_ast_tree(code) + + visitor = JonesComplexityVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('code', [ + module_without_nodes, + module_with_nodes, +]) +def test_module_score_error(assert_errors, parse_ast_tree, code, options): + """Testing that regular nodes do raise errors.""" + tree = parse_ast_tree(code) + + option_values = options(max_jones_score=-1) + visitor = JonesComplexityVisitor(option_values, tree=tree) + visitor.run() + + assert_errors(visitor, [JonesScoreViolation]) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index 8fb5d2d1f..bcb2a79d0 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -20,6 +20,9 @@ from wemake_python_styleguide.visitors.ast.complexity.function import ( FunctionComplexityVisitor, ) +from wemake_python_styleguide.visitors.ast.complexity.jones import ( + JonesComplexityVisitor, +) from wemake_python_styleguide.visitors.ast.complexity.nested import ( NestedComplexityVisitor, ) @@ -70,6 +73,7 @@ OffsetVisitor, ModuleMembersVisitor, MethodMembersVisitor, + JonesComplexityVisitor, # Modules: WrongModuleNameVisitor, @@ -90,6 +94,7 @@ class Checker(object): config = Configuration() options: ConfigurationOptions + # Receive `logic_line` as the first argument to make this plugin logical def __init__(self, tree: Module, filename: str = constants.STDIN) -> None: """Creates new checker instance.""" self.tree = tree diff --git a/wemake_python_styleguide/errors/complexity.py b/wemake_python_styleguide/errors/complexity.py index 5393b2b2d..a97670112 100644 --- a/wemake_python_styleguide/errors/complexity.py +++ b/wemake_python_styleguide/errors/complexity.py @@ -22,7 +22,10 @@ """ -from wemake_python_styleguide.errors.base import ASTStyleViolation +from wemake_python_styleguide.errors.base import ( + ASTStyleViolation, + SimpleStyleViolation, +) class NestedFunctionViolation(ASTStyleViolation): @@ -324,3 +327,75 @@ class TooManyMethodsViolation(ASTStyleViolation): error_template = '{0} Found too many methods "{1}"' code = 'Z209' + + +class LineComplexityViolation(ASTStyleViolation): + """ + This rule forbids to have complex lines. + + We are using Jones Complexity algorithm to count complexity. + What is Jones Complexity? It is a simple yet power method to count + the number of ``ast`` nodes per line. + If the complexity of a single line is higher than a tresshold, + then an error is raised. + + What nodes do we count? All except the following: + + 1. modules + 2. function and classes, since they are checked differently + 3. type annotations, since they do not increase complexity + + Reasoning: + Having a complex line indicates that you somehow managed to put too + many logic inside a single line. + At some point in time you will no longer be able to understand + what this line means and what it does. + + Solution: + Split a single line into several lines: by creating new variables, + statements or functions. Note, this might trigger new complexity issues. + With this technique a single new node in a line might trigger a complex + refactoring process including several modules. + + See also: + https://github.com/Miserlou/JonesComplexity + + This rule is configurable with ``--max-line-complexity``. + + Note: + Returns Z210 as error code + + """ + + error_template = '{0} Found too complex line: {1}' + code = 'Z210' + + +class JonesScoreViolation(SimpleStyleViolation): + """ + This rule forbids to have modules with complex lines. + + We are using Jones Complexity algorithm to count module score. + See + :py:class:`~.LineComplexityViolation` for details of per-line-complexity. + How it is done: we count complexity per line + + Reasoning: + Having complex modules will decrease your code maintability. + + Solution: + Refactor the module contents. + + See also: + https://github.com/Miserlou/JonesComplexity + + This rule is configurable with ``--max-module-score``. + + Note: + Returns Z211 as error code + + """ + + should_use_text = False + error_template = '{0} Found module with high Jones score' + code = 'Z211' diff --git a/wemake_python_styleguide/logics/nodes.py b/wemake_python_styleguide/logics/nodes.py new file mode 100644 index 000000000..9c62732bc --- /dev/null +++ b/wemake_python_styleguide/logics/nodes.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +import ast +from typing import Iterable, Type + + +def is_subtype_of_any( + node: ast.AST, + to_check: Iterable[Type[ast.AST]], +) -> bool: + """ + Checks either the given node is subtype of any of the provided types. + + >>> import ast + >>> node = ast.parse('') # ast.Module + >>> is_subtype_of_any(node, [ast.Str, ast.Name]) + False + + >>> is_subtype_of_any(node, [ast.Module]) + True + + >>> is_subtype_of_any(node, []) + False + + """ + return any(isinstance(node, class_) for class_ in to_check) diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index e8cc1297f..802ab7a5d 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -69,10 +69,18 @@ class Configuration(object): - ``min-module-name-length`` - minimum required module's name length, defaults to :str:`wemake_python_styleguide.options.defaults.MIN_MODULE_NAME_LENGTH` + - ``max-line-complexity`` - maximum line complexity measured in number of + ``ast`` nodes per line, defaults to + :str:`wemake_python_styleguide.options.defaults.MAX_LINE_COMPLEXITY` + - ``max-jones-score`` - maximum Jones score for a module, which is equal + to the median of all lines complexity sum, defaults to + :str:`wemake_python_styleguide.options.defaults.MAX_JONES_SCORE` """ - def _all_options(self) -> Sequence[_Option]: + @classmethod + def all_options(cls) -> Sequence[_Option]: + """Returns a list of option values we use in this plugin.""" return [ _Option( '--max-returns', @@ -133,10 +141,21 @@ def _all_options(self) -> Sequence[_Option]: defaults.MIN_MODULE_NAME_LENGTH, "Minimum required module's name length", ), + + _Option( + '--max-line-complexity', + defaults.MAX_LINE_COMPLEXITY, + 'Maximum line complexity, measured in `ast` nodes.', + ), + + _Option( + '--max-jones-score', + defaults.MAX_JONES_SCORE, + 'Maximum median module complexity, based on sum of lines.', + ), ] def register_options(self, parser: OptionManager) -> None: """Registers options for our plugin.""" - options = self._all_options() - for option in options: + for option in self.all_options(): parser.add_option(**attr.asdict(option)) diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index 4a61d57d7..cf0fbe221 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -41,6 +41,12 @@ #: Maximum number of methods in a single class: MAX_METHODS = 7 +#: Maximum line complexity: +MAX_LINE_COMPLEXITY = 14 # 7 * 2, also almost guessed + +#: Maximum median module Jones complexity: +MAX_JONES_SCORE = 12 # this value was "guessed" based on existing source code + # Modules diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index 2421150ab..4e4feb01d 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -50,6 +50,8 @@ class or structure. max_elifs: int max_module_members: int max_methods: int + max_line_complexity: int + max_jones_score: int # Modules: min_module_name_length: int diff --git a/wemake_python_styleguide/visitors/ast/complexity/function.py b/wemake_python_styleguide/visitors/ast/complexity/function.py index cdb247504..25d57fcb3 100644 --- a/wemake_python_styleguide/visitors/ast/complexity/function.py +++ b/wemake_python_styleguide/visitors/ast/complexity/function.py @@ -18,7 +18,7 @@ class _ComplexityCounter(object): - """Helper class to encapsulate logics from the visitor.""" + """Helper class to encapsulate logic from the visitor.""" def __init__(self, delegate: 'FunctionComplexityVisitor') -> None: self.delegate = delegate @@ -133,10 +133,10 @@ class FunctionComplexityVisitor(BaseNodeVisitor): This includes: 1. Number of arguments - 2. Number of `return`s + 2. Number of `return` statements 3. Number of expressions 4. Number of local variables - 5. Number of `elif`s + 5. Number of `elif` branches """ diff --git a/wemake_python_styleguide/visitors/ast/complexity/jones.py b/wemake_python_styleguide/visitors/ast/complexity/jones.py new file mode 100644 index 000000000..6c38151d7 --- /dev/null +++ b/wemake_python_styleguide/visitors/ast/complexity/jones.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +""" +Jones Complexity to count inline complexity. + +Based on the original `jones-complexity` project: +https://github.com/Miserlou/JonesComplexity + +Original project is licensed under MIT. +""" + +import ast +from collections import defaultdict +from statistics import median +from typing import DefaultDict, List + +from wemake_python_styleguide.errors.complexity import ( + JonesScoreViolation, + LineComplexityViolation, +) +from wemake_python_styleguide.logics.nodes import is_subtype_of_any +from wemake_python_styleguide.visitors.base import BaseNodeVisitor + + +class JonesComplexityVisitor(BaseNodeVisitor): + """ + This visitor is used to find complex lines in the code. + + Calculates the number of AST nodes per line of code. + Also calculates the median nodes/line score. + Then compares these numbers to the given tressholds. + + Some nodes are ignored because there's no sense in analyzing them. + Some nodes like type annotations are not affecting line complexity, + so we do not count them. + """ + + _ignored_nodes = ( + ast.FunctionDef, + ast.ClassDef, + ) + + def __init__(self, *args, **kwargs) -> None: + """Initializes line number counter.""" + super().__init__(*args, **kwargs) + self._lines: DefaultDict[int, List[ast.AST]] = defaultdict(list) + self._to_ignore: List[ast.AST] = [] + + def _post_visit(self) -> None: + """ + Triggers after the whole module was processed. + + Checks each line for its complexity, compares it to the tresshold. + We also calculate the final Jones score for the whole module. + """ + for line_nodes in self._lines.values(): + complexity = len(line_nodes) + if complexity > self.options.max_line_complexity: + self.add_error(LineComplexityViolation( + line_nodes[0], text=str(complexity), + )) + + node_counts = [len(nodes) for nodes in self._lines.values()] + total_count = median(node_counts) if node_counts else 0 + if total_count > self.options.max_jones_score: + self.add_error(JonesScoreViolation()) + + def _maybe_ignore_child(self, node: ast.AST) -> bool: + if isinstance(node, ast.AnnAssign): # type: ignore + self._to_ignore.append(node.annotation) + + return node in self._to_ignore + + def visit(self, node: ast.AST) -> None: + """ + Visits all nodes, sums the number of nodes per line. + + Then calculates the median value of all line results. + + Raises: + JonesScoreViolation + LineComplexityViolation + + """ + line_number = getattr(node, 'lineno', None) + is_ignored = is_subtype_of_any(node, self._ignored_nodes) + if line_number is not None and not is_ignored: + if not self._maybe_ignore_child(node): + self._lines[line_number].append(node) + + self.generic_visit(node) diff --git a/wemake_python_styleguide/visitors/base.py b/wemake_python_styleguide/visitors/base.py index 7078728d2..a1cfc0cee 100644 --- a/wemake_python_styleguide/visitors/base.py +++ b/wemake_python_styleguide/visitors/base.py @@ -14,8 +14,8 @@ class BaseChecker(object): Attributes: tree: AST tree to be checked if any. - options: contains the options objects passed and parsed by `flake8`. - filename: filename passed by `flake8`. + options: contains the options objects passed and parsed by ``flake8``. + filename: filename passed by ``flake8``. errors: list of errors for the specific checker. """ @@ -46,20 +46,28 @@ class BaseNodeVisitor(ast.NodeVisitor, BaseChecker): """ This class allows to store errors while traversing node tree. - This class should be used as a base class for all `ast`-based checkers. - Method `visit()` is defined in `NodeVisitor` class. + This class should be used as a base class for all ``ast`` based checkers. + Method ``visit()`` is defined in ``NodeVisitor`` class. """ + def _post_visit(self) -> None: + """ + This method is executed after all nodes have been visited. + + By default, does nothing. + """ + def run(self) -> None: - """Runs `visit()` method of `NodeVisitor` with the correct params.""" + """Runs the checking process.""" if self.tree is None: - raise ValueError('Parsing without a defined trie') + raise ValueError('Parsing without a defined tree') self.visit(self.tree) + self._post_visit() class BaseFilenameVisitor(BaseChecker): """ - This class allows to check module filenames. + This class allows to check module file names. Method `visit()` is used only for API compatibility. """ @@ -73,7 +81,7 @@ def run(self) -> None: Checks module's filename. If filename equals to ``STDIN`` constant then this check is ignored. - Otherwise, runs `visit_filename()` method. + Otherwise, runs ``visit_filename()`` method. """ if self.filename != constants.STDIN: self.visit_filename()