From 7e42c5f28accd533fa84081e53f66856e92d6a4b Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 16 Mar 2022 13:51:31 -0400 Subject: [PATCH 1/9] Frame ANN401 Error --- README.md | 8 +++++++- flake8_annotations/checker.py | 4 ++++ flake8_annotations/error_codes.py | 9 +++++++++ testing/test_opinionated_any.py | 0 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 testing/test_opinionated_any.py diff --git a/README.md b/README.md index 5986aeb..40e5345 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ $ flake8 --version ## Table of Warnings -All warnings are enabled by default. +With the exception of `ANN4xx`-level warnings, all warnings are enabled by default. ### Function Annotations | ID | Description | @@ -68,6 +68,12 @@ All warnings are enabled by default. |----------|-----------------------------------------------------------| | `ANN301` | PEP 484 disallows both type annotations and type comments | +### Opinionated Warnings +These warnings are disabled by default. +| ID | Description | +|----------|------------------------------------------------------------| +| `ANN401` | Dynamically typed expressions (typing.Any) are disallowed. | + **Notes:** 1. See: [PEP 484](https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods) and [PEP 563](https://www.python.org/dev/peps/pep-0563/) for suggestions on annotating `self` and `cls` arguments. diff --git a/flake8_annotations/checker.py b/flake8_annotations/checker.py index a7795fc..d82a5f2 100644 --- a/flake8_annotations/checker.py +++ b/flake8_annotations/checker.py @@ -19,6 +19,8 @@ "overload", ] +_DISABLED_BY_DEFAULT = ("ANN401",) # Disable opinionated warnings by default + class TypeHintChecker: """Top level checker for linting the presence of type hints in function definitions.""" @@ -133,6 +135,8 @@ def run(self) -> t.Generator[FORMATTED_ERROR, None, None]: @classmethod def add_options(cls, parser: OptionManager) -> None: # pragma: no cover """Add custom configuration option(s) to flake8.""" + parser.extend_default_ignore(_DISABLED_BY_DEFAULT) + parser.add_option( "--suppress-none-returning", default=False, diff --git a/flake8_annotations/error_codes.py b/flake8_annotations/error_codes.py index ad8b533..4dd5252 100644 --- a/flake8_annotations/error_codes.py +++ b/flake8_annotations/error_codes.py @@ -166,3 +166,12 @@ def __init__(self, argname: str, lineno: int, col_offset: int): self.argname = argname self.lineno = lineno self.col_offset = col_offset + + +# Opinionated warnings +class ANN401(Error): + def __init__(self, argname: str, lineno: int, col_offset: int): + super().__init__("ANN401 Dynamically typed expressions (typing.Any) are disallowed") + self.argname = argname + self.lineno = lineno + self.col_offset = col_offset diff --git a/testing/test_opinionated_any.py b/testing/test_opinionated_any.py new file mode 100644 index 0000000..e69de29 From 60901719846a761e9dd1544ea86238a19ed97ee9 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 16 Mar 2022 14:07:11 -0400 Subject: [PATCH 2/9] Be better about type guarding Ignore the test code, not really worth whacking all of them --- flake8_annotations/checker.py | 13 ++- flake8_annotations/models.py | 13 ++- poetry.lock | 185 +++++++++++++++++++--------------- pyproject.toml | 9 +- testing/helpers.py | 8 +- tox.ini | 7 +- 6 files changed, 134 insertions(+), 101 deletions(-) diff --git a/flake8_annotations/checker.py b/flake8_annotations/checker.py index d82a5f2..8eeafde 100644 --- a/flake8_annotations/checker.py +++ b/flake8_annotations/checker.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import typing as t -from argparse import Namespace from functools import lru_cache -from flake8.options.manager import OptionManager - from flake8_annotations import PY_GTE_38, __version__, enums, error_codes -from flake8_annotations.models import Argument, Function from flake8_annotations.visitors import FunctionVisitor, ast +if t.TYPE_CHECKING: + from argparse import Namespace + + from flake8.options.manager import OptionManager + + from flake8_annotations.models import Argument, Function + FORMATTED_ERROR = t.Tuple[int, int, str, t.Type[t.Any]] _DEFAULT_DISPATCH_DECORATORS = [ diff --git a/flake8_annotations/models.py b/flake8_annotations/models.py index d6cb857..2bdc1ef 100644 --- a/flake8_annotations/models.py +++ b/flake8_annotations/models.py @@ -1,15 +1,14 @@ +from __future__ import annotations + import typing as t from itertools import zip_longest from flake8_annotations import PY_GTE_38, PY_GTE_311 from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType -from flake8_annotations.visitors import ( - AST_ARG_TYPES, - AST_DECORATOR_NODES, - AST_FUNCTION_TYPES, - ReturnVisitor, - ast, -) +from flake8_annotations.visitors import AST_ARG_TYPES, ReturnVisitor, ast + +if t.TYPE_CHECKING: + from flake8_annotations.visitors import AST_DECORATOR_NODES, AST_FUNCTION_TYPES # Check if we can use the stdlib ast module instead of typed_ast # stdlib ast gains native type comment support in Python 3.8 diff --git a/poetry.lock b/poetry.lock index 5546a8f..38d7a06 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,7 +61,7 @@ python-versions = ">=3.6.1" [[package]] name = "click" -version = "8.0.3" +version = "8.0.4" description = "Composable command line interface toolkit" category = "dev" optional = false @@ -89,7 +89,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.3.1" +version = "6.3.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -111,7 +111,7 @@ python-versions = "*" [[package]] name = "filelock" -version = "3.5.1" +version = "3.6.0" description = "A platform independent file lock." category = "dev" optional = false @@ -193,9 +193,20 @@ python-versions = "*" [package.dependencies] flake8 = "*" +[[package]] +name = "flake8-type-checking" +version = "1.3.3" +description = "A flake8 plugin for managing type-checking imports & forward references" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" + +[package.dependencies] +flake8 = "*" + [[package]] name = "identify" -version = "2.4.10" +version = "2.4.12" description = "File identification library for Python" category = "dev" optional = false @@ -263,7 +274,7 @@ python-versions = "*" [[package]] name = "mypy" -version = "0.931" +version = "0.941" description = "Optional static typing for Python" category = "dev" optional = false @@ -278,6 +289,7 @@ typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" @@ -328,7 +340,7 @@ flake8-polyfill = ">=1.0.2,<2" [[package]] name = "platformdirs" -version = "2.5.0" +version = "2.5.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -421,11 +433,11 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.0.1" +version = "7.1.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -559,7 +571,7 @@ python-versions = ">=3.6" [[package]] name = "virtualenv" -version = "20.13.1" +version = "20.13.3" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -591,7 +603,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "fa721838fb42273a74253c29a98f339d90d719fe50b6889d57a1868a0669ed6b" +content-hash = "ed1d2e71802723bc6fc1a7d75897fb78130015d0709bf35ccfd6c8b92f31e10f" [metadata.files] atomicwrites = [ @@ -636,8 +648,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] cogapp = [ {file = "cogapp-3.3.0-py2.py3-none-any.whl", hash = "sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50"}, @@ -648,55 +660,55 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, - {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, - {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, - {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, - {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, - {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, - {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, - {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, - {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, - {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, - {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, - {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, - {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, ] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] filelock = [ - {file = "filelock-3.5.1-py3-none-any.whl", hash = "sha256:7b23620a293cf3e19924e469cb96672dc72b36c26e8f80f85668310117fcbe4e"}, - {file = "filelock-3.5.1.tar.gz", hash = "sha256:d1eccb164ed020bc84edd9e45bf6cdb177f64749f6b8fe066648832d2e98726d"}, + {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, + {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, @@ -722,9 +734,13 @@ flake8-polyfill = [ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] +flake8-type-checking = [ + {file = "flake8-type-checking-1.3.3.tar.gz", hash = "sha256:a9d7523ee6dbefcaf9010b0ae3ddca938232e8c0d7b89f26e082e29aebcb7823"}, + {file = "flake8_type_checking-1.3.3-py3-none-any.whl", hash = "sha256:241fb3533b64271f811fcb6d79f45609f69c2a2d16527289b98be68ae9c01fa4"}, +] identify = [ - {file = "identify-2.4.10-py2.py3-none-any.whl", hash = "sha256:7d10baf6ba6f1912a0a49f4c1c2c49fa1718765c3a37d72d13b07779567c5b85"}, - {file = "identify-2.4.10.tar.gz", hash = "sha256:e12b2aea3cf108de73ae055c2260783bde6601de09718f6768cf8e9f6f6322a6"}, + {file = "identify-2.4.12-py2.py3-none-any.whl", hash = "sha256:5f06b14366bd1facb88b00540a1de05b69b310cbc2654db3c7e07fa3a4339323"}, + {file = "identify-2.4.12.tar.gz", hash = "sha256:3f3244a559290e7d3deb9e9adc7b33594c1bc85a9dd82e0f1be519bf12a1ec17"}, ] importlib-metadata = [ {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, @@ -746,26 +762,29 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] mypy = [ - {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"}, - {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"}, - {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"}, - {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"}, - {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"}, - {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"}, - {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"}, - {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"}, - {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"}, - {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"}, - {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"}, - {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"}, - {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"}, - {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"}, - {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"}, - {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"}, - {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"}, - {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"}, - {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"}, - {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"}, + {file = "mypy-0.941-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:98f61aad0bb54f797b17da5b82f419e6ce214de0aa7e92211ebee9e40eb04276"}, + {file = "mypy-0.941-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6a8e1f63357851444940351e98fb3252956a15f2cabe3d698316d7a2d1f1f896"}, + {file = "mypy-0.941-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b30d29251dff4c59b2e5a1fa1bab91ff3e117b4658cb90f76d97702b7a2ae699"}, + {file = "mypy-0.941-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8eaf55fdf99242a1c8c792247c455565447353914023878beadb79600aac4a2a"}, + {file = "mypy-0.941-cp310-cp310-win_amd64.whl", hash = "sha256:080097eee5393fd740f32c63f9343580aaa0fb1cda0128fd859dfcf081321c3d"}, + {file = "mypy-0.941-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f79137d012ff3227866222049af534f25354c07a0d6b9a171dba9f1d6a1fdef4"}, + {file = "mypy-0.941-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e5974583a77d630a5868eee18f85ac3093caf76e018c510aeb802b9973304ce"}, + {file = "mypy-0.941-cp36-cp36m-win_amd64.whl", hash = "sha256:0dd441fbacf48e19dc0c5c42fafa72b8e1a0ba0a39309c1af9c84b9397d9b15a"}, + {file = "mypy-0.941-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0d3bcbe146247997e03bf030122000998b076b3ac6925b0b6563f46d1ce39b50"}, + {file = "mypy-0.941-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bada0cf7b6965627954b3a128903a87cac79a79ccd83b6104912e723ef16c7b"}, + {file = "mypy-0.941-cp37-cp37m-win_amd64.whl", hash = "sha256:eea10982b798ff0ccc3b9e7e42628f932f552c5845066970e67cd6858655d52c"}, + {file = "mypy-0.941-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:108f3c7e14a038cf097d2444fa0155462362c6316e3ecb2d70f6dd99cd36084d"}, + {file = "mypy-0.941-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d61b73c01fc1de799226963f2639af831307fe1556b04b7c25e2b6c267a3bc76"}, + {file = "mypy-0.941-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:42c216a33d2bdba08098acaf5bae65b0c8196afeb535ef4b870919a788a27259"}, + {file = "mypy-0.941-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fc5ecff5a3bbfbe20091b1cad82815507f5ae9c380a3a9bf40f740c70ce30a9b"}, + {file = "mypy-0.941-cp38-cp38-win_amd64.whl", hash = "sha256:bf446223b2e0e4f0a4792938e8d885e8a896834aded5f51be5c3c69566495540"}, + {file = "mypy-0.941-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:745071762f32f65e77de6df699366d707fad6c132a660d1342077cbf671ef589"}, + {file = "mypy-0.941-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:465a6ce9ca6268cadfbc27a2a94ddf0412568a6b27640ced229270be4f5d394d"}, + {file = "mypy-0.941-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d051ce0946521eba48e19b25f27f98e5ce4dbc91fff296de76240c46b4464df0"}, + {file = "mypy-0.941-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:818cfc51c25a5dbfd0705f3ac1919fff6971eb0c02e6f1a1f6a017a42405a7c0"}, + {file = "mypy-0.941-cp39-cp39-win_amd64.whl", hash = "sha256:b2ce2788df0c066c2ff4ba7190fa84f18937527c477247e926abeb9b1168b8cc"}, + {file = "mypy-0.941-py3-none-any.whl", hash = "sha256:3cf77f138efb31727ee7197bc824c9d6d7039204ed96756cc0f9ca7d8e8fc2a4"}, + {file = "mypy-0.941.tar.gz", hash = "sha256:cbcc691d8b507d54cb2b8521f0a2a3d4daa477f62fe77f0abba41e5febb377b7"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -788,8 +807,8 @@ pep8-naming = [ {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, ] platformdirs = [ - {file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"}, - {file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"}, + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -820,8 +839,8 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, - {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, + {file = "pytest-7.1.0-py3-none-any.whl", hash = "sha256:b555252a95bbb2a37a97b5ac2eb050c436f7989993565f5e0c9128fcaacadd0e"}, + {file = "pytest-7.1.0.tar.gz", hash = "sha256:f1089d218cfcc63a212c42896f1b7fbf096874d045e1988186861a1a87d27b47"}, ] pytest-check = [ {file = "pytest_check-1.0.4-py3-none-any.whl", hash = "sha256:aacc9500178611f8ad075a7c46ce8de8ac34f05270eee28f223fb0c8622fbfbe"}, @@ -921,8 +940,8 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] virtualenv = [ - {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, - {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, + {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"}, + {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"}, ] zipp = [ {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, diff --git a/pyproject.toml b/pyproject.toml index 7f2608c..d40dd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ include = [ [tool.poetry.dependencies] python = "^3.7" -flake8 = ">=3.7, <5.0" +flake8 = ">=3.7" typed-ast = {version="^1.4,<2.0", python="<3.8"} [tool.poetry.dev-dependencies] @@ -46,8 +46,9 @@ flake8-bugbear = "^22.0" flake8-docstrings = "^1.6" flake8-fixme = "^1.1" flake8-formatter-junit-xml = "^0.0" +flake8-type-checking = {version="^1.3", python=">=3.8,<4.0"} isort = "^5.9" -mypy = "^0.931" +mypy = "^0.941" pep8-naming = "^0.12" pre-commit = "^2.13" pytest = "^7.0" @@ -85,5 +86,5 @@ warn_unused_configs = true warn_unused_ignores = true [build-system] -requires = ["poetry>=1.0"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.8"] +build-backend = "poetry.core.masonry.api" diff --git a/testing/helpers.py b/testing/helpers.py index 79e655c..d5a2069 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -1,17 +1,21 @@ +from __future__ import annotations + import typing as t from pytest_check import check_func from flake8_annotations import PY_GTE_38 from flake8_annotations.checker import ( - FORMATTED_ERROR, TypeHintChecker, _DEFAULT_DISPATCH_DECORATORS, _DEFAULT_OVERLOAD_DECORATORS, ) -from flake8_annotations.models import Function from flake8_annotations.visitors import FunctionVisitor, ast +if t.TYPE_CHECKING: + from flake8_annotations.checker import FORMATTED_ERROR + from flake8_annotations.models import Function + def parse_source(src: str) -> t.Tuple[ast.Module, t.List[str]]: """Parse the provided Python source string and return an (typed AST, source) tuple.""" diff --git a/tox.ini b/tox.ini index 4b7e6ca..fc5a825 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [flake8] max-line-length=100 docstring-convention=all -ignore= +extend-ignore= P102,B311,W503,E226,S311, # Missing Docstrings D100,D104,D105,D107, @@ -15,12 +15,17 @@ ignore= ANN002,ANN003,ANN101,ANN102,ANN204,ANN206, # pep8-naming N802,N806,N815, +extend-select= + # Type Guards + TC1 exclude= __pycache__,.cache, venv,.venv, build, dist, error_codes.py, .tox +per-file-ignores = + testing/test_*.py:TC import-order-style=pycharm application-import-names=flake8_annotations,testing From c949b6211079dff676df2cc2df5d80c6e30a1daf Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 16 Mar 2022 20:14:47 -0400 Subject: [PATCH 3/9] Rejoin ast walking components Breaking them into separate modules (See: f69b39) was nice in theory but it ends up being more of a circular dependency nightmare than it's worth --- .coveragerc | 2 - .../{models.py => ast_walker.py} | 128 ++++++++++++++++- flake8_annotations/checker.py | 4 +- flake8_annotations/error_codes.py | 10 +- flake8_annotations/visitors.py | 129 ------------------ testing/helpers.py | 4 +- .../test_cases/argument_parsing_test_cases.py | 2 +- .../test_cases/function_parsing_test_cases.py | 2 +- .../object_formatting_test_cases.py | 19 +-- testing/test_cases/type_comment_test_cases.py | 2 +- testing/test_classifier.py | 2 +- testing/test_fully_annotated.py | 2 +- testing/test_parser.py | 3 +- testing/test_type_comment_parsing.py | 3 +- tox.ini | 2 +- 15 files changed, 151 insertions(+), 163 deletions(-) delete mode 100644 .coveragerc rename flake8_annotations/{models.py => ast_walker.py} (78%) delete mode 100644 flake8_annotations/visitors.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 65b8b2a..0000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[run] -parallel = True diff --git a/flake8_annotations/models.py b/flake8_annotations/ast_walker.py similarity index 78% rename from flake8_annotations/models.py rename to flake8_annotations/ast_walker.py index 2bdc1ef..3bf6b12 100644 --- a/flake8_annotations/models.py +++ b/flake8_annotations/ast_walker.py @@ -5,19 +5,30 @@ from flake8_annotations import PY_GTE_38, PY_GTE_311 from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType -from flake8_annotations.visitors import AST_ARG_TYPES, ReturnVisitor, ast -if t.TYPE_CHECKING: - from flake8_annotations.visitors import AST_DECORATOR_NODES, AST_FUNCTION_TYPES - -# Check if we can use the stdlib ast module instead of typed_ast -# stdlib ast gains native type comment support in Python 3.8 +# Check if we can use the stdlib ast module instead of typed_ast; stdlib ast gains native type +# comment support in Python 3.8 if PY_GTE_38: + import ast from ast import Ellipsis as ast_Ellipsis else: + from typed_ast import ast3 as ast # type: ignore[no-redef] from typed_ast.ast3 import Ellipsis as ast_Ellipsis # type: ignore[misc] +AST_DECORATOR_NODES = t.Union[ast.Attribute, ast.Call, ast.Name] +AST_DEF_NODES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef] +AST_FUNCTION_TYPES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef] + +# The order of AST_ARG_TYPES must match Python's grammar +# See: https://docs.python.org/3/library/ast.html#abstract-grammar +AST_ARG_TYPES: t.Tuple[str, ...] = ("args", "vararg", "kwonlyargs", "kwarg") +if PY_GTE_38: + # Positional-only args introduced in Python 3.8 + # If posonlyargs are present, they will be before other argument types + AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES + + class Argument: """Represent a function argument & its metadata.""" @@ -493,3 +504,108 @@ def get_class_decorator_type( return ClassDecoratorType.STATICMETHOD else: return None + + +class FunctionVisitor(ast.NodeVisitor): + """An ast.NodeVisitor instance for walking the AST and describing all contained functions.""" + + AST_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) + + def __init__(self, lines: t.List[str]): + self.lines = lines + self.function_definitions: t.List[Function] = [] + self._context: t.List[AST_DEF_NODES] = [] + + def switch_context(self, node: AST_DEF_NODES) -> None: + """ + Utilize a context switcher as a generic function visitor in order to track function context. + + Without keeping track of context, it's challenging to reliably differentiate class methods + from "regular" functions, especially in the case of nested classes. + + Thank you for the inspiration @isidentical :) + """ + if isinstance(node, self.AST_FUNC_TYPES): + # Check for non-empty context first to prevent IndexErrors for non-nested nodes + if self._context: + if isinstance(self._context[-1], ast.ClassDef): + # Check if current context is a ClassDef node & pass the appropriate flag + self.function_definitions.append( + Function.from_function_node(node, self.lines, is_class_method=True) + ) + elif isinstance(self._context[-1], self.AST_FUNC_TYPES): # pragma: no branch + # Check for nested function & pass the appropriate flag + self.function_definitions.append( + Function.from_function_node(node, self.lines, is_nested=True) + ) + else: + self.function_definitions.append(Function.from_function_node(node, self.lines)) + + self._context.append(node) + self.generic_visit(node) + self._context.pop() + + visit_FunctionDef = switch_context + visit_AsyncFunctionDef = switch_context + visit_ClassDef = switch_context + + +class ReturnVisitor(ast.NodeVisitor): + """ + Special-case of `ast.NodeVisitor` for visiting return statements of a function node. + + If the function node being visited has an explicit return statement of anything other than + `None`, the `instance.has_only_none_returns` flag will be set to `False`. + + If the function node being visited has no return statement, or contains only return + statement(s) that explicitly return `None`, the `instance.has_only_none_returns` flag will be + set to `True`. + + Due to the generic visiting being done, we need to keep track of the context in which a + non-`None` return node is found. These functions are added to a set that is checked to see + whether nor not the parent node is present. + """ + + def __init__(self, parent_node: AST_FUNCTION_TYPES): + self.parent_node = parent_node + self._context: t.List[AST_FUNCTION_TYPES] = [] + self._non_none_return_nodes: t.Set[AST_FUNCTION_TYPES] = set() + + @property + def has_only_none_returns(self) -> bool: + """Return `True` if the parent node isn't in the visited nodes that don't return `None`.""" + return self.parent_node not in self._non_none_return_nodes + + def visit_Return(self, node: ast.Return) -> None: + """ + Check each Return node to see if it returns anything other than `None`. + + If the node being visited returns anything other than `None`, its parent context is added to + the set of non-returning child nodes of the parent node. + """ + if node.value is not None: + # In the event of an explicit `None` return (`return None`), the node body will be an + # instance of either `ast.Constant` (3.8+) or `ast.NameConstant`, which we need to check + # to see if it's actually `None` + if isinstance(node.value, (ast.Constant, ast.NameConstant)): # pragma: no branch + if node.value.value is None: + return + + self._non_none_return_nodes.add(self._context[-1]) + + def switch_context(self, node: AST_FUNCTION_TYPES) -> None: + """ + Utilize a context switcher as a generic visitor in order to properly track function context. + + Using a traditional `ast.generic_visit` setup, return nodes of nested functions are visited + without any knowledge of their context, causing the top-level function to potentially be + mis-classified. + + Thank you for the inspiration @isidentical :) + """ + self._context.append(node) + self.generic_visit(node) + self._context.pop() + + visit_FunctionDef = switch_context + visit_AsyncFunctionDef = switch_context diff --git a/flake8_annotations/checker.py b/flake8_annotations/checker.py index 8eeafde..835db11 100644 --- a/flake8_annotations/checker.py +++ b/flake8_annotations/checker.py @@ -4,14 +4,14 @@ from functools import lru_cache from flake8_annotations import PY_GTE_38, __version__, enums, error_codes -from flake8_annotations.visitors import FunctionVisitor, ast +from flake8_annotations.ast_walker import FunctionVisitor, ast if t.TYPE_CHECKING: from argparse import Namespace from flake8.options.manager import OptionManager - from flake8_annotations.models import Argument, Function + from flake8_annotations.ast_walker import Argument, Function FORMATTED_ERROR = t.Tuple[int, int, str, t.Type[t.Any]] diff --git a/flake8_annotations/error_codes.py b/flake8_annotations/error_codes.py index 4dd5252..4d780d3 100644 --- a/flake8_annotations/error_codes.py +++ b/flake8_annotations/error_codes.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import typing as t from flake8_annotations import checker -from flake8_annotations.models import Argument, Function + +if t.TYPE_CHECKING: + from flake8_annotations.ast_walker import Argument, Function class Error: @@ -22,12 +26,12 @@ def __init__(self, message: str): self.message = message @classmethod - def from_argument(cls, argument: Argument) -> "Error": + def from_argument(cls, argument: Argument) -> Error: """Set error metadata from the input Argument object.""" return cls(argument.argname, argument.lineno, argument.col_offset) # type: ignore[call-arg] @classmethod - def from_function(cls, function: Function) -> "Error": + def from_function(cls, function: Function) -> Error: """Set error metadata from the input Function object.""" return cls(function.name, function.lineno, function.col_offset) # type: ignore[call-arg] diff --git a/flake8_annotations/visitors.py b/flake8_annotations/visitors.py deleted file mode 100644 index b507418..0000000 --- a/flake8_annotations/visitors.py +++ /dev/null @@ -1,129 +0,0 @@ -import typing as t - -from flake8_annotations import PY_GTE_38, models - -# Check if we can use the stdlib ast module instead of typed_ast -# stdlib ast gains native type comment support in Python 3.8 -if PY_GTE_38: - import ast -else: - from typed_ast import ast3 as ast # type: ignore[no-redef] - -AST_DECORATOR_NODES = t.Union[ast.Attribute, ast.Call, ast.Name] -AST_DEF_NODES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef] -AST_FUNCTION_TYPES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef] - -# The order of AST_ARG_TYPES must match Python's grammar -# See: https://docs.python.org/3/library/ast.html#abstract-grammar -AST_ARG_TYPES: t.Tuple[str, ...] = ("args", "vararg", "kwonlyargs", "kwarg") -if PY_GTE_38: - # Positional-only args introduced in Python 3.8 - # If posonlyargs are present, they will be before other argument types - AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES - - -class FunctionVisitor(ast.NodeVisitor): - """An ast.NodeVisitor instance for walking the AST and describing all contained functions.""" - - AST_FUNC_TYPES = (ast.FunctionDef, ast.AsyncFunctionDef) - - def __init__(self, lines: t.List[str]): - self.lines = lines - self.function_definitions: t.List[models.Function] = [] - self._context: t.List[AST_DEF_NODES] = [] - - def switch_context(self, node: AST_DEF_NODES) -> None: - """ - Utilize a context switcher as a generic function visitor in order to track function context. - - Without keeping track of context, it's challenging to reliably differentiate class methods - from "regular" functions, especially in the case of nested classes. - - Thank you for the inspiration @isidentical :) - """ - if isinstance(node, self.AST_FUNC_TYPES): - # Check for non-empty context first to prevent IndexErrors for non-nested nodes - if self._context: - if isinstance(self._context[-1], ast.ClassDef): - # Check if current context is a ClassDef node & pass the appropriate flag - self.function_definitions.append( - models.Function.from_function_node(node, self.lines, is_class_method=True) - ) - elif isinstance(self._context[-1], self.AST_FUNC_TYPES): # pragma: no branch - # Check for nested function & pass the appropriate flag - self.function_definitions.append( - models.Function.from_function_node(node, self.lines, is_nested=True) - ) - else: - self.function_definitions.append( - models.Function.from_function_node(node, self.lines) - ) - - self._context.append(node) - self.generic_visit(node) - self._context.pop() - - visit_FunctionDef = switch_context - visit_AsyncFunctionDef = switch_context - visit_ClassDef = switch_context - - -class ReturnVisitor(ast.NodeVisitor): - """ - Special-case of `ast.NodeVisitor` for visiting return statements of a function node. - - If the function node being visited has an explicit return statement of anything other than - `None`, the `instance.has_only_none_returns` flag will be set to `False`. - - If the function node being visited has no return statement, or contains only return - statement(s) that explicitly return `None`, the `instance.has_only_none_returns` flag will be - set to `True`. - - Due to the generic visiting being done, we need to keep track of the context in which a - non-`None` return node is found. These functions are added to a set that is checked to see - whether nor not the parent node is present. - """ - - def __init__(self, parent_node: AST_FUNCTION_TYPES): - self.parent_node = parent_node - self._context: t.List[AST_FUNCTION_TYPES] = [] - self._non_none_return_nodes: t.Set[AST_FUNCTION_TYPES] = set() - - @property - def has_only_none_returns(self) -> bool: - """Return `True` if the parent node isn't in the visited nodes that don't return `None`.""" - return self.parent_node not in self._non_none_return_nodes - - def visit_Return(self, node: ast.Return) -> None: - """ - Check each Return node to see if it returns anything other than `None`. - - If the node being visited returns anything other than `None`, its parent context is added to - the set of non-returning child nodes of the parent node. - """ - if node.value is not None: - # In the event of an explicit `None` return (`return None`), the node body will be an - # instance of either `ast.Constant` (3.8+) or `ast.NameConstant`, which we need to check - # to see if it's actually `None` - if isinstance(node.value, (ast.Constant, ast.NameConstant)): # pragma: no branch - if node.value.value is None: - return - - self._non_none_return_nodes.add(self._context[-1]) - - def switch_context(self, node: AST_FUNCTION_TYPES) -> None: - """ - Utilize a context switcher as a generic visitor in order to properly track function context. - - Using a traditional `ast.generic_visit` setup, return nodes of nested functions are visited - without any knowledge of their context, causing the top-level function to potentially be - mis-classified. - - Thank you for the inspiration @isidentical :) - """ - self._context.append(node) - self.generic_visit(node) - self._context.pop() - - visit_FunctionDef = switch_context - visit_AsyncFunctionDef = switch_context diff --git a/testing/helpers.py b/testing/helpers.py index d5a2069..9c230ed 100644 --- a/testing/helpers.py +++ b/testing/helpers.py @@ -5,16 +5,16 @@ from pytest_check import check_func from flake8_annotations import PY_GTE_38 +from flake8_annotations.ast_walker import FunctionVisitor, ast from flake8_annotations.checker import ( TypeHintChecker, _DEFAULT_DISPATCH_DECORATORS, _DEFAULT_OVERLOAD_DECORATORS, ) -from flake8_annotations.visitors import FunctionVisitor, ast if t.TYPE_CHECKING: + from flake8_annotations.ast_walker import Function from flake8_annotations.checker import FORMATTED_ERROR - from flake8_annotations.models import Function def parse_source(src: str) -> t.Tuple[ast.Module, t.List[str]]: diff --git a/testing/test_cases/argument_parsing_test_cases.py b/testing/test_cases/argument_parsing_test_cases.py index bf0383a..f69596b 100644 --- a/testing/test_cases/argument_parsing_test_cases.py +++ b/testing/test_cases/argument_parsing_test_cases.py @@ -2,8 +2,8 @@ from textwrap import dedent from typing import NamedTuple, Tuple +from flake8_annotations.ast_walker import Argument from flake8_annotations.enums import AnnotationType -from flake8_annotations.models import Argument class ArgumentTestCase(NamedTuple): diff --git a/testing/test_cases/function_parsing_test_cases.py b/testing/test_cases/function_parsing_test_cases.py index 37e02d7..772811e 100644 --- a/testing/test_cases/function_parsing_test_cases.py +++ b/testing/test_cases/function_parsing_test_cases.py @@ -2,8 +2,8 @@ from textwrap import dedent from typing import NamedTuple, Tuple +from flake8_annotations.ast_walker import Function from flake8_annotations.enums import ClassDecoratorType, FunctionType -from flake8_annotations.models import Function class FunctionTestCase(NamedTuple): diff --git a/testing/test_cases/object_formatting_test_cases.py b/testing/test_cases/object_formatting_test_cases.py index d146862..8797638 100644 --- a/testing/test_cases/object_formatting_test_cases.py +++ b/testing/test_cases/object_formatting_test_cases.py @@ -1,8 +1,8 @@ from functools import partial from typing import NamedTuple, Union +from flake8_annotations.ast_walker import Argument, Function from flake8_annotations.enums import AnnotationType -from flake8_annotations.models import Argument, Function class FormatTestCase(NamedTuple): @@ -29,7 +29,8 @@ class FormatTestCase(NamedTuple): "annotation_type=AnnotationType.ARGS, " "has_type_annotation=False, " "has_3107_annotation=False, " - "has_type_comment=False" + "has_type_comment=False, " + "is_dynamically_typed=False" ")" ), ), @@ -49,14 +50,14 @@ class FormatTestCase(NamedTuple): "has_only_none_returns=True, " "is_nested=False, " "decorator_list=[], " - "args=[Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " # noqa: E501 - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False)]" + "args=[Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " + "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False)]" ")" ), ), "func_has_arg": FormatTestCase( test_object=func(args=[arg(argname="foo"), arg(argname="return")]), - str_output=", ]>", # noqa: E501 + str_output=", ]>", repr_output=( "Function(" "name='test_func', " @@ -70,10 +71,10 @@ class FormatTestCase(NamedTuple): "has_only_none_returns=True, " "is_nested=False, " "decorator_list=[], " - "args=[Argument(argname='foo', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " # noqa: E501 - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False), " - "Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " # noqa: E501 - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False)]" + "args=[Argument(argname='foo', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " + "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False), " + "Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " + "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False)]" ")" ), ), diff --git a/testing/test_cases/type_comment_test_cases.py b/testing/test_cases/type_comment_test_cases.py index dac6007..3409f21 100644 --- a/testing/test_cases/type_comment_test_cases.py +++ b/testing/test_cases/type_comment_test_cases.py @@ -2,8 +2,8 @@ from textwrap import dedent from typing import List, NamedTuple +from flake8_annotations.ast_walker import Argument from flake8_annotations.enums import AnnotationType -from flake8_annotations.models import Argument class ParserTestCase(NamedTuple): diff --git a/testing/test_classifier.py b/testing/test_classifier.py index fc22cd1..cd36306 100644 --- a/testing/test_classifier.py +++ b/testing/test_classifier.py @@ -3,10 +3,10 @@ import pytest import pytest_check as check +from flake8_annotations.ast_walker import Argument, Function from flake8_annotations.checker import FORMATTED_ERROR, classify_error from flake8_annotations.enums import AnnotationType from flake8_annotations.error_codes import Error -from flake8_annotations.models import Argument, Function from testing.helpers import check_source from testing.test_cases import classifier_object_attributes from testing.test_cases.type_comment_test_cases import ParserTestCase, parser_test_cases diff --git a/testing/test_fully_annotated.py b/testing/test_fully_annotated.py index e415b44..40021b7 100644 --- a/testing/test_fully_annotated.py +++ b/testing/test_fully_annotated.py @@ -3,7 +3,7 @@ import pytest import pytest_check as check -from flake8_annotations.models import Function +from flake8_annotations.ast_walker import Function from testing.helpers import functions_from_source from testing.test_cases.annotation_presence_test_cases import ( AnnotationTestCase, diff --git a/testing/test_parser.py b/testing/test_parser.py index 98c5504..568bd46 100644 --- a/testing/test_parser.py +++ b/testing/test_parser.py @@ -5,8 +5,7 @@ import pytest import pytest_check as check -from flake8_annotations.models import Argument, Function -from flake8_annotations.visitors import FunctionVisitor +from flake8_annotations.ast_walker import Argument, Function, FunctionVisitor from testing.helpers import find_matching_function, parse_source from testing.test_cases.argument_parsing_test_cases import argument_test_cases from testing.test_cases.function_parsing_test_cases import function_test_cases diff --git a/testing/test_type_comment_parsing.py b/testing/test_type_comment_parsing.py index 0b0d7ae..3c14f99 100644 --- a/testing/test_type_comment_parsing.py +++ b/testing/test_type_comment_parsing.py @@ -4,8 +4,7 @@ import pytest import pytest_check as check -from flake8_annotations.models import Argument -from flake8_annotations.visitors import FunctionVisitor +from flake8_annotations.ast_walker import Argument, FunctionVisitor from testing.helpers import parse_source from testing.test_cases.type_comment_test_cases import parser_test_cases diff --git a/tox.ini b/tox.ini index fc5a825..82ddae4 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ exclude= error_codes.py, .tox per-file-ignores = - testing/test_*.py:TC + testing/test_*.py:TC E501 D103, import-order-style=pycharm application-import-names=flake8_annotations,testing From d2c5fe9bf347e6827a5c23ed256a40522d89084b Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Thu, 17 Mar 2022 18:57:21 -0400 Subject: [PATCH 4/9] Add detection of dynamic argument typing --- README.md | 16 ++- flake8_annotations/ast_walker.py | 39 ++++++- flake8_annotations/checker.py | 20 +++- testing/test_flake8_actually_runs_checker.py | 7 +- testing/test_opinionated_any.py | 114 +++++++++++++++++++ 5 files changed, 181 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 40e5345..c284673 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,11 @@ With the exception of `ANN4xx`-level warnings, all warnings are enabled by defau These warnings are disabled by default. | ID | Description | |----------|------------------------------------------------------------| -| `ANN401` | Dynamically typed expressions (typing.Any) are disallowed. | +| `ANN401` | Dynamically typed expressions (typing.Any) are disallowed.2 | **Notes:** 1. See: [PEP 484](https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods) and [PEP 563](https://www.python.org/dev/peps/pep-0563/) for suggestions on annotating `self` and `cls` arguments. +2. See: [Dynamic Typing Caveats](#dynamic-typing-caveats) ## Configuration Options Some opinionated flags are provided to tailor the linting errors emitted. @@ -112,7 +113,7 @@ Comma-separated list of decorators flake8-annotations should consider as dispatc Decorators are matched based on their attribute name. For example, `"singledispatch"` will match any of the following: * `import functools; @functools.singledispatch` - * `import functools as fnctls; @fnctls.singledispatch` + * `import functools as ; @.singledispatch` * `from functools import singledispatch; @singledispatch` **NOTE:** Deeper imports, such as `a.b.singledispatch` are not supported. @@ -126,7 +127,7 @@ Comma-separated list of decorators flake8-annotations should consider as [`typin Decorators are matched based on their attribute name. For example, `"overload"` will match any of the following: * `import typing; @typing.overload` - * `import typing as t; @t.overload` + * `import typing as ; @.overload` * `from typing import overload; @overload` **NOTE:** Deeper imports, such as `a.b.overload` are not supported. @@ -188,7 +189,6 @@ Will not raise linting errors for missing annotations for the arguments & return Decorator(s) to treat as `typing.overload` may be specified by the [`--overload-decorators`](#--overload-decorators-liststr) configuration option. ## Caveats for PEP 484-style Type Comments - ### Mixing argument-level and function-level type comments Support is provided for mixing argument-level and function-level type comments. @@ -237,6 +237,14 @@ Will show `arg1` as missing a type hint. **Deprecation notice**: Explicit support for utilization of ellipses as placeholders will be removed in version `3.0`. See [this issue](https://github.com/sco1/flake8-annotations/issues/95) for more information. +## Dynamic Typing Caveats +Support is only provided for the following patterns: + * `from typing import any; foo: Any` + * `import typing; foo: typing.Any` + * `import typing as ; foo: .Any` + +Nested dynamic types (e.g. `typing.Tuple[typing.Any]`) and redefinition (e.g. `from typing import Any as Foo`) will not be identified. + ## Contributing ### Development Environment diff --git a/flake8_annotations/ast_walker.py b/flake8_annotations/ast_walker.py index 3bf6b12..c7080c6 100644 --- a/flake8_annotations/ast_walker.py +++ b/flake8_annotations/ast_walker.py @@ -40,6 +40,7 @@ class Argument: "has_type_annotation", "has_3107_annotation", "has_type_comment", + "is_dynamically_typed", ] def __init__( @@ -51,6 +52,7 @@ def __init__( has_type_annotation: bool = False, has_3107_annotation: bool = False, has_type_comment: bool = False, + is_dynamically_typed: bool = False, ): self.argname = argname self.lineno = lineno @@ -59,6 +61,7 @@ def __init__( self.has_type_annotation = has_type_annotation self.has_3107_annotation = has_3107_annotation self.has_type_comment = has_type_comment + self.is_dynamically_typed = is_dynamically_typed def __str__(self) -> str: """ @@ -87,12 +90,13 @@ def __repr__(self) -> str: f"annotation_type={annotation_type}, " f"has_type_annotation={self.has_type_annotation}, " f"has_3107_annotation={self.has_3107_annotation}, " - f"has_type_comment={self.has_type_comment}" + f"has_type_comment={self.has_type_comment}, " + f"is_dynamically_typed={self.is_dynamically_typed}" ")" ) @classmethod - def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> "Argument": + def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> Argument: """Create an Argument object from an ast.arguments node.""" annotation_type = AnnotationType[annotation_type_name] new_arg = cls(node.arg, node.lineno, node.col_offset, annotation_type) @@ -102,12 +106,34 @@ def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> "Argument": new_arg.has_type_annotation = True new_arg.has_3107_annotation = True + if cls._is_annotated_any(node.annotation): + new_arg.is_dynamically_typed = True + if node.type_comment: new_arg.has_type_annotation = True new_arg.has_type_comment = True return new_arg + @staticmethod + def _is_annotated_any(arg_expr: ast.expr) -> bool: + """ + Check if the provided expression node is annotated with `typing.Any`. + + Support is provided for the following patterns: + * `from typing import Any; foo: Any` + * `import typing; foo: typing.Any` + * `import typing as ; foo: .Any` + """ + if isinstance(arg_expr, ast.Name): + if arg_expr.id == "Any": + return True + elif isinstance(arg_expr, ast.Attribute): + if arg_expr.attr == "Any": + return True + + return False + class Function: """ @@ -274,7 +300,7 @@ def __repr__(self) -> str: @classmethod def from_function_node( cls, node: AST_FUNCTION_TYPES, lines: t.List[str], **kwargs: t.Any - ) -> "Function": + ) -> Function: """ Create an Function object from ast.FunctionDef or ast.AsyncFunctionDef nodes. @@ -321,6 +347,9 @@ def from_function_node( return_arg.has_3107_annotation = True new_function.is_return_annotated = True + if Argument._is_annotated_any(node.returns): + return_arg.is_dynamically_typed = True + new_function.args.append(return_arg) # Type comments in-line with input arguments are handled by the Argument class @@ -388,7 +417,7 @@ def _single_line_colon_seeker(node: AST_FUNCTION_TYPES, line: str) -> t.Tuple[in return node.lineno, def_end_col_offset @staticmethod - def try_type_comment(func_obj: "Function", node: AST_FUNCTION_TYPES) -> "Function": + def try_type_comment(func_obj: Function, node: AST_FUNCTION_TYPES) -> Function: """ Attempt to infer type hints from a function-level type comment. @@ -419,7 +448,7 @@ def try_type_comment(func_obj: "Function", node: AST_FUNCTION_TYPES) -> "Functio @staticmethod def _maybe_inject_class_argument( - hint_tree: ast.FunctionType, func_obj: "Function" + hint_tree: ast.FunctionType, func_obj: Function ) -> ast.FunctionType: """ Inject `self` or `cls` args into a type comment to align with PEP 3107-style annotations. diff --git a/flake8_annotations/checker.py b/flake8_annotations/checker.py index 835db11..801cdbe 100644 --- a/flake8_annotations/checker.py +++ b/flake8_annotations/checker.py @@ -91,7 +91,8 @@ def run(self) -> t.Generator[FORMATTED_ERROR, None, None]: # Iterate over annotated args to detect mixing of type annotations and type comments # Emit this only once per function definition - for arg in function.get_annotated_arguments(): + annotated_args = function.get_annotated_arguments() + for arg in annotated_args: if arg.has_type_comment: has_type_comment = True @@ -103,6 +104,14 @@ def run(self) -> t.Generator[FORMATTED_ERROR, None, None]: yield error_codes.ANN301.from_function(function).to_flake8() break + # Iterate over the annotated args to look for `typing.Any`` annotations + # We could combine this with the above loop but I'd rather not add even more sentinels + # unless we'd notice a significant enough performance impact + for arg in annotated_args: + if arg.is_dynamically_typed: + # Always yield these and let flake8 take care of ignoring + yield error_codes.ANN401.from_argument(arg).to_flake8() + # Before we iterate over the function's missing annotations, check to see if it's the # closing function def in a series of `typing.overload` decorated functions. if last_overload_decorated_function_name == function.name: @@ -119,15 +128,16 @@ def run(self) -> t.Generator[FORMATTED_ERROR, None, None]: if self.suppress_none_returning: # Skip yielding return errors if the function has only `None` returns # This includes the case of no returns. - if not arg.has_type_annotation and function.has_only_none_returns: + if function.has_only_none_returns: continue if self.mypy_init_return: # Skip yielding return errors for `__init__` if at least one argument is # annotated if function.is_class_method and function.name == "__init__": - # If we've gotten here, then `function.get_annotated_arguments` won't - # contain `return`, since we're iterating over missing annotations - if function.get_annotated_arguments(): + # If we've gotten here, then our annotated args won't contain "return" + # since we're in a logic check for missing "return". So if our annotated + # are non-empty, then __init__ has at least one annotated argument + if annotated_args: continue # If the `--suppress-dummy-args` flag is `True`, skip yielding errors for any diff --git a/testing/test_flake8_actually_runs_checker.py b/testing/test_flake8_actually_runs_checker.py index 45f6357..c0cd79c 100644 --- a/testing/test_flake8_actually_runs_checker.py +++ b/testing/test_flake8_actually_runs_checker.py @@ -4,6 +4,11 @@ def test_checker_runs() -> None: """Test that the checker is properly registered by Flake8 as needing to run on the input src.""" substr = "ANN001 Missing type annotation for function argument 'x'" - p = run(["flake8", "-"], stdout=PIPE, input="def bar(x) -> None:\n pass\n", encoding="ascii") + p = run( + ["flake8", "--select=ANN", "-"], + stdout=PIPE, + input="def bar(x) -> None:\n pass\n", + encoding="ascii", + ) assert substr in p.stdout diff --git a/testing/test_opinionated_any.py b/testing/test_opinionated_any.py index e69de29..3c7b65d 100644 --- a/testing/test_opinionated_any.py +++ b/testing/test_opinionated_any.py @@ -0,0 +1,114 @@ +from functools import partial +from subprocess import PIPE, run +from textwrap import dedent + +import pytest + +from flake8_annotations import error_codes +from testing.helpers import check_source + +ERR = partial(error_codes.ANN401, lineno=3) + + +TEST_CASES = ( + # Type annotations + ( + dedent( + """\ + from typing import Any + + def foo(a: Any) -> None: + ... + """ + ), + ), + ( + dedent( + """\ + from typing import Any + + def foo(a: int) -> Any: + ... + """ + ), + ), + ( + dedent( + """\ + import typing as t + + def foo(a: t.Any) -> None: + ... + """ + ), + ), + ( + dedent( + """\ + import typing as t + + def foo(a: int) -> t.Any: + ... + """ + ), + ), + # Type comments + ( + dedent( + """\ + def foo( + a # type: Any + ): + # type: (...) -> int + ... + """ + ), + ), + ( + dedent( + """\ + def foo(a): + # type: (int) -> Any + ... + """ + ), + ), +) + + +@pytest.mark.parametrize(("src",), TEST_CASES) +def test_dynamic_typing_errors(src: str) -> None: + found_errors = list(check_source(src)) + + assert len(found_errors) == 1 + + _, _, err_msg, _ = found_errors[0] + assert "ANN401" in err_msg + + +def test_ANN401_ignored_default() -> None: + inp = dedent( + """\ + import typing + def foo(a: typing.Any) -> None: + ... + """ + ) + p = run(["flake8", "--select=ANN", "-"], stdout=PIPE, input=inp, encoding="ascii") + + assert len(p.stdout) == 0 + + +def test_ANN401_fire_when_selected() -> None: + inp = dedent( + """\ + import typing + def foo(a: typing.Any) -> None: + ... + """ + ) + p = run( + ["flake8", "--select=ANN", "--ignore=''", "-"], stdout=PIPE, input=inp, encoding="ascii" + ) + + assert "ANN401" in p.stdout From a690f5ef9285d6e33491b0563550b15a5538bb7f Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Fri, 18 Mar 2022 12:56:32 -0400 Subject: [PATCH 5/9] Switch `Argument` and `Function` classes to dataclasses Remove `repr` testing, we don't need to test the stdlib ourselves --- flake8_annotations/__init__.py | 5 - flake8_annotations/ast_walker.py | 152 +++--------------- .../object_formatting_test_cases.py | 49 ------ testing/test_object_formatting.py | 6 - 4 files changed, 25 insertions(+), 187 deletions(-) diff --git a/flake8_annotations/__init__.py b/flake8_annotations/__init__.py index d4cdaf0..aa21450 100644 --- a/flake8_annotations/__init__.py +++ b/flake8_annotations/__init__.py @@ -5,9 +5,4 @@ else: PY_GTE_38 = False -if sys.version_info >= (3, 11): - PY_GTE_311 = True -else: - PY_GTE_311 = False - __version__ = "2.7.0" diff --git a/flake8_annotations/ast_walker.py b/flake8_annotations/ast_walker.py index c7080c6..db34a15 100644 --- a/flake8_annotations/ast_walker.py +++ b/flake8_annotations/ast_walker.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing as t +from dataclasses import dataclass from itertools import zip_longest -from flake8_annotations import PY_GTE_38, PY_GTE_311 +from flake8_annotations import PY_GTE_38 from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType # Check if we can use the stdlib ast module instead of typed_ast; stdlib ast gains native type @@ -29,39 +30,18 @@ AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES +@dataclass(slots=True) class Argument: """Represent a function argument & its metadata.""" - __slots__ = [ - "argname", - "lineno", - "col_offset", - "annotation_type", - "has_type_annotation", - "has_3107_annotation", - "has_type_comment", - "is_dynamically_typed", - ] - - def __init__( - self, - argname: str, - lineno: int, - col_offset: int, - annotation_type: AnnotationType, - has_type_annotation: bool = False, - has_3107_annotation: bool = False, - has_type_comment: bool = False, - is_dynamically_typed: bool = False, - ): - self.argname = argname - self.lineno = lineno - self.col_offset = col_offset - self.annotation_type = annotation_type - self.has_type_annotation = has_type_annotation - self.has_3107_annotation = has_3107_annotation - self.has_type_comment = has_type_comment - self.is_dynamically_typed = is_dynamically_typed + argname: str + lineno: int + col_offset: int + annotation_type: AnnotationType + has_type_annotation: bool = False + has_3107_annotation: bool = False + has_type_comment: bool = False + is_dynamically_typed: bool = False def __str__(self) -> str: """ @@ -72,29 +52,6 @@ def __str__(self) -> str: """ return f"" - def __repr__(self) -> str: - """Format the Argument object into its "official" representation.""" - # Python 3.11 introduces a backwards-incompatible change to Enum str/repr - # See: https://bugs.python.org/issue40066 - # See: https://bugs.python.org/issue44559 - if PY_GTE_311: - annotation_type = repr(self.annotation_type) - else: - annotation_type = str(self.annotation_type) - - return ( - f"Argument(" - f"argname={self.argname!r}, " - f"lineno={self.lineno}, " - f"col_offset={self.col_offset}, " - f"annotation_type={annotation_type}, " - f"has_type_annotation={self.has_type_annotation}, " - f"has_3107_annotation={self.has_3107_annotation}, " - f"has_type_comment={self.has_type_comment}, " - f"is_dynamically_typed={self.is_dynamically_typed}" - ")" - ) - @classmethod def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> Argument: """Create an Argument object from an ast.arguments node.""" @@ -135,6 +92,7 @@ def _is_annotated_any(arg_expr: ast.expr) -> bool: return False +@dataclass(slots=True) class Function: """ Represent a function and its relevant metadata. @@ -144,49 +102,18 @@ class Function: aligns with ast's naming convention. """ - __slots__ = [ - "name", - "lineno", - "col_offset", - "function_type", - "is_class_method", - "class_decorator_type", - "is_return_annotated", - "has_type_comment", - "has_only_none_returns", - "is_nested", - "decorator_list", - "args", - ] - - def __init__( - self, - name: str, - lineno: int, - col_offset: int, - function_type: FunctionType = FunctionType.PUBLIC, - is_class_method: bool = False, - class_decorator_type: t.Union[ClassDecoratorType, None] = None, - is_return_annotated: bool = False, - has_type_comment: bool = False, - has_only_none_returns: bool = True, - is_nested: bool = False, - *, - decorator_list: t.List[AST_DECORATOR_NODES], - args: t.List[Argument], - ): - self.name = name - self.lineno = lineno - self.col_offset = col_offset - self.function_type = function_type - self.is_class_method = is_class_method - self.class_decorator_type = class_decorator_type - self.is_return_annotated = is_return_annotated - self.has_type_comment = has_type_comment - self.has_only_none_returns = has_only_none_returns - self.is_nested = is_nested - self.decorator_list = decorator_list - self.args = args + name: str + lineno: int + col_offset: int + decorator_list: t.List[AST_DECORATOR_NODES] + args: t.List[Argument] + function_type: FunctionType = FunctionType.PUBLIC + is_class_method: bool = False + class_decorator_type: t.Union[ClassDecoratorType, None] = None + is_return_annotated: bool = False + has_type_comment: bool = False + has_only_none_returns: bool = True + is_nested: bool = False def is_fully_annotated(self) -> bool: """ @@ -268,35 +195,6 @@ def __str__(self) -> str: return f"" - def __repr__(self) -> str: - """Format the Function object into its "official" representation.""" - # Python 3.11 introduces a backwards-incompatible change to Enum str/repr - # See: https://bugs.python.org/issue40066 - # See: https://bugs.python.org/issue44559 - if PY_GTE_311: - function_type = repr(self.function_type) - class_decorator_type = repr(self.class_decorator_type) - else: - function_type = str(self.function_type) - class_decorator_type = str(self.class_decorator_type) - - return ( - f"Function(" - f"name={self.name!r}, " - f"lineno={self.lineno}, " - f"col_offset={self.col_offset}, " - f"function_type={function_type}, " - f"is_class_method={self.is_class_method}, " - f"class_decorator_type={class_decorator_type}, " - f"is_return_annotated={self.is_return_annotated}, " - f"has_type_comment={self.has_type_comment}, " - f"has_only_none_returns={self.has_only_none_returns}, " - f"is_nested={self.is_nested}, " - f"decorator_list={self.decorator_list}, " - f"args={self.args}" - ")" - ) - @classmethod def from_function_node( cls, node: AST_FUNCTION_TYPES, lines: t.List[str], **kwargs: t.Any @@ -422,7 +320,7 @@ def try_type_comment(func_obj: Function, node: AST_FUNCTION_TYPES) -> Function: Attempt to infer type hints from a function-level type comment. If a function is type commented it is assumed to have a return annotation, otherwise Python - will fail to parse the hint + will fail to parse the hint. """ # If we're in this function then the node is guaranteed to have a type comment, so we can # ignore mypy's complaint about an incompatible type for `node.type_comment` diff --git a/testing/test_cases/object_formatting_test_cases.py b/testing/test_cases/object_formatting_test_cases.py index 8797638..f7f18cf 100644 --- a/testing/test_cases/object_formatting_test_cases.py +++ b/testing/test_cases/object_formatting_test_cases.py @@ -10,7 +10,6 @@ class FormatTestCase(NamedTuple): test_object: Union[Argument, Function] str_output: str - repr_output: str # Define partial functions to simplify object creation @@ -21,61 +20,13 @@ class FormatTestCase(NamedTuple): "arg": FormatTestCase( test_object=arg(argname="test_arg"), str_output="", - repr_output=( - "Argument(" - "argname='test_arg', " - "lineno=0, " - "col_offset=0, " - "annotation_type=AnnotationType.ARGS, " - "has_type_annotation=False, " - "has_3107_annotation=False, " - "has_type_comment=False, " - "is_dynamically_typed=False" - ")" - ), ), "func_no_args": FormatTestCase( test_object=func(args=[arg(argname="return")]), str_output="]>", - repr_output=( - "Function(" - "name='test_func', " - "lineno=0, " - "col_offset=0, " - "function_type=FunctionType.PUBLIC, " - "is_class_method=False, " - "class_decorator_type=None, " - "is_return_annotated=False, " - "has_type_comment=False, " - "has_only_none_returns=True, " - "is_nested=False, " - "decorator_list=[], " - "args=[Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False)]" - ")" - ), ), "func_has_arg": FormatTestCase( test_object=func(args=[arg(argname="foo"), arg(argname="return")]), str_output=", ]>", - repr_output=( - "Function(" - "name='test_func', " - "lineno=0, " - "col_offset=0, " - "function_type=FunctionType.PUBLIC, " - "is_class_method=False, " - "class_decorator_type=None, " - "is_return_annotated=False, " - "has_type_comment=False, " - "has_only_none_returns=True, " - "is_nested=False, " - "decorator_list=[], " - "args=[Argument(argname='foo', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False), " - "Argument(argname='return', lineno=0, col_offset=0, annotation_type=AnnotationType.ARGS, " - "has_type_annotation=False, has_3107_annotation=False, has_type_comment=False, is_dynamically_typed=False)]" - ")" - ), ), } diff --git a/testing/test_object_formatting.py b/testing/test_object_formatting.py index fadfd3c..98e2b6f 100644 --- a/testing/test_object_formatting.py +++ b/testing/test_object_formatting.py @@ -25,9 +25,3 @@ def test_str(build_test_cases: Tuple[FormatTestCase, str]) -> None: """Test the __str__ method for Argument and Function objects.""" test_case, failure_msg = build_test_cases check.equal(str(test_case.test_object), test_case.str_output, msg=failure_msg) - - -def test_repr(build_test_cases: Tuple[FormatTestCase, str]) -> None: - """Test the __repr__ method for Argument and Function objects.""" - test_case, failure_msg = build_test_cases - check.equal(repr(test_case.test_object), test_case.repr_output, msg=failure_msg) From 5767362ed77ff3d1dc580753efdcad1dcf14aac6 Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Fri, 18 Mar 2022 13:46:34 -0400 Subject: [PATCH 6/9] Add type comment support to `ANN401` check --- flake8_annotations/ast_walker.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/flake8_annotations/ast_walker.py b/flake8_annotations/ast_walker.py index db34a15..670595b 100644 --- a/flake8_annotations/ast_walker.py +++ b/flake8_annotations/ast_walker.py @@ -70,10 +70,13 @@ def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> Argument: new_arg.has_type_annotation = True new_arg.has_type_comment = True + if cls._is_annotated_any(node.type_comment): + new_arg.is_dynamically_typed = True + return new_arg @staticmethod - def _is_annotated_any(arg_expr: ast.expr) -> bool: + def _is_annotated_any(arg_expr: t.Union[ast.expr, str]) -> bool: """ Check if the provided expression node is annotated with `typing.Any`. @@ -81,6 +84,9 @@ def _is_annotated_any(arg_expr: ast.expr) -> bool: * `from typing import Any; foo: Any` * `import typing; foo: typing.Any` * `import typing as ; foo: .Any` + + Type comments are also supported. Inline type comments are assumed to be passed here as + `str`, and function-level type comments are assumed to be passed as `ast.expr`. """ if isinstance(arg_expr, ast.Name): if arg_expr.id == "Any": @@ -88,6 +94,9 @@ def _is_annotated_any(arg_expr: ast.expr) -> bool: elif isinstance(arg_expr, ast.Attribute): if arg_expr.attr == "Any": return True + elif isinstance(arg_expr, str): + if arg_expr.split(".", maxsplit=1)[-1] == "Any": + return True return False @@ -337,10 +346,15 @@ def try_type_comment(func_obj: Function, node: AST_FUNCTION_TYPES) -> Function: arg.has_type_annotation = True arg.has_type_comment = True + if Argument._is_annotated_any(hint_comment): + arg.is_dynamically_typed = True + # Return arg is always last func_obj.args[-1].has_type_annotation = True func_obj.args[-1].has_type_comment = True func_obj.is_return_annotated = True + if Argument._is_annotated_any(hint_tree.returns): + arg.is_dynamically_typed = True return func_obj From 7ea9922ecd735128e888a3916a69035dfff9e4eb Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Fri, 18 Mar 2022 13:51:52 -0400 Subject: [PATCH 7/9] Swap `attrs` in for `dataclass` Dataclasses only become slotted in Python 3.10, whoops --- flake8_annotations/ast_walker.py | 6 +++--- poetry.lock | 10 +++++----- pyproject.toml | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/flake8_annotations/ast_walker.py b/flake8_annotations/ast_walker.py index 670595b..39fffa3 100644 --- a/flake8_annotations/ast_walker.py +++ b/flake8_annotations/ast_walker.py @@ -1,9 +1,9 @@ from __future__ import annotations import typing as t -from dataclasses import dataclass from itertools import zip_longest +from attrs import define from flake8_annotations import PY_GTE_38 from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType @@ -30,7 +30,7 @@ AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES -@dataclass(slots=True) +@define(slots=True) class Argument: """Represent a function argument & its metadata.""" @@ -101,7 +101,7 @@ def _is_annotated_any(arg_expr: t.Union[ast.expr, str]) -> bool: return False -@dataclass(slots=True) +@define(slots=True) class Function: """ Represent a function and its relevant metadata. diff --git a/poetry.lock b/poetry.lock index 38d7a06..3d505ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -10,7 +10,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -433,7 +433,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.0" +version = "7.1.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -603,7 +603,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "ed1d2e71802723bc6fc1a7d75897fb78130015d0709bf35ccfd6c8b92f31e10f" +content-hash = "1968939a5b73ef75af5805e0b8c768b4b7b2aa055791aa665a6b4537a02f6a00" [metadata.files] atomicwrites = [ @@ -839,8 +839,8 @@ pyparsing = [ {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ - {file = "pytest-7.1.0-py3-none-any.whl", hash = "sha256:b555252a95bbb2a37a97b5ac2eb050c436f7989993565f5e0c9128fcaacadd0e"}, - {file = "pytest-7.1.0.tar.gz", hash = "sha256:f1089d218cfcc63a212c42896f1b7fbf096874d045e1988186861a1a87d27b47"}, + {file = "pytest-7.1.1-py3-none-any.whl", hash = "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea"}, + {file = "pytest-7.1.1.tar.gz", hash = "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63"}, ] pytest-check = [ {file = "pytest_check-1.0.4-py3-none-any.whl", hash = "sha256:aacc9500178611f8ad075a7c46ce8de8ac34f05270eee28f223fb0c8622fbfbe"}, diff --git a/pyproject.toml b/pyproject.toml index d40dd2a..f2af804 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ include = [ [tool.poetry.dependencies] python = "^3.7" +attrs = "^21.4" flake8 = ">=3.7" typed-ast = {version="^1.4,<2.0", python="<3.8"} From 9d9f313c3b93332de6738552584f4b0b0732acce Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Fri, 18 Mar 2022 14:02:07 -0400 Subject: [PATCH 8/9] Bump ver --- .bumpversion.cfg | 6 +--- CHANGELOG.md | 56 ++++++++++++++++++-------------- README.md | 2 +- flake8_annotations/__init__.py | 2 +- flake8_annotations/ast_walker.py | 1 + pyproject.toml | 2 +- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f40d5bd..c0468ad 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.7.0 +current_version = 2.8.0 commit = False [bumpversion:file:pyproject.toml] @@ -9,7 +9,3 @@ replace = version = "{new_version}" [bumpversion:file:flake8_annotations/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" - -[bumpversion:file:README.md] -search = flake8-annotations: {current_version} -replace = flake8-annotations: {new_version} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eadfb9..86ff358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (``.``.``) +## [v2.8.0] +### Added +* #131 Add the `ANN4xx` error level for opinionated warnings that are disabled by default. +* #131 Add `ANN401` for use of `typing.Any` as an argument annotation. + +### Changed +* Python 3.7 is now the minimum supported version + ## [v2.7.0] ### Added * #122 Add support for Flake8 v4.x @@ -59,8 +67,8 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (` ```bash $ flake8 --version -4.0.1 (flake8-annotations: 2.7.0, mccabe: 0.6.1, pycodestyle: 2.8.0, pyflakes:2.4.0) CPython 3.10.2 on Darwin +4.0.1 (flake8-annotations: 2.8.0, mccabe: 0.6.1, pycodestyle: 2.8.0, pyflakes:2.4.0) CPython 3.10.2 on Darwin ``` diff --git a/flake8_annotations/__init__.py b/flake8_annotations/__init__.py index aa21450..25e2e73 100644 --- a/flake8_annotations/__init__.py +++ b/flake8_annotations/__init__.py @@ -5,4 +5,4 @@ else: PY_GTE_38 = False -__version__ = "2.7.0" +__version__ = "2.8.0" diff --git a/flake8_annotations/ast_walker.py b/flake8_annotations/ast_walker.py index 39fffa3..3a28cb9 100644 --- a/flake8_annotations/ast_walker.py +++ b/flake8_annotations/ast_walker.py @@ -4,6 +4,7 @@ from itertools import zip_longest from attrs import define + from flake8_annotations import PY_GTE_38 from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType diff --git a/pyproject.toml b/pyproject.toml index f2af804..7ee5bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flake8-annotations" -version = "2.7.0" +version = "2.8.0" description = "Flake8 Type Annotation Checks" license = "MIT" readme = "README.md" From ef0c9758047854a3a02f1a7c8893ff3e48f9d7dc Mon Sep 17 00:00:00 2001 From: "S. Co1" Date: Wed, 30 Mar 2022 12:08:50 -0400 Subject: [PATCH 9/9] Clean up internal flake8 warnings --- flake8_annotations/checker.py | 18 +++++++++--------- tox.ini | 5 ++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/flake8_annotations/checker.py b/flake8_annotations/checker.py index 801cdbe..54b2509 100644 --- a/flake8_annotations/checker.py +++ b/flake8_annotations/checker.py @@ -159,7 +159,7 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover parse_from_config=True, help=( "Suppress ANN200-level errors for functions that contain no return statement or " - "contain only bare return statements. (Default: False)" + "contain only bare return statements. (Default: %(default)s)" ), ) @@ -169,7 +169,7 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover action="store_true", parse_from_config=True, help=( - "Suppress ANN000-level errors for dummy arguments, defined as '_'. (Default: False)" + "Suppress ANN000-level errors for dummy arguments, defined as '_'. (Default: %(default)s)" # noqa: E501 ), ) @@ -178,7 +178,7 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover default=False, action="store_true", parse_from_config=True, - help="Suppress all errors for dynamically typed functions. (Default: False)", + help="Suppress all errors for dynamically typed functions. (Default: %(default)s)", ) parser.add_option( @@ -186,7 +186,7 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover default=False, action="store_true", parse_from_config=True, - help="Suppress all errors for dynamically typed nested functions. (Default: False)", + help="Suppress all errors for dynamically typed nested functions. (Default: %(default)s)", # noqa: E501 ) parser.add_option( @@ -196,7 +196,7 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover parse_from_config=True, help=( "Allow omission of a return type hint for __init__ if at least one argument is " - "annotated. (Default: False)" + "annotated. (Default: %(default)s)" ), ) @@ -204,12 +204,12 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover "--dispatch-decorators", default=_DEFAULT_DISPATCH_DECORATORS, action="store", - type="string", + type=str, parse_from_config=True, comma_separated_list=True, help=( "Comma-separated list of decorators flake8-annotations should consider as dispatch " - "decorators. (Default: %default)" + "decorators. (Default: %(default)s)" ), ) @@ -217,12 +217,12 @@ def add_options(cls, parser: OptionManager) -> None: # pragma: no cover "--overload-decorators", default=_DEFAULT_OVERLOAD_DECORATORS, action="store", - type="string", + type=str, parse_from_config=True, comma_separated_list=True, help=( "Comma-separated list of decorators flake8-annotations should consider as " - "typing.overload decorators. (Default: %default)" + "typing.overload decorators. (Default: %(default)s)" ), ) diff --git a/tox.ini b/tox.ini index 82ddae4..5426099 100644 --- a/tox.ini +++ b/tox.ini @@ -59,7 +59,10 @@ commands = coverage erase [testenv:cog] commands = cog -r README.md -deps = cogapp +deps = + cogapp + attrs + flake8 [gh-actions] # For tox GHA python =