Repository: asottile/pyupgrade

Issue:
Rewriting TypedDict can lead to SyntaxError
pyupgrade checks if the keys of a TypedDict are strings, but not if they are valid for usage in class syntax.

Here's a minimal input:

```python
from typing_extensions import TypedDict

MyDict = TypedDict("MyDict", {"my-key": str, "another key": int})
```


This is the rewrite produced by `pyupgrade --py36-plus minimal.py`:

```python
from typing_extensions import TypedDict

class MyDict(TypedDict):
    my-key: str
    another key: int
```

The `-` and the space in the keys are invalid syntax.

Discussion:

Patch:
diff --git a/pyupgrade.py b/pyupgrade.py
index f6e8e94..d740aa0 100644
--- a/pyupgrade.py
+++ b/pyupgrade.py
@@ -2449,6 +2449,7 @@ class FindPy36Plus(ast.NodeVisitor):
                         isinstance(tup, ast.Tuple) and
                         len(tup.elts) == 2 and
                         isinstance(tup.elts[0], ast.Str) and
+                        tup.elts[0].s.isidentifier() and
                         tup.elts[0].s not in _KEYWORDS
                         for tup in node.value.args[1].elts
                     )
@@ -2475,7 +2476,9 @@ class FindPy36Plus(ast.NodeVisitor):
                     isinstance(node.value.args[1], ast.Dict) and
                     node.value.args[1].keys and
                     all(
-                        isinstance(k, ast.Str)
+                        isinstance(k, ast.Str) and
+                        k.s.isidentifier() and
+                        k.s not in _KEYWORDS
                         for k in node.value.args[1].keys
                     )
             ):


Test patch:
diff --git a/tests/typing_named_tuple_test.py b/tests/typing_named_tuple_test.py
index 3e38502..083d672 100644
--- a/tests/typing_named_tuple_test.py
+++ b/tests/typing_named_tuple_test.py
@@ -41,6 +41,10 @@ from pyupgrade import _fix_py36_plus
             'C = typing.NamedTuple("C", [("def", int)])',
             id='uses keyword',
         ),
+        pytest.param(
+            'C = typing.NamedTuple("C", [("not-ok", int)])',
+            id='uses non-identifier',
+        ),
         pytest.param(
             'C = typing.NamedTuple("C", *types)',
             id='NamedTuple starargs',
diff --git a/tests/typing_typed_dict_test.py b/tests/typing_typed_dict_test.py
index fb39c6b..eea6ef4 100644
--- a/tests/typing_typed_dict_test.py
+++ b/tests/typing_typed_dict_test.py
@@ -19,6 +19,14 @@ from pyupgrade import _fix_py36_plus
             'D = typing.TypedDict("D", {1: str})',
             id='key is not a string',
         ),
+        pytest.param(
+            'D = typing.TypedDict("D", {"a-b": str})',
+            id='key is not an identifier',
+        ),
+        pytest.param(
+            'D = typing.TypedDict("D", {"class": str})',
+            id='key is a keyword',
+        ),
         pytest.param(
             'D = typing.TypedDict("D", {**d, "a": str})',
             id='dictionary splat operator',