From 6aceee2e4c0d6b55dec6b71bf045a74cfffe0255 Mon Sep 17 00:00:00 2001 From: Christopher Dignam Date: Sun, 13 Jun 2021 21:02:27 -0400 Subject: [PATCH] add rule `prefer-simpler-iterator` --- flake8_pie/__init__.py | 15 +++ flake8_pie/pie805_prefer_simple_iterator.py | 110 ++++++++++++++++++ .../test_pie805_prefer_simple_iterator.py | 86 ++++++++++++++ poetry.lock | 15 ++- pyproject.toml | 5 +- 5 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 flake8_pie/pie805_prefer_simple_iterator.py create mode 100644 flake8_pie/tests/test_pie805_prefer_simple_iterator.py diff --git a/flake8_pie/__init__.py b/flake8_pie/__init__.py index d710be8..bf5a591 100644 --- a/flake8_pie/__init__.py +++ b/flake8_pie/__init__.py @@ -36,6 +36,10 @@ pie803_prefer_logging_interpolation, ) from flake8_pie.pie804_no_unnecessary_dict_kwargs import pie804_no_dict_kwargs +from flake8_pie.pie805_prefer_simple_iterator import ( + pie805_prefer_simple_iterator_for, + pie805_prefer_simple_iterator_generator, +) @dataclass(frozen=True) @@ -61,6 +65,7 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: def visit_For(self, node: ast.For) -> None: self._visit_body(node) self._visit_body(BodyNode(node.orelse)) + pie805_prefer_simple_iterator_for(node, self.errors) self.generic_visit(node) def visit_AsyncFor(self, node: ast.AsyncFor) -> None: @@ -113,6 +118,11 @@ def visit_Expr(self, node: ast.Expr) -> None: self.generic_visit(node) + def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None: + pie805_prefer_simple_iterator_generator(node, self.errors) + + self.generic_visit(node) + def visit_If(self, node: ast.If) -> None: pie787_no_len_condition(node, self.errors) pie789_prefer_isinstance_type_compare(node, self.errors) @@ -147,6 +157,11 @@ def _visit_body(self, node: Body) -> None: pie799_prefer_col_init(node, self.errors) pie801_prefer_simple_return(node, self.errors) + def visit_ListComp(self, node: ast.ListComp) -> None: + pie805_prefer_simple_iterator_generator(node, self.errors) + + self.generic_visit(node) + def visit_Module(self, node: ast.Module) -> None: self._visit_body(node) self.generic_visit(node) diff --git a/flake8_pie/pie805_prefer_simple_iterator.py b/flake8_pie/pie805_prefer_simple_iterator.py new file mode 100644 index 0000000..8754568 --- /dev/null +++ b/flake8_pie/pie805_prefer_simple_iterator.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import ast +import string +from collections.abc import Iterable +from typing import NamedTuple + +from typing_extensions import TypeGuard + +from flake8_pie.base import Error + +KNOWN_FUNCTIONS = {"items"} + + +def is_name_list(val: Iterable[ast.expr]) -> TypeGuard[Iterable[ast.Name]]: + return all(isinstance(x, ast.Name) for x in val) + + +class UsedVarRes(NamedTuple): + method: str + var: str + + +def get_used_var(vars: Iterable[ast.Name]) -> UsedVarRes | None: + for idx, var in enumerate(vars): + if not var.id.startswith("_"): + if idx == 0: + method = "keys" + elif idx == 1: + method = "values" + else: + raise ValueError(f"Unexpected index: {idx}") + + return UsedVarRes(method=method, var=var.id) + return None + + +def has_unused_var(vars: Iterable[ast.Name]) -> bool: + return any(x.id.startswith("_") for x in vars) + + +def pie805_prefer_simple_iterator_for(node: ast.For, errors: list[Error]) -> None: + if ( + isinstance(node.target, ast.Tuple) + and len(node.target.elts) == 2 + and is_name_list(node.target.elts) + and has_unused_var(node.target.elts) + ): + res = get_used_var(node.target.elts) + if res is None: + return + + if ( + isinstance(node.iter, ast.Call) + and isinstance(node.iter.func, ast.Attribute) + and node.iter.func.attr == "items" + ): + errors.append( + PIE805( + lineno=node.lineno, + col_offset=node.col_offset, + suggestion=f"use `for {res.var} in foo.{res.method}()`", + ) + ) + + if ( + isinstance(node.iter, ast.Call) + and isinstance(node.iter.func, ast.Name) + and node.iter.func.id == "enumerate" + ): + errors.append( + PIE805( + lineno=node.lineno, + col_offset=node.col_offset, + suggestion=f"use `for {res.var} in enumerate(...)`", + ) + ) + + +def pie805_prefer_simple_iterator_generator( + node: ast.GeneratorExp | ast.ListComp, errors: list[Error] +) -> None: + for comprehension in node.generators: + if ( + isinstance(comprehension.iter, ast.Call) + and isinstance(comprehension.iter.func, ast.Attribute) + and comprehension.iter.func.attr == "items" + and isinstance(comprehension.target, ast.Tuple) + and len(comprehension.target.elts) == 2 + and is_name_list(comprehension.target.elts) + and has_unused_var(comprehension.target.elts) + ): + res = get_used_var(comprehension.target.elts) + if res is None: + continue + errors.append( + PIE805( + lineno=node.lineno, + col_offset=node.col_offset, + suggestion=f"use `for {res.var} in foo.{res.method}()`", + ) + ) + + +def PIE805(lineno: int, col_offset: int, suggestion: str) -> Error: + return Error( + lineno=lineno, + col_offset=col_offset, + message=f"PIE805: prefer-simple-iterator: {suggestion}", + ) diff --git a/flake8_pie/tests/test_pie805_prefer_simple_iterator.py b/flake8_pie/tests/test_pie805_prefer_simple_iterator.py new file mode 100644 index 0000000..11d8b21 --- /dev/null +++ b/flake8_pie/tests/test_pie805_prefer_simple_iterator.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import ast + +import pytest + +from flake8_pie import Flake8PieCheck +from flake8_pie.base import Error +from flake8_pie.pie805_prefer_simple_iterator import PIE805 +from flake8_pie.tests.utils import ex, to_errors + +EXAMPLES = [ + ex( + code=""" +for _idx, foo in enumerate(bar): + ... +""", + errors=[ + PIE805(lineno=2, col_offset=0, suggestion="use `for foo in enumerate(...)`") + ], + ), + ex( + code=""" +for _key, value in foo.items(): + ... +""", + errors=[ + PIE805(lineno=2, col_offset=0, suggestion="use `for value in foo.values()`") + ], + ), + ex( + code=""" +for key, _value in foo.items(): + ... +""", + errors=[ + PIE805(lineno=2, col_offset=0, suggestion="use `for key in foo.keys()`") + ], + ), + ex( + code=""" +fields = [ + k for k, _v in serialize(user).fields.items() if k != "internal" +] +""", + errors=[PIE805(lineno=3, col_offset=4, suggestion="use `for k in foo.keys()`")], + ), + ex( + code=""" +users = ( + User(data) + for _id, data in user_map.items() + for f, _y in blah.items() +) +""", + errors=[ + PIE805(lineno=3, col_offset=4, suggestion="use `for data in foo.values()`"), + PIE805(lineno=3, col_offset=4, suggestion="use `for f in foo.keys()`"), + ], + ), + # we need to do usage analysis on the variables to determine if they are actually unused instead of looking at `_`. + ex( + code=""" +[dict(_name=_name, data=data) for _name, data in users.items()] +""", + errors=[ + PIE805(lineno=2, col_offset=1, suggestion="use `for data in foo.values()`") + ], + ), + ex( + code=""" +for key, value in foo.items(): + ... +for idx, foo in enumerate(bar): + ... +[k for k, v in users.items() if v == "guest"] +""", + errors=[], + ), +] + + +@pytest.mark.parametrize("code,errors", EXAMPLES) +def test_examples(code: str, errors: list[Error]) -> None: + expr = ast.parse(code) + assert to_errors(Flake8PieCheck(expr, filename="foo.py").run()) == errors diff --git a/poetry.lock b/poetry.lock index 539e09b..980437d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -242,13 +242,17 @@ description = "Optional static typing for Python" name = "mypy" optional = false python-versions = ">=3.5" -version = "0.812" +version = "0.900" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" +toml = "*" typing-extensions = ">=3.7.4" +[package.dependencies.typed-ast] +python = "<3.8" +version = ">=1.4.0,<1.5.0" + [[package]] category = "dev" description = "Experimental type system extensions for programs checked with the mypy typechecker." @@ -403,7 +407,7 @@ python = "<3.8" version = ">=0.12" [package.dependencies.more-itertools] -python = ">2.7" +python = ">=2.8" version = ">=4.0.0" [[package]] @@ -513,6 +517,7 @@ tqdm = ">=4.14" [[package]] category = "dev" description = "a fork of Python 2 and 3 ast modules with type comment support" +marker = "python_version < \"3.8\"" name = "typed-ast" optional = false python-versions = "*" @@ -576,7 +581,7 @@ python-versions = ">=3.6" version = "3.4.1" [metadata] -content-hash = "b8839e8c1922368e02db6e279c248d4bd2974884d637f97c316f5ab3c6ba6e26" +content-hash = "9d61d5a78b1d756323ce14d134a986e2b24e6301fe777e8fa7ab303485d54371" python-versions = ">=3.7" [metadata.hashes] @@ -604,7 +609,7 @@ isort = ["54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", "6e jedi = ["18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", "92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] more-itertools = ["5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", "c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"] -mypy = ["0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e", "25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064", "28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c", "2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4", "33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97", "3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df", "499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8", "4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a", "552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56", "5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7", "61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6", "674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5", "7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a", "9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521", "9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564", "a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49", "abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66", "b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a", "cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119", "d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506", "d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c", "d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"] +mypy = ["07efc88486877d595cca7f7d237e6d04d1ba6f01dc8f74a81b716270f6770968", "0e703c0afe36511746513d168e1d2a52f88e2a324169b87a6b6a58901c3afcf3", "2220f97804890f3e6da3f849f81f3e56e367a2027a51dde5ce3b7ebb2ad3342b", "22f97de97373dd6180c4abee90b20c60780820284d2cdc5579927c0e37854cf6", "23100137579d718cd6f05d572574ca00701fa2bfc7b645ebc5130d93e2af3bee", "3be7c68fab8b318a2d5bcfac8e028dc77b9096ea1ec5594e9866c8fb57ae0296", "3f1d0601842c6b4248923963fc59a6fdd05dee0fddc8b07e30c508b6a269e68f", "41f082275a20e3eea48364915f7bc6ec5338be89db1ed8b2e570b9e3d12d4dc6", "42d66b3d716fe5e22b32915d1fa59e7183a0e02f00b337b834a596c1f5e37f01", "468b3918b26f81d003e8e9b788c62160acb885487cf4d83a3f22ba9061cb49e2", "6598e39cd5aa1a09d454ad39687b89cf3f3fd7cf1f9c3f81a1a2775f6f6b16f8", "65c78570329c54fb40f956f7645e2359af5da9d8c54baa44f461cdc7f4984108", "68fd1c1c1fc9b405f0ed6cfcd00541de7e83f41007419a125c20fa5db3881cb1", "7eb1e5820deb71e313aa2b5a5220803a9b2e3efa43475537a71d0ffed7495e1e", "80c96f97de241ee7383cfe646bfc51113a48089d50c33275af0033b98dee3b1c", "83adbf3f8c5023f4276557fbcb3b6306f9dce01783e8ac5f8c11fcb29f62e899", "9560b1f572cdaab43fdcdad5ef45138e89dc191729329db1b8ce5636f4cdeacf", "a0461da00ed23d17fcb04940db2b72f920435cf79be943564b717e378ffeeddf", "a354613b4cc6e0e9f1ba7811083cd8f63ccee97333d1df7594c438399c83249a", "c2f87505840c0f3557ea4aa5893f2459daf6516adac30b15d1d5cf567e0d7939", "d794f10b9f28d21af7a93054e217872aaf9b9ad1bd354ae5e1a3a923d734b73f", "d90c296cd5cdef86e720a0994d41d72c06d6ff8ab8fc6aaaf0ee6c675835d596", "e75f0c97cfe8d86da89b22ad7039f5af44b8f6b0af12bd2877791a92b4b9e987"] mypy-extensions = ["090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", "2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"] packaging = ["5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"] parso = ["12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398", "a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"] diff --git a/pyproject.toml b/pyproject.toml index 5ba2fac..2cb0d64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flake8-pie" -version = "0.13.0" +version = "0.14.0" description = "A flake8 extension that implements misc. lints" repository = "https://github.com/sbdchd/flake8-pie" authors = ["Steve Dignam "] @@ -29,9 +29,10 @@ twine = "^1.12" pytest-watch = "^4.2" pytest = "^4.0" ipython = "^7.2" -mypy = "^0.812.0" +mypy = "^0.900" astpretty = "^2.1" isort = "^4.3" +typing_extensions = "^3.10.0.0" [tool.poetry.plugins] [tool.poetry.plugins."flake8.extension"]