Repository: asottile/pyupgrade

Issue:
Do not upgrade string annotations types wrapped in `Optional` to new Union | formatting
Simplistic python code:

```python
qqq: "int" | None = 0
```

Output:

```
python qqq.py
Traceback (most recent call last):
  File "qqq.py", line 2, in <module>
    qqq: "int" | None = 0
TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'
```

When running pyupgrade a segment of code was updated to use the `|` notation and caused this explosion. I worked around it, but I am sure others might run into similar issues. 


Discussion:

Patch:
diff --git a/pyupgrade/_plugins/typing_pep604.py b/pyupgrade/_plugins/typing_pep604.py
index 6ffcc20..8d55009 100644
--- a/pyupgrade/_plugins/typing_pep604.py
+++ b/pyupgrade/_plugins/typing_pep604.py
@@ -126,6 +126,15 @@ def _supported_version(state: State) -> bool:
     )
 
 
+def _any_arg_is_str(node_slice: ast.expr) -> bool:
+    return (
+        isinstance(node_slice, ast.Str) or (
+            isinstance(node_slice, ast.Tuple) and
+            any(isinstance(elt, ast.Str) for elt in node_slice.elts)
+        )
+    )
+
+
 @register(ast.Subscript)
 def visit_Subscript(
         state: State,
@@ -135,6 +144,17 @@ def visit_Subscript(
     if not _supported_version(state):
         return
 
+    # prevent rewriting forward annotations
+    if (
+            (sys.version_info >= (3, 9) and _any_arg_is_str(node.slice)) or
+            (
+                sys.version_info < (3, 9) and
+                isinstance(node.slice, ast.Index) and
+                _any_arg_is_str(node.slice.value)
+            )
+    ):
+        return
+
     if is_name_attr(node.value, state.from_imports, 'typing', ('Optional',)):
         yield ast_to_offset(node), _fix_optional
     elif is_name_attr(node.value, state.from_imports, 'typing', ('Union',)):


Test patch:
diff --git a/tests/features/typing_pep604_test.py b/tests/features/typing_pep604_test.py
index 503bc2a..7b734e9 100644
--- a/tests/features/typing_pep604_test.py
+++ b/tests/features/typing_pep604_test.py
@@ -41,6 +41,19 @@ from pyupgrade._main import _fix_plugins
             (3, 10),
             id='3.10+ empty Union',
         ),
+        # https://github.com/asottile/pyupgrade/issues/567
+        pytest.param(
+            'from typing import Optional\n'
+            'def f() -> Optional["str"]: ...\n',
+            (3, 10),
+            id='3.10+ Optional of forward reference',
+        ),
+        pytest.param(
+            'from typing import Union\n'
+            'def f() -> Union[int, "str"]: ...\n',
+            (3, 10),
+            id='3.10+ Union of forward reference',
+        ),
     ),
 )
 def test_fix_pep604_types_noop(s, version):