From 4dea0fde0f4cb2dd6297cbf6e0d66d5c8e669d3c Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 19 Feb 2020 23:43:15 +0300 Subject: [PATCH] Closes #1170 --- CHANGELOG.md | 8 +- docs/pages/usage/violations/index.rst | 1 - poetry.lock | 42 ++---- pyproject.toml | 1 - tests/fixtures/external_plugins.py | 6 - tests/fixtures/noqa/noqa.py | 5 +- tests/test_checker/test_noqa.py | 1 + tests/test_plugins.py | 1 - .../test_annotation_complexity_nesting.py | 125 ++++++++++++++++++ .../test_functions/test_positional_only.py | 6 +- .../logic/complexity/annotations.py | 42 ++++++ wemake_python_styleguide/options/config.py | 9 ++ wemake_python_styleguide/options/defaults.py | 13 +- .../options/validation.py | 1 + .../presets/topics/complexity.py | 3 + wemake_python_styleguide/types.py | 4 + .../violations/complexity.py | 33 +++++ .../visitors/ast/annotations.py | 14 +- .../visitors/ast/complexity/annotations.py | 72 ++++++++++ 19 files changed, 334 insertions(+), 53 deletions(-) create mode 100644 tests/test_visitors/test_ast/test_complexity/test_annotation_complexity/test_annotation_complexity_nesting.py create mode 100644 wemake_python_styleguide/logic/complexity/annotations.py create mode 100644 wemake_python_styleguide/visitors/ast/complexity/annotations.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 52dc76311..c81d80a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Semantic versioning in our case means: ### Features - **Breaking**: removes `flake8-print`, now using `WPS421` instead of `T001` +- **Breaking**: removes `flake8-annotations-complexity`, + now using `WPS234` instead of `TAE002` - Adds `python3.8` support - Removes `radon`, because `cognitive-complexity` is enough - Removes `flake8-loggin-format` as a direct dependency @@ -22,7 +24,9 @@ Semantic versioning in our case means: - Adds support for positional arguments in different checks - Changes `styleguide.toml` and `flake8.toml` scripts definition - Extracts new violation - WPS450 from WPS436 #1118 -- Adds domain names options, that are used to create variable names' blacklist #1106 +- Adds domain names options: + `--allowed-domain-names` and `--forbidden-domain-names`, + that are used to create variable names' blacklist #1106 - Forbids to use `:=` operator - Forbids to use positional only `/` arguments - Adds `__call__` to list of methods that should be on top #1125 @@ -34,6 +38,8 @@ Semantic versioning in our case means: - Fixes that cognitive complexity was ignoring `ast.Continue`, `ast.Break`, and `ast.Raise` statements - Fixes that cognitive complexity was ignoring `ast.AsyncFor` loops +- Fixes that annotation complexity was not reported for `async` functions +- Fixes that annotation complexity was not reported from lists ### Misc diff --git a/docs/pages/usage/violations/index.rst b/docs/pages/usage/violations/index.rst index 256d031bb..77a13e152 100644 --- a/docs/pages/usage/violations/index.rst +++ b/docs/pages/usage/violations/index.rst @@ -46,7 +46,6 @@ flake8-quotes `Q000 ` flake8-pep3101 `S001 `_ flake8-bandit `S100 - S710 `_, see also original ``bandit`` `codes `_ flake8-debugger `T100 `_ -flake8-annotations-complexity `TAE002 `_ flake8-rst-docstrings `RST201 - RST499 `_ flake8-executable `EXE001 - EXE005 `_ darglint `DAR001 - DAR501 `_ diff --git a/poetry.lock b/poetry.lock index 33985f418..ee97bd542 100644 --- a/poetry.lock +++ b/poetry.lock @@ -296,17 +296,6 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.5.0,<2.6.0" pyflakes = ">=2.1.0,<2.2.0" -[[package]] -category = "main" -description = "A flake8 extension that checks for type annotations complexity" -name = "flake8-annotations-complexity" -optional = false -python-versions = "*" -version = "0.0.2" - -[package.dependencies] -setuptools = "*" - [[package]] category = "main" description = "Automated security testing with bandit and flake8." @@ -808,12 +797,12 @@ description = "A lightweight library for converting complex datatypes to and fro name = "marshmallow" optional = false python-versions = ">=3.5" -version = "3.4.0" +version = "3.5.0" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.3)", "pre-commit (>=1.20,<3.0)", "tox"] -docs = ["sphinx (2.3.1)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"] -lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.3)", "pre-commit (>=1.20,<3.0)"] +dev = ["pytest", "pytz", "simplejson", "mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"] +docs = ["sphinx (2.4.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"] +lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -1025,7 +1014,7 @@ wcwidth = "*" [[package]] category = "dev" description = "Run a subprocess in a pseudo terminal" -marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\" or python_version >= \"3.4\" and sys_platform != \"win32\" and (python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\")" +marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\"" name = "ptyprocess" optional = false python-versions = "*" @@ -1294,7 +1283,7 @@ description = "Python documentation generator" name = "sphinx" optional = false python-versions = ">=3.5" -version = "2.4.1" +version = "2.4.2" [package.dependencies] Jinja2 = ">=2.3" @@ -1434,7 +1423,7 @@ description = "A collection of helpers and mock objects for unit tests and doc t name = "testfixtures" optional = false python-versions = "*" -version = "6.12.1" +version = "6.13.0" [package.extras] build = ["setuptools-git", "wheel", "twine"] @@ -1553,7 +1542,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "486efd9827f52641b797ea34c72278dd624c057bed42be2329ebd260b17336e2" +content-hash = "b3f9bf85e70ac4472e035ae034faef5cc36543e52ffadf59288bde1ebfaf97a1" python-versions = "^3.6" [metadata.files] @@ -1695,9 +1684,6 @@ flake8 = [ {file = "flake8-3.7.9-py2.py3-none-any.whl", hash = "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"}, {file = "flake8-3.7.9.tar.gz", hash = "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb"}, ] -flake8-annotations-complexity = [ - {file = "flake8_annotations_complexity-0.0.2.tar.gz", hash = "sha256:e499d2186efcc5f6f2f1c7eb18568d08f4d9313fad15f614645f3f445f70b45c"}, -] flake8-bandit = [ {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, ] @@ -1872,8 +1858,8 @@ markupsafe = [ {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] marshmallow = [ - {file = "marshmallow-3.4.0-py2.py3-none-any.whl", hash = "sha256:7669b944d6233b81f68739d5826f1176c3841cc31cf6b856841083b5a72f5ca9"}, - {file = "marshmallow-3.4.0.tar.gz", hash = "sha256:c9d277f6092f32300395fb83d343be9f61b5e99d66d22bae1e5e7cd82608fee6"}, + {file = "marshmallow-3.5.0-py2.py3-none-any.whl", hash = "sha256:4b95c7735f93eb781dfdc4dded028108998cad759dda8dd9d4b5b4ac574cbf13"}, + {file = "marshmallow-3.5.0.tar.gz", hash = "sha256:3a94945a7461f2ab4df9576e51c97d66bee2c86155d3d3933fab752b31effab8"}, ] marshmallow-polyfield = [ {file = "marshmallow-polyfield-5.8.tar.gz", hash = "sha256:1af6699f80be03766169a021860bdb8113641c329148bae4b4d5c485229be75c"}, @@ -2071,8 +2057,8 @@ sortedcontainers = [ {file = "sortedcontainers-2.1.0.tar.gz", hash = "sha256:974e9a32f56b17c1bac2aebd9dcf197f3eb9cd30553c5852a3187ad162e1a03a"}, ] sphinx = [ - {file = "Sphinx-2.4.1-py3-none-any.whl", hash = "sha256:5024a67f065fe60d9db2005580074d81f22a02dd8f00a5b1ec3d5f4d42bc88d8"}, - {file = "Sphinx-2.4.1.tar.gz", hash = "sha256:f929b72e0cfe45fa581b8964d54457117863a6a6c9369ecc1a65b8827abd3bf2"}, + {file = "Sphinx-2.4.2-py3-none-any.whl", hash = "sha256:543d39db5f82d83a5c1aa0c10c88f2b6cff2da3e711aa849b2c627b4b403bbd9"}, + {file = "Sphinx-2.4.2.tar.gz", hash = "sha256:525527074f2e0c2585f68f73c99b4dc257c34bbe308b27f5f8c7a6e20642742f"}, ] sphinx-autodoc-typehints = [ {file = "sphinx-autodoc-typehints-1.10.3.tar.gz", hash = "sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0"}, @@ -2113,8 +2099,8 @@ termcolor = [ {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, ] testfixtures = [ - {file = "testfixtures-6.12.1-py2.py3-none-any.whl", hash = "sha256:0a8a369dba5e01fe6b8da9300379d60fe62094536c8d971b559ec8167ab1fce3"}, - {file = "testfixtures-6.12.1.tar.gz", hash = "sha256:fb42846633b159e38f2c7ef2056818e9f15ee9689f5b0a8a88b4775957853048"}, + {file = "testfixtures-6.13.0-py2.py3-none-any.whl", hash = "sha256:13ed160824e0eff537cbda9c7cb3c3165445e5cfa1e8eaea67cc92b9b4aa8f86"}, + {file = "testfixtures-6.13.0.tar.gz", hash = "sha256:2c2e772e294aef07230781fda8cc83c608d28840a46dcd2bb3c40360caf81068"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, diff --git a/pyproject.toml b/pyproject.toml index 4a2742967..6cbc41eed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,6 @@ flake8-isort = "^2.6" flake8-eradicate = "^0.2" flake8-bandit = "^2.1" flake8-broken-line = "^0.1" -flake8-annotations-complexity = "^0.0.2" flake8-rst-docstrings = "^0.0.12" flake8-executable = "^2.0" pep8-naming = "^0.9.1" diff --git a/tests/fixtures/external_plugins.py b/tests/fixtures/external_plugins.py index 97bfc131e..9aa69c199 100644 --- a/tests/fixtures/external_plugins.py +++ b/tests/fixtures/external_plugins.py @@ -33,12 +33,6 @@ def function_name(plugin: str ='flake8') ->str: '\'' -def complex_annotation( - first: List[Union[List[str], Dict[str, Dict[str, str]]]], -): - ... - - def darglint_check(arg): """ Used to trigger DAR101. diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py index b174541cb..8bc1af1ec 100644 --- a/tests/fixtures/noqa/noqa.py +++ b/tests/fixtures/noqa/noqa.py @@ -7,6 +7,8 @@ from __future__ import print_function # noqa: WPS422 +from typing import List + import os.path # noqa: WPS301 import sys as sys # noqa: WPS113 @@ -19,7 +21,6 @@ import import2 import import3 import import4 -import import5 from some_name import ( name1, @@ -684,6 +685,8 @@ def consecutive_yields(): deep_func(a)(b)(c)(d) # noqa: WPS233 +annotated: List[List[List[List[int]]]] # noqa: WPS234 + extra_new_line = [ # noqa: WPS355 'wrong', diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py index 86f1788f7..4a1afba6e 100644 --- a/tests/test_checker/test_noqa.py +++ b/tests/test_checker/test_noqa.py @@ -103,6 +103,7 @@ 'WPS231': 1, 'WPS232': 0, # logically unacceptable. 'WPS233': 1, + 'WPS234': 1, 'WPS300': 1, 'WPS301': 1, diff --git a/tests/test_plugins.py b/tests/test_plugins.py index cc484d466..361bdea11 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -31,7 +31,6 @@ 'S001', # flake8-pep3101 'S101', # flake8-bandit 'T100', # flake8-debugger - 'TAE002', # flake8-annotations-complexity 'RST215', # flake8-rst-docstrings 'EXE003', # flake8-executable 'DAR101', # darglint diff --git a/tests/test_visitors/test_ast/test_complexity/test_annotation_complexity/test_annotation_complexity_nesting.py b/tests/test_visitors/test_ast/test_complexity/test_annotation_complexity/test_annotation_complexity_nesting.py new file mode 100644 index 000000000..89649d50a --- /dev/null +++ b/tests/test_visitors/test_ast/test_complexity/test_annotation_complexity/test_annotation_complexity_nesting.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.violations.complexity import ( + TooComplexAnnotationViolation, +) +from wemake_python_styleguide.visitors.ast.complexity.annotations import ( + AnnotationComplexityVisitor, +) + +annassign_template = 'some: {0}' + +function_arg_template = """ +def some(arg: {0}): + ... +""" + +function_return_template = """ +def some(arg) -> {0}: + ... +""" + +class_field_template = """ +class Test(object): + some: {0} + other = 1 +""" + + +@pytest.mark.parametrize('template', [ + annassign_template, + function_arg_template, + function_return_template, + class_field_template, +]) +@pytest.mark.parametrize('code', [ + 'int', + 'List[int]', + 'List["MyType"]', + '"List[MyType]"', + 'Dict[int, str]', + 'Callable[[str, int], int]', + 'List[List[int]]', +]) +def test_correct_annotations( + assert_errors, + parse_ast_tree, + template, + code, + mode, + default_options, +): + """Testing that expressions with correct call chain length work well.""" + tree = parse_ast_tree(mode(template.format(code))) + + visitor = AnnotationComplexityVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('template', [ + annassign_template, + function_arg_template, + function_return_template, + class_field_template, +]) +@pytest.mark.parametrize('code', [ + 'List[List[List[int]]]', + '"List[List[List[int]]]"', + + 'Callable[[], "List[List[str]]"]', + 'Callable[[List["List[str]"]], str]', + + 'Dict[int, Tuple[List[List[str]], ...]]', + '"Dict[int, Tuple[List[List[str]], ...]]"', + 'Dict[int, "Tuple[List[List[str]], ...]"]', + 'Dict[int, Tuple["List[List[str]]", ...]]', + 'Dict[int, Tuple[List["List[str]"], ...]]', +]) +def test_complex_annotations( + assert_errors, + parse_ast_tree, + template, + code, + mode, + default_options, +): + """Testing that expressions with correct call chain length work well.""" + tree = parse_ast_tree(mode(template.format(code))) + + visitor = AnnotationComplexityVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, [TooComplexAnnotationViolation]) + + +@pytest.mark.parametrize('template', [ + annassign_template, + function_arg_template, + function_return_template, + class_field_template, +]) +@pytest.mark.parametrize('code', [ + 'List[List[int]]', + '"List[List[int]]"', + 'List["List[int]"]', +]) +def test_complex_annotations_config( + assert_errors, + parse_ast_tree, + template, + code, + mode, + options, +): + """Testing that expressions with correct call chain length work well.""" + tree = parse_ast_tree(mode(template.format(code))) + + option_values = options(max_annotation_complexity=2) + visitor = AnnotationComplexityVisitor(option_values, tree=tree) + visitor.run() + + assert_errors(visitor, [TooComplexAnnotationViolation]) diff --git a/tests/test_visitors/test_ast/test_functions/test_positional_only.py b/tests/test_visitors/test_ast/test_functions/test_positional_only.py index a62596796..e51f5cb03 100644 --- a/tests/test_visitors/test_ast/test_functions/test_positional_only.py +++ b/tests/test_visitors/test_ast/test_functions/test_positional_only.py @@ -63,10 +63,11 @@ def test_not_posonlyargs( assert_errors, parse_ast_tree, code, + mode, default_options, ): """Testing that regular code is allowed.""" - tree = parse_ast_tree(code) + tree = parse_ast_tree(mode(code)) visitor = PositionalOnlyArgumentsVisitor(default_options, tree=tree) visitor.run() @@ -88,10 +89,11 @@ def test_posonyargs( assert_errors, parse_ast_tree, code, + mode, default_options, ): """Testing that ``/`` is not allowed.""" - tree = parse_ast_tree(code) + tree = parse_ast_tree(mode(code)) visitor = PositionalOnlyArgumentsVisitor(default_options, tree=tree) visitor.run() diff --git a/wemake_python_styleguide/logic/complexity/annotations.py b/wemake_python_styleguide/logic/complexity/annotations.py new file mode 100644 index 000000000..fcfe6c7d5 --- /dev/null +++ b/wemake_python_styleguide/logic/complexity/annotations.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +""" +Counts annotation complexity by getting the nesting level of nodes. + +So ``List[int]`` complexity is 2 +and ``Tuple[List[Optional[str]], int]`` is 4. + +Adapted from: https://github.com/best-doctor/flake8-annotations-complexity +""" + +import ast +from typing import Union + +_Annotation = Union[ + ast.expr, + ast.Str, +] + + +def get_annotation_compexity(annotation_node: _Annotation) -> int: + """ + Recursevly counts complexity of annotation nodes. + + When annotations are written as strings, + we additionally parse them to ``ast`` nodes. + """ + if isinstance(annotation_node, ast.Str): + annotation_node = ast.parse( # type: ignore + annotation_node.s, + ).body[0].value + + if isinstance(annotation_node, ast.Subscript): + return 1 + get_annotation_compexity( + annotation_node.slice.value, # type: ignore + ) + elif isinstance(annotation_node, (ast.Tuple, ast.List)): + return max( + (get_annotation_compexity(node) for node in annotation_node.elts), + default=1, + ) + return 1 diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index 1cdecf110..7670dedc1 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -130,6 +130,9 @@ :str:`wemake_python_styleguide.options.defaults.NESTED_CLASSES_WHITELIST` - ``max-call-level`` - maximum number of call chains, defaults to :str:`wemake_python_styleguide.options.defaults.MAX_CALL_LEVEL` +- ``max-annotation-complexity`` - maximum number of nested annotations, + defaults to + :str:`wemake_python_styleguide.options.defaults.MAX_ANN_COMPLEXITY` """ @@ -374,6 +377,12 @@ class Configuration(object): defaults.MAX_CALL_LEVEL, 'Maximum number of call chains.', ), + + _Option( + '--max-annotation-complexity', + defaults.MAX_ANN_COMPLEXITY, + 'Maximum number of nested annotations.', + ), ] def register_options(self, parser: OptionManager) -> None: diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index c5c3a164b..6091a5d59 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -38,6 +38,12 @@ 'Params', # factoryboy specific ) +#: Domain names that are removed from variable names' blacklist. +ALLOWED_DOMAIN_NAMES: Final = () + +#: Domain names that extends variable names' blacklist. +FORBIDDEN_DOMAIN_NAMES: Final = () + # =========== # Complexity: @@ -112,8 +118,5 @@ #: Maximum number of call chains. MAX_CALL_LEVEL: Final = 3 -#: Domain names that are removed from variable names' blacklist. -ALLOWED_DOMAIN_NAMES: Final = () - -#: Domain names that extends variable names' blacklist. -FORBIDDEN_DOMAIN_NAMES: Final = () +#: Maximum number of nested annotations. +MAX_ANN_COMPLEXITY: Final = 3 diff --git a/wemake_python_styleguide/options/validation.py b/wemake_python_styleguide/options/validation.py index e91a33805..fc5cf50b6 100644 --- a/wemake_python_styleguide/options/validation.py +++ b/wemake_python_styleguide/options/validation.py @@ -92,6 +92,7 @@ class _ValidatedOptions(object): max_cognitive_score: int = attr.ib(validator=[_min_max(min=1)]) max_cognitive_average: int = attr.ib(validator=[_min_max(min=1)]) max_call_level: int = attr.ib(validator=[_min_max(min=1)]) + max_annotation_complexity: int = attr.ib(validator=[_min_max(min=2)]) def validate_options(options: ConfigurationOptions) -> _ValidatedOptions: diff --git a/wemake_python_styleguide/presets/topics/complexity.py b/wemake_python_styleguide/presets/topics/complexity.py index 5e20c5734..3d12211e6 100644 --- a/wemake_python_styleguide/presets/topics/complexity.py +++ b/wemake_python_styleguide/presets/topics/complexity.py @@ -4,6 +4,7 @@ from wemake_python_styleguide.visitors.ast.complexity import ( access, + annotations, calls, classes, counts, @@ -41,4 +42,6 @@ access.AccessVisitor, calls.CallChainsVisitor, + + annotations.AnnotationComplexityVisitor, ) diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index af29fee7f..ecf47da75 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -221,3 +221,7 @@ def max_cognitive_average(self) -> int: @property def max_call_level(self) -> int: ... + + @property + def max_annotation_complexity(self) -> int: + ... diff --git a/wemake_python_styleguide/violations/complexity.py b/wemake_python_styleguide/violations/complexity.py index a5dba5d19..a10cbc75b 100644 --- a/wemake_python_styleguide/violations/complexity.py +++ b/wemake_python_styleguide/violations/complexity.py @@ -1058,3 +1058,36 @@ class TooLongCallChainViolation(ASTViolation): error_template = 'Found too lang call chain length: {0}' code = 233 + + +@final +class TooComplexAnnotationViolation(ASTViolation): + """ + Forbids too complex annotations. + + Annotation complexity is maximum annotation nesting level. + Example: ``List[int]`` has complexity of 2 + and ``Tuple[List[Optional[str]], int]`` has complexity of 4. + + Reasoning: + Too complex annotations make your types unreadable. + And make developers afraid of types. + + Solution: + Create type aliases. And use them a lot! + + Configuration: + This rule is configurable with ``--max-annotation-complexity``. + Default: + :str:`wemake_python_styleguide.options.defaults.MAX_ANN_COMPLEXITY` + + See also: + https://mypy.readthedocs.io/en/stable/kinds_of_types.html#type-aliases + https://github.com/best-doctor/flake8-annotations-complexity + + .. versionadded:: 0.14.0 + + """ + + error_template = 'Found too complex annotation: {0}' + code = 234 diff --git a/wemake_python_styleguide/visitors/ast/annotations.py b/wemake_python_styleguide/visitors/ast/annotations.py index 52e0f5d7b..b2b6e6096 100644 --- a/wemake_python_styleguide/visitors/ast/annotations.py +++ b/wemake_python_styleguide/visitors/ast/annotations.py @@ -42,13 +42,6 @@ def visit_arg(self, node: ast.arg) -> None: self._check_arg_annotation(node) self.generic_visit(node) - def _check_arg_annotation(self, node: ast.arg) -> None: - for sub_node in ast.walk(node): - lineno = getattr(sub_node, 'lineno', None) - if lineno and lineno != node.lineno: - self.add_violation(MultilineFunctionAnnotationViolation(node)) - return - def _check_return_annotation(self, node: AnyFunctionDef) -> None: if not node.returns: return @@ -58,3 +51,10 @@ def _check_return_annotation(self, node: AnyFunctionDef) -> None: if lineno and lineno != node.returns.lineno: self.add_violation(MultilineFunctionAnnotationViolation(node)) return + + def _check_arg_annotation(self, node: ast.arg) -> None: + for sub_node in ast.walk(node): + lineno = getattr(sub_node, 'lineno', None) + if lineno and lineno != node.lineno: + self.add_violation(MultilineFunctionAnnotationViolation(node)) + return diff --git a/wemake_python_styleguide/visitors/ast/complexity/annotations.py b/wemake_python_styleguide/visitors/ast/complexity/annotations.py new file mode 100644 index 000000000..3b780b20b --- /dev/null +++ b/wemake_python_styleguide/visitors/ast/complexity/annotations.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +import ast +from typing import List + +from typing_extensions import final + +from wemake_python_styleguide.logic.complexity.annotations import ( + get_annotation_compexity, +) +from wemake_python_styleguide.types import AnyFunctionDef +from wemake_python_styleguide.violations.complexity import ( + TooComplexAnnotationViolation, +) +from wemake_python_styleguide.visitors.base import BaseNodeVisitor +from wemake_python_styleguide.visitors.decorators import alias + + +@final +@alias('visit_any_function', ( + 'visit_FunctionDef', + 'visit_AsyncFunctionDef', +)) +class AnnotationComplexityVisitor(BaseNodeVisitor): + """Ensures that annotations are used correctly.""" + + def visit_any_function(self, node: AnyFunctionDef) -> None: + """ + Checks return type annotations. + + Raises: + TooComplexAnnotationViolation + + """ + self._check_function_annotations_complexity(node) + self.generic_visit(node) + + def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + """ + Check assignment annotation. + + Raises: + TooComplexAnnotationViolation + + """ + self._check_annotations_complexity(node, [node.annotation]) + self.generic_visit(node) + + def _check_function_annotations_complexity( + self, node: AnyFunctionDef, + ) -> None: + annotations = [ + arg.annotation + for arg in node.args.args + if arg.annotation is not None + ] + if node.returns is not None: + annotations.append(node.returns) + self._check_annotations_complexity(node, annotations) + + def _check_annotations_complexity( + self, + node: ast.AST, + annotations: List[ast.expr], + ) -> None: + max_complexity = self.options.max_annotation_complexity + for annotation in annotations: + complexity = get_annotation_compexity(annotation) + if complexity > max_complexity: + self.add_violation( + TooComplexAnnotationViolation(node, text=str(complexity)), + )