Repository: asottile/pyupgrade

Issue:
NotImplementedError on optional TypedDict union in functional definition
```python
Traceback (most recent call last):
  File "/Users/v832272/.asdf/installs/python/3.11.0/bin/pyupgrade", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_main.py", line 381, in main
    ret |= _fix_file(filename, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_main.py", line 318, in _fix_file
    contents_text = _fix_plugins(
                    ^^^^^^^^^^^^^
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_main.py", line 80, in _fix_plugins
    callback(i, tokens)
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_plugins/typing_classes.py", line 141, in _fix_dict_typed_dict
    end, attrs = _typed_class_replacement(tokens, i, call, types)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_plugins/typing_classes.py", line 82, in _typed_class_replacement
    member = f'{indent}{k}: {_unparse(v)}'
                             ^^^^^^^^^^^
  File "/Users/v832272/.asdf/installs/python/3.11.0/lib/python3.11/site-packages/pyupgrade/_plugins/typing_classes.py", line 50, in _unparse
    raise NotImplementedError(ast.dump(node))
NotImplementedError: BinOp(left=Name(id='ExampleChangeRequestResponseOutageDict', ctx=Load()), op=BitOr(), right=Constant(value=None))
```

This is triggered by:

```python
ExampleChangeRequestResponseOutageDict = TypedDict(
    "ExampleChangeRequestResponseOutageDict",
    {"outage-seq-id": int, "outage-start-time": str, "outage-end-time": str},
)

ExampleChangeRequestResponseOutageListDict = TypedDict(
    "ExampleChangeRequestResponseOutageListDict",
    {"outages": ExampleChangeRequestResponseOutageDict | None},
)
```

Command used:

```shell
find src -name '*.py' -type f | xargs -n 1 -I {} pyupgrade --py311-plus {}
```

Discussion:

Patch:
diff --git a/pyupgrade/_plugins/typing_classes.py b/pyupgrade/_plugins/typing_classes.py
index d0afe13..387f18c 100644
--- a/pyupgrade/_plugins/typing_classes.py
+++ b/pyupgrade/_plugins/typing_classes.py
@@ -46,6 +46,8 @@ def _unparse(node: ast.expr) -> str:
         return '[{}]'.format(', '.join(_unparse(elt) for elt in node.elts))
     elif isinstance(node, ast.NameConstant):
         return repr(node.value)
+    elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
+        return f'{_unparse(node.left)} | {_unparse(node.right)}'
     else:
         raise NotImplementedError(ast.dump(node))
 


Test patch:
diff --git a/tests/features/typing_classes_test.py b/tests/features/typing_classes_test.py
index 3ca324b..3142185 100644
--- a/tests/features/typing_classes_test.py
+++ b/tests/features/typing_classes_test.py
@@ -210,6 +210,16 @@ def test_typing_named_tuple_noop(s):
 
             id='preserves comments without alignment',
         ),
+        pytest.param(
+            'from typing import NamedTuple\n'
+            'Foo = NamedTuple("Foo", [("union", str | None)])',
+
+            'from typing import NamedTuple\n'
+            'class Foo(NamedTuple):\n'
+            '    union: str | None',
+
+            id='BitOr unparse error',
+        ),
     ),
 )
 def test_fix_typing_named_tuple(s, expected):
@@ -393,6 +403,16 @@ def test_typing_typed_dict_noop(s):
 
             id='right after a dedent',
         ),
+        pytest.param(
+            'from typing import TypedDict\n'
+            'Foo = TypedDict("Foo", {"union": str | int | None})',
+
+            'from typing import TypedDict\n'
+            'class Foo(TypedDict):\n'
+            '    union: str | int | None',
+
+            id='BitOr unparse error',
+        ),
     ),
 )
 def test_typing_typed_dict(s, expected):