Repository: adamchainz/flake8-comprehensions
Pull request #282 (2020-10-23 09:03:30)

Restore C408 checking for dict with keyword-only arguments
Ref #249 where a user said they missed it.


Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 285cd08..edb1447 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -4,6 +4,9 @@ History
 
 * Support Python 3.9.
 * Move license from ISC to MIT License.
+* Partially reverted the change to ``C408`` to make it apply again to when
+  ``dict`` is called with keyword arguments, e.g. ``dict(a=1, b=2)`` will be
+  flagged to be rewritten in the literal form ``{"a": 1, "b": 2}``
 
 3.2.3 (2020-06-06)
 ------------------
diff --git a/README.rst b/README.rst
index 5637917..1c44752 100644
--- a/README.rst
+++ b/README.rst
@@ -140,12 +140,12 @@ The list of builtins that are checked for are:
 C408: Unnecessary (dict/list/tuple) call - rewrite as a literal.
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-It's slower to call these types than to construct them with literals, e.g.
-``dict()`` is slower than ``{}``, because the name ``dict`` must be looked up
-in the global scope in case it has been rebound, and it includes the overhead
-of a function call. Same for the other two builtin types here. For example:
+It's slower to call e.g. ``dict()`` than using the empty literal, because the
+name ``dict`` must be looked up in the global scope in case it has been
+rebound. Same for the other two basic types here. For example:
 
 * Rewrite ``dict()`` as ``{}``
+* Rewrite ``dict(a=1, b=2)`` as ``{}``
 * Rewrite ``list()`` as ``[]``
 * Rewrite ``tuple()`` as ``()``
 
diff --git a/src/flake8_comprehensions.py b/src/flake8_comprehensions.py
index ab7130c..ee349a1 100644
--- a/src/flake8_comprehensions.py
+++ b/src/flake8_comprehensions.py
@@ -44,6 +44,7 @@ class ComprehensionChecker:
         for node in ast.walk(self.tree):
             if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
                 num_positional_args = len(node.args)
+                num_keyword_args = len(node.keywords)
 
                 if (
                     num_positional_args == 1
@@ -201,8 +202,21 @@ class ComprehensionChecker:
 
                 elif (
                     num_positional_args == 0
-                    and node.func.id in ("tuple", "list", "dict")
-                    and len(node.keywords) == 0
+                    and not has_star_args(node)
+                    and not has_double_star_args(node)
+                    and node.func.id == "dict"
+                ):
+                    yield (
+                        node.lineno,
+                        node.col_offset,
+                        self.messages["C408"].format(type=node.func.id),
+                        type(self),
+                    )
+
+                elif (
+                    num_positional_args == 0
+                    and num_keyword_args == 0
+                    and node.func.id in ("tuple", "list")
                 ):
                     yield (
                         node.lineno,
@@ -334,6 +348,14 @@ class ComprehensionChecker:
                     )
 
 
+def has_star_args(call_node):
+    return any(isinstance(a, ast.Starred) for a in call_node.args)
+
+
+def has_double_star_args(call_node):
+    return any(k.arg is None for k in call_node.keywords)
+
+
 comp_type = {
     ast.DictComp: "dict",
     ast.ListComp: "list",


Test patch:
diff --git a/tests/test_flake8_comprehensions.py b/tests/test_flake8_comprehensions.py
index 0a3ab85..db1e9a2 100644
--- a/tests/test_flake8_comprehensions.py
+++ b/tests/test_flake8_comprehensions.py
@@ -597,6 +597,17 @@ def test_C408_pass_4(flake8dir):
 
 
 def test_C408_pass_5(flake8dir):
+    flake8dir.make_example_py(
+        """\
+        foo = [('foo', 2)]
+        dict(foo)
+    """
+    )
+    result = flake8dir.run_flake8()
+    assert result.out_lines == []
+
+
+def test_C408_pass_6(flake8dir):
     flake8dir.make_example_py(
         """\
         foo = {}
@@ -607,19 +618,24 @@ def test_C408_pass_5(flake8dir):
     assert result.out_lines == []
 
 
-def test_C408_pass_6(flake8dir):
+def test_C408_pass_7(flake8dir):
     flake8dir.make_example_py(
         """\
         foo = [1, 2]
-        list(*foo)
+        list(foo)
     """
     )
     result = flake8dir.run_flake8()
     assert result.out_lines == []
 
 
-def test_C408_pass_7(flake8dir):
-    flake8dir.make_example_py("dict(a=1)")
+def test_C408_pass_8(flake8dir):
+    flake8dir.make_example_py(
+        """\
+        foo = [1, 2]
+        list(*foo)
+    """
+    )
     result = flake8dir.run_flake8()
     assert result.out_lines == []
 
@@ -648,6 +664,14 @@ def test_C408_fail_3(flake8dir):
     ]
 
 
+def test_C408_fail_4(flake8dir):
+    flake8dir.make_example_py("dict(a=1)")
+    result = flake8dir.run_flake8()
+    assert result.out_lines == [
+        "./example.py:1:1: C408 Unnecessary dict call - rewrite as a literal."
+    ]
+
+
 # C409