├── .codeclimate.yml ├── .flake_master ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ ├── build_pr.yml │ └── publish.yml ├── .gitignore ├── .mdlrc ├── .mdlrc.rb ├── LICENSE ├── Makefile ├── README.md ├── flake8_annotations_complexity ├── __init__.py ├── ast_helpers.py └── checker.py ├── requirements_dev.txt ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── test_annotations_complexity.py └── test_files ├── dynamic_annotations.py ├── empty.py ├── empty_string.py ├── empty_tuple.py ├── pep_585.py ├── string_annotations.py ├── too_long_annotation.py ├── unannotated.py ├── var_annotation.py └── weird_annotations.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | file-lines: 4 | config: 5 | threshold: 500 6 | complex-logic: 7 | config: 8 | threshold: 8 9 | -------------------------------------------------------------------------------- /.flake_master: -------------------------------------------------------------------------------- 1 | {"name": "rose", "revision": "4", "url": "https://raw.githubusercontent.com/Melevir/flake_master_presets/master/presets/rose.cfg", "filepath": null} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test code 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.9', '3.10', '3.11'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: pip install -r requirements_dev.txt 21 | - name: Run mdl 22 | uses: actionshub/markdownlint@main 23 | - name: Run checks 24 | run: make style types requirements 25 | - name: Run tests and publish coverage 26 | uses: paambaati/codeclimate-action@v2.7.5 27 | env: 28 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 29 | with: 30 | coverageCommand: make coverage 31 | -------------------------------------------------------------------------------- /.github/workflows/build_pr.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test code 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.9', '3.10', '3.11'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: pip install -r requirements_dev.txt 21 | - name: Run mdl 22 | uses: actionshub/markdownlint@main 23 | - name: Run checks 24 | run: make style types requirements 25 | - name: Run tests 26 | run: make test 27 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ['3.9', '3.10', '3.11'] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: pip install -r requirements_dev.txt 22 | - name: Run checks 23 | run: make style types requirements test 24 | publish: 25 | runs-on: ubuntu-latest 26 | needs: build 27 | environment: 28 | name: pypi 29 | url: https://pypi.org/p/flake8-annotations-complexity 30 | permissions: 31 | id-token: write 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v5 36 | - name: Install dependencies 37 | run: pip install -r requirements_dev.txt 38 | - name: Build and publish 39 | run: | 40 | python setup.py sdist bdist_wheel 41 | - name: Publish package distributions to PyPI 42 | uses: pypa/gh-action-pypi-publish@release/v1 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE 107 | .idea 108 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | style '.mdlrc.rb' 2 | -------------------------------------------------------------------------------- /.mdlrc.rb: -------------------------------------------------------------------------------- 1 | all 2 | rule 'MD013', :line_length => 120 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 BestDoctor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python -m pytest 3 | 4 | coverage: 5 | python -m pytest --cov=flake8_annotations_complexity --cov-report=xml 6 | 7 | types: 8 | mypy . 9 | 10 | style: 11 | flake8 . 12 | 13 | readme: 14 | mdl README.md 15 | 16 | requirements: 17 | safety check -r requirements_dev.txt 18 | 19 | check: 20 | make style 21 | make types 22 | make test 23 | make requirements 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flake8-annotations-complexity 2 | 3 | [![Build Status](https://github.com/best-doctor/flake8-annotations-complexity/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/best-doctor/flake8-annotations-complexity/actions/workflows/build.yml) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/c81ff76755380663b7d3/maintainability)](https://codeclimate.com/github/best-doctor/flake8-annotations-complexity/maintainability) 5 | [![Test Coverage](https://api.codeclimate.com/v1/badges/c81ff76755380663b7d3/test_coverage)](https://codeclimate.com/github/best-doctor/flake8-annotations-complexity/test_coverage) 6 | [![PyPI version](https://badge.fury.io/py/flake8-annotations-complexity.svg)](https://badge.fury.io/py/flake8-annotations-complexity) 7 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flake8-annotations-complexity) 8 | 9 | An extension for flake8 to report on too complex type annotations. 10 | 11 | Complex type annotations often means bad annotations usage, 12 | wrong code decomposition or improper data structure choice. 13 | They are also hard to read and make code look java-like. 14 | 15 | Annotation complexity is maximum annotation nesting level. 16 | So `List[int]` complexity is 2 and `Tuple[List[Optional[str]], int]` is 4. 17 | 18 | Default max annotation complexity is 3 and can be configured 19 | via `--max-annotations-complexity` option. 20 | 21 | ## Installation 22 | 23 | ```bash 24 | pip install flake8-annotations-complexity 25 | ``` 26 | 27 | ## Example 28 | 29 | Sample file: 30 | 31 | ```python 32 | # test.py 33 | 34 | def foo() -> List[int]: 35 | return [1] 36 | ``` 37 | 38 | Usage: 39 | 40 | ```terminal 41 | $ flake8 --max-annotations-complexity=1 test.py 42 | test.py:4:14: TAE002 too complex annotation (2 > 1) 43 | ``` 44 | 45 | ## Contributing 46 | 47 | We would love you to contribute to our project. It's simple: 48 | 49 | 1. Create an issue with bug you found or proposal you have. 50 | Wait for approve from maintainer. 51 | 1. Create a pull request. Make sure all checks are green. 52 | 1. Fix review comments if any. 53 | 1. Be awesome. 54 | 55 | Here are useful tips: 56 | 57 | - You can run all checks and tests with `make check`. 58 | Please do it before CI does. 59 | - We use [BestDoctor python styleguide](https://github.com/best-doctor/guides/blob/master/guides/en/python_styleguide.md). 60 | - We respect [Django CoC](https://www.djangoproject.com/conduct/). 61 | Make soft, not bullshit. 62 | -------------------------------------------------------------------------------- /flake8_annotations_complexity/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.0' 2 | -------------------------------------------------------------------------------- /flake8_annotations_complexity/ast_helpers.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from logging import getLogger 3 | from typing import List, Tuple, Any 4 | 5 | logger = getLogger(__name__) 6 | 7 | 8 | def get_annotation_complexity(annotation_node) -> int: 9 | """ 10 | Recursively counts complexity of annotation nodes. 11 | 12 | When annotations are written as strings, 13 | we additionally parse them to ``ast`` nodes. 14 | """ 15 | if isinstance(annotation_node, ast.Str): 16 | # try to parse string-wrapped annotations 17 | try: 18 | annotation_node = ast.parse(annotation_node.s).body[0].value # type: ignore 19 | except Exception as exc: 20 | logger.debug(f'Cannot parse string-wrapped annotation: {exc!r}') 21 | return 1 22 | 23 | if isinstance(annotation_node, ast.Subscript): 24 | return 1 + get_annotation_complexity(annotation_node.slice) 25 | 26 | if isinstance(annotation_node, (ast.Tuple, ast.List)): 27 | return max((get_annotation_complexity(n) for n in annotation_node.elts), default=1) 28 | 29 | return 1 30 | 31 | 32 | def get_annotation_len(annotation_node) -> int: 33 | """ 34 | Recursively counts length of annotation nodes. 35 | 36 | When annotations are written as strings, 37 | we additionally parse them to ``ast`` nodes. 38 | """ 39 | if isinstance(annotation_node, ast.Str): 40 | # try to parse string-wrapped annotations 41 | try: 42 | annotation_node = ast.parse(annotation_node.s).body[0].value # type: ignore 43 | except Exception as exc: 44 | logger.debug(f'Cannot parse string-wrapped annotation: {exc!r}') 45 | return 0 46 | 47 | if isinstance(annotation_node, ast.Subscript): 48 | try: 49 | return len(annotation_node.slice.elts) # type: ignore 50 | except AttributeError: 51 | logger.debug('Attribute error on annotation length counting') 52 | return 0 53 | 54 | return 0 55 | 56 | 57 | def validate_annotations_in_ast_node( 58 | node, 59 | max_annotations_complexity, 60 | max_annotations_len, 61 | ) -> List[Tuple[Any, str]]: 62 | too_difficult_annotations = [] 63 | func_defs = [ 64 | f for f in ast.walk(node) 65 | if isinstance(f, ast.FunctionDef) 66 | ] 67 | annotations: List[ast.AST] = [] 68 | for funcdef in func_defs: 69 | annotations += list(filter(None, (a.annotation for a in funcdef.args.args))) 70 | if funcdef.returns: 71 | annotations.append(funcdef.returns) 72 | annotations += [a.annotation for a in ast.walk(node) if isinstance(a, ast.AnnAssign) and a.annotation] 73 | for annotation in annotations: 74 | complexity = get_annotation_complexity(annotation) 75 | if complexity > max_annotations_complexity: 76 | too_difficult_annotations.append(( 77 | annotation, 78 | 'TAE002 too complex annotation ({0} > {1})'.format(complexity, max_annotations_complexity), 79 | )) 80 | annotation_len = get_annotation_len(annotation) 81 | if annotation_len > max_annotations_len: 82 | too_difficult_annotations.append(( 83 | annotation, 84 | 'TAE003 too long annotation ({0} > {1})'.format(annotation_len, max_annotations_len), 85 | )) 86 | return too_difficult_annotations 87 | -------------------------------------------------------------------------------- /flake8_annotations_complexity/checker.py: -------------------------------------------------------------------------------- 1 | from typing import Generator, Tuple 2 | 3 | from flake8_annotations_complexity import __version__ as version 4 | from flake8_annotations_complexity.ast_helpers import validate_annotations_in_ast_node 5 | 6 | 7 | class AnnotationsComplexityChecker: 8 | name = 'flake8-annotations-complexity' 9 | version = version 10 | 11 | max_annotations_complexity = None 12 | default_max_annotations_complexity = 3 13 | 14 | max_annotations_len = None 15 | default_max_annotations_len = 7 16 | 17 | def __init__(self, tree, filename: str): 18 | self.filename = filename 19 | self.tree = tree 20 | if AnnotationsComplexityChecker.max_annotations_complexity is None: 21 | AnnotationsComplexityChecker.max_annotations_complexity = self.default_max_annotations_complexity 22 | if AnnotationsComplexityChecker.max_annotations_len is None: 23 | AnnotationsComplexityChecker.max_annotations_len = self.default_max_annotations_len 24 | 25 | @classmethod 26 | def add_options(cls, parser) -> None: 27 | parser.add_option( 28 | '--max-annotations-complexity', 29 | type=int, 30 | parse_from_config=True, 31 | default=cls.default_max_annotations_complexity, 32 | ) 33 | parser.add_option( 34 | '--max-annotations-len', 35 | type=int, 36 | parse_from_config=True, 37 | default=cls.default_max_annotations_len, 38 | ) 39 | 40 | @classmethod 41 | def parse_options(cls, options) -> None: 42 | cls.max_annotations_complexity = int(options.max_annotations_complexity) 43 | 44 | def run(self) -> Generator[Tuple[int, int, str, type], None, None]: 45 | too_difficult_annotations = validate_annotations_in_ast_node( 46 | self.tree, 47 | self.max_annotations_complexity, 48 | self.max_annotations_len, 49 | ) 50 | 51 | for annotation, error_msg in too_difficult_annotations: 52 | yield ( 53 | annotation.lineno, 54 | annotation.col_offset, 55 | error_msg, 56 | type(self), 57 | ) 58 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest==6.2.5 2 | pytest-cov==3.0.0 3 | pydocstyle==6.1.1 4 | flake8==4.0.1 5 | flake8-2020==1.6.0 6 | flake8-blind-except==0.2.0 7 | flake8-broken-line==0.4.0 8 | flake8-bugbear==22.1.11 9 | flake8-builtins==1.5.3 10 | flake8-class-attributes-order==0.1.3 11 | flake8-comprehensions==3.8.0 12 | flake8-debugger==4.0.0 13 | flake8-docstrings==1.6.0 14 | flake8-eradicate==1.2.0 15 | flake8-fixme==1.1.1 16 | flake8-polyfill==1.0.2 17 | flake8-print==3.1.4 18 | flake8-quotes==3.0.0 19 | flake8-string-format==0.3.0 20 | flake8-tidy-imports==4.6.0 21 | flake8-typing-imports==1.9.0 22 | flake8-variables-names==0.0.5 23 | mypy==0.921 24 | mypy-extensions==0.4.3 25 | safety==2.3.4 26 | dlint==0.12.0 27 | flake8-if-statements==0.1.0 28 | flake8-functions==0.0.7 29 | flake8-annotations-coverage==0.0.6 30 | flake8-expression-complexity==0.0.10 31 | packaging==22.0 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-complexity = 8 3 | max-line-length = 120 4 | ignore = W503, P103, D, B902 5 | exclude = node_modules,env,venv,venv36,tests 6 | max-annotations-complexity = 4 7 | max-returns-amount = 4 8 | var_names_exclude_pathes = node_modules,env,venv,venv36 9 | assert_allowed_in_pathes = tests,migrations,env,venv,venv36 10 | adjustable-default-max-complexity = 8 11 | per-file-ignores = 12 | __init__.py: F401 13 | ban-relative-imports = True 14 | min-coverage-percents = 100 15 | 16 | [mypy] 17 | ignore_missing_imports = True 18 | warn_no_return = False 19 | exclude = build|env|venv.*|migrations|tests 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | package_name = 'flake8_annotations_complexity' 7 | 8 | 9 | def get_version() -> Optional[str]: 10 | with open('flake8_annotations_complexity/__init__.py', 'r') as f: 11 | lines = f.readlines() 12 | for line in lines: 13 | if line.startswith('__version__'): 14 | return line.split('=')[-1].strip().strip("'") 15 | 16 | 17 | def get_long_description() -> str: 18 | with open('README.md') as f: 19 | return f.read() 20 | 21 | 22 | setup( 23 | name=package_name, 24 | description='A flake8 extension that checks for type annotations complexity', 25 | classifiers=[ 26 | 'Environment :: Console', 27 | 'Framework :: Flake8', 28 | 'Operating System :: OS Independent', 29 | 'Topic :: Software Development :: Documentation', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | 'Topic :: Software Development :: Quality Assurance', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.9', 34 | 'Programming Language :: Python :: 3.10', 35 | 'Programming Language :: Python :: 3.11', 36 | ], 37 | long_description=get_long_description(), 38 | long_description_content_type='text/markdown', 39 | python_requires='>=3.9', 40 | packages=find_packages(), 41 | include_package_data=True, 42 | keywords='flake8 annotations', 43 | version=get_version(), 44 | author='Ilya Lebedev', 45 | author_email='melevir@gmail.com', 46 | install_requires=['flake8'], 47 | entry_points={ 48 | 'flake8.extension': [ 49 | 'TAE002 = flake8_annotations_complexity.checker:AnnotationsComplexityChecker', 50 | ], 51 | }, 52 | url='https://github.com/best-doctor/flake8-annotations-complexity', 53 | license='MIT', 54 | py_modules=[package_name], 55 | zip_safe=False, 56 | ) 57 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | 4 | from flake8_annotations_complexity.checker import AnnotationsComplexityChecker 5 | 6 | 7 | def run_validator_for_test_file(filename, max_annotations_complexity=None): 8 | test_file_path = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 10 | 'test_files', 11 | filename, 12 | ) 13 | with open(test_file_path, 'r') as file_handler: 14 | raw_content = file_handler.read() 15 | tree = ast.parse(raw_content) 16 | checker = AnnotationsComplexityChecker(tree=tree, filename=filename) 17 | if max_annotations_complexity: 18 | checker.max_annotations_complexity = max_annotations_complexity 19 | 20 | return list(checker.run()) 21 | -------------------------------------------------------------------------------- /tests/test_annotations_complexity.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from conftest import run_validator_for_test_file 6 | 7 | 8 | def test_always_ok_for_empty_file(): 9 | errors = run_validator_for_test_file('empty.py') 10 | assert not errors 11 | errors = run_validator_for_test_file('empty.py', max_annotations_complexity=1) 12 | assert not errors 13 | 14 | 15 | def test_ok_for_unannotated_file(): 16 | errors = run_validator_for_test_file('unannotated.py', max_annotations_complexity=1) 17 | assert not errors 18 | 19 | 20 | def test_ok_for_dynamic_annotations_file(): 21 | errors = run_validator_for_test_file('dynamic_annotations.py') 22 | assert len(errors) == 1 23 | errors = run_validator_for_test_file('dynamic_annotations.py', max_annotations_complexity=2) 24 | assert len(errors) == 1 25 | errors = run_validator_for_test_file('dynamic_annotations.py', max_annotations_complexity=1) 26 | assert len(errors) == 3 27 | 28 | 29 | def test_ok_for_string_annotations_file(): 30 | errors = run_validator_for_test_file('string_annotations.py') 31 | assert len(errors) == 1 32 | errors = run_validator_for_test_file('string_annotations.py', max_annotations_complexity=1) 33 | assert len(errors) == 2 34 | 35 | 36 | def test_validates_annotations_complexity_for_annassigments(): 37 | errors = run_validator_for_test_file('var_annotation.py') 38 | assert len(errors) == 1 39 | 40 | 41 | def test_ok_for_empty_tuple(): 42 | errors = run_validator_for_test_file('empty_tuple.py') 43 | assert not errors 44 | errors = run_validator_for_test_file('empty_tuple.py', max_annotations_complexity=1) 45 | assert len(errors) == 1 46 | errors = run_validator_for_test_file('empty_tuple.py', max_annotations_complexity=2) 47 | assert not errors 48 | 49 | 50 | def test_not_raises_errors_for_weird_annotations(): 51 | errors = run_validator_for_test_file('weird_annotations.py') 52 | assert not errors 53 | 54 | 55 | def test_ok_for_empty_string(): 56 | errors = run_validator_for_test_file('empty_string.py') 57 | assert not errors 58 | errors = run_validator_for_test_file('empty_string.py', max_annotations_complexity=1) 59 | assert len(errors) == 2 60 | errors = run_validator_for_test_file('empty_string.py', max_annotations_complexity=2) 61 | assert not errors 62 | 63 | 64 | @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python 3.7+") 65 | def test_pep_585_compliance(): 66 | errors = run_validator_for_test_file('pep_585.py') 67 | assert not errors 68 | errors = run_validator_for_test_file('pep_585.py', max_annotations_complexity=1) 69 | assert len(errors) == 11 70 | errors = run_validator_for_test_file('pep_585.py', max_annotations_complexity=2) 71 | assert len(errors) == 2 72 | 73 | 74 | def test_validates_too_long_annotations(): 75 | errors = run_validator_for_test_file('too_long_annotation.py') 76 | assert len(errors) == 4 77 | -------------------------------------------------------------------------------- /tests/test_files/dynamic_annotations.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | List, 3 | Dict, 4 | Optional, 5 | Tuple, 6 | Literal, 7 | ) 8 | 9 | 10 | def foo() -> List[int]: 11 | return [1] 12 | 13 | 14 | def bar(arg1: str, arg2: Tuple[List[int], Optional[Dict[str, int]]]) -> int: 15 | return 1 16 | 17 | 18 | def gaz() -> Literal['regular', 'raise', 'is']: 19 | raise Exception 20 | -------------------------------------------------------------------------------- /tests/test_files/empty.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/best-doctor/flake8-annotations-complexity/9ddfc5a6e817a0ec3a93027c7f66652e2417c4e5/tests/test_files/empty.py -------------------------------------------------------------------------------- /tests/test_files/empty_string.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | foo: Literal[""] = "" 4 | bar: Literal[''] = '' 5 | -------------------------------------------------------------------------------- /tests/test_files/empty_tuple.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | foo: Tuple[()] = () 4 | -------------------------------------------------------------------------------- /tests/test_files/pep_585.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import defaultdict, ChainMap, Counter, deque 4 | 5 | 6 | # Taken from https://www.python.org/dev/peps/pep-0585/ 7 | def find(haystack: dict[str, list[int]]) -> int: 8 | return 0 9 | 10 | 11 | l: list[str] = [] 12 | cm: ChainMap[str, list[str]] 13 | 14 | # Taken from https://github.com/python/mypy/blob/master/test-data/unit/check-future.test 15 | t1: type[int] 16 | t2: list[int] 17 | t3: dict[int, int] 18 | t4: tuple[int, str, int] 19 | 20 | c1: defaultdict[int, int] 21 | c2: ChainMap[int, int] 22 | c3: Counter[int] 23 | c4: deque[int] 24 | -------------------------------------------------------------------------------- /tests/test_files/string_annotations.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Optional, Tuple 2 | 3 | 4 | def foo() -> 'List[int]': 5 | return [1] 6 | 7 | 8 | def bar(arg1: 'str', arg2: 'Tuple[List[int], Optional[Dict[str, int]]]') -> 'int': 9 | return 1 10 | -------------------------------------------------------------------------------- /tests/test_files/too_long_annotation.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Tuple 2 | 3 | foo: Tuple[str, str, str, int, List, Any, str, Dict, int] = tuple() 4 | 5 | bar: 'Tuple[str, str, str, int, List, Any, str, Dict, int]' = tuple() 6 | 7 | egg: Tuple[str, str, str, int, List, Any, List[int], Optional[Dict[str, int]]] = tuple() 8 | -------------------------------------------------------------------------------- /tests/test_files/unannotated.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | pass 3 | 4 | 5 | def bar(arg: str): 6 | pass 7 | -------------------------------------------------------------------------------- /tests/test_files/var_annotation.py: -------------------------------------------------------------------------------- 1 | 2 | foo: Tuple[List[int], Optional[Dict[str, int]]] = tuple() 3 | -------------------------------------------------------------------------------- /tests/test_files/weird_annotations.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def foo(a: None, b: 1) -> 'String Annontation': # PEP 3107 allows any expressions in type annotations 4 | pass 5 | --------------------------------------------------------------------------------