Repository: asottile/pyupgrade

Issue:
replace `typing.Text` with `str` for `--py3-plus`
Perhaps another example of the same idea, replace `typing.Text` with `str` for `--py3-plus`.

Example input:

```py
if False:
    from typing import Text


def f():
    # type: () -> Text
    return "hello"
```

Example diff:

```diff
--- before.py
+++ after.py
@@ -3,5 +3,5 @@
 
 
 def f():
-    # type: () -> Text
+    # type: () -> str
     return "hello"

```

_Originally posted by @jdufresne in https://github.com/asottile/pyupgrade/issues/343#issuecomment-750930060_

Discussion:

Patch:
diff --git a/README.md b/README.md
index 2d2de40..aae7308 100644
--- a/README.md
+++ b/README.md
@@ -410,6 +410,17 @@ except OSError:
     raise
 ```
 
+### `typing.Text` str alias
+
+Availability:
+- `--py3-plus` is passed on the commandline.
+
+```diff
+-def f(x: Text) -> None:
++def f(x: str) -> None:
+     ...
+```
+
 ### `typing.NamedTuple` / `typing.TypedDict` py36+ syntax
 
 Availability:
diff --git a/pyupgrade/_plugins/six_b.py b/pyupgrade/_plugins/six_b.py
new file mode 100644
index 0000000..f50cdd4
--- /dev/null
+++ b/pyupgrade/_plugins/six_b.py
@@ -0,0 +1,51 @@
+import ast
+from typing import Iterable
+from typing import List
+from typing import Tuple
+
+from tokenize_rt import Offset
+from tokenize_rt import Token
+
+from pyupgrade._ast_helpers import ast_to_offset
+from pyupgrade._ast_helpers import has_starargs
+from pyupgrade._ast_helpers import is_name_attr
+from pyupgrade._data import register
+from pyupgrade._data import State
+from pyupgrade._data import TokenFunc
+from pyupgrade._string_helpers import is_ascii
+from pyupgrade._token_helpers import find_open_paren
+from pyupgrade._token_helpers import parse_call_args
+from pyupgrade._token_helpers import replace_call
+
+
+def _fix_six_b(i: int, tokens: List[Token]) -> None:
+    j = find_open_paren(tokens, i)
+    if (
+            tokens[j + 1].name == 'STRING' and
+            is_ascii(tokens[j + 1].src) and
+            tokens[j + 2].src == ')'
+    ):
+        func_args, end = parse_call_args(tokens, j)
+        replace_call(tokens, i, end, func_args, 'b{args[0]}')
+
+
+@register(ast.Call)
+def visit_Call(
+        state: State,
+        node: ast.Call,
+        parent: ast.AST,
+) -> Iterable[Tuple[Offset, TokenFunc]]:
+    if (
+            state.settings.min_version >= (3,) and
+            is_name_attr(
+                node.func,
+                state.from_imports,
+                'six',
+                ('b', 'ensure_binary'),
+            ) and
+            not node.keywords and
+            not has_starargs(node) and
+            len(node.args) == 1 and
+            isinstance(node.args[0], ast.Str)
+    ):
+        yield ast_to_offset(node), _fix_six_b
diff --git a/pyupgrade/_plugins/six_calls.py b/pyupgrade/_plugins/six_calls.py
index 3fd30cd..b81c542 100644
--- a/pyupgrade/_plugins/six_calls.py
+++ b/pyupgrade/_plugins/six_calls.py
@@ -2,12 +2,10 @@ import ast
 import functools
 import sys
 from typing import Iterable
-from typing import List
 from typing import Tuple
 from typing import Type
 
 from tokenize_rt import Offset
-from tokenize_rt import Token
 
 from pyupgrade._ast_helpers import ast_to_offset
 from pyupgrade._ast_helpers import has_starargs
@@ -15,11 +13,7 @@ from pyupgrade._ast_helpers import is_name_attr
 from pyupgrade._data import register
 from pyupgrade._data import State
 from pyupgrade._data import TokenFunc
-from pyupgrade._string_helpers import is_ascii
 from pyupgrade._token_helpers import find_and_replace_call
-from pyupgrade._token_helpers import find_open_paren
-from pyupgrade._token_helpers import parse_call_args
-from pyupgrade._token_helpers import replace_call
 
 _EXPR_NEEDS_PARENS: Tuple[Type[ast.expr], ...] = (
     ast.Await, ast.BinOp, ast.BoolOp, ast.Compare, ast.GeneratorExp, ast.IfExp,
@@ -57,17 +51,6 @@ RERAISE_2_TMPL = 'raise {args[1]}.with_traceback(None)'
 RERAISE_3_TMPL = 'raise {args[1]}.with_traceback({args[2]})'
 
 
-def _fix_six_b(i: int, tokens: List[Token]) -> None:
-    j = find_open_paren(tokens, i)
-    if (
-            tokens[j + 1].name == 'STRING' and
-            is_ascii(tokens[j + 1].src) and
-            tokens[j + 2].src == ')'
-    ):
-        func_args, end = parse_call_args(tokens, j)
-        replace_call(tokens, i, end, func_args, 'b{args[0]}')
-
-
 @register(ast.Call)
 def visit_Call(
         state: State,
@@ -133,20 +116,6 @@ def visit_Call(
             template=SIX_INT2BYTE_TMPL,
         )
         yield ast_to_offset(node), func
-    elif (
-            state.settings.min_version >= (3,) and
-            is_name_attr(
-                node.func,
-                state.from_imports,
-                'six',
-                ('b', 'ensure_binary'),
-            ) and
-            not node.keywords and
-            not has_starargs(node) and
-            len(node.args) == 1 and
-            isinstance(node.args[0], ast.Str)
-    ):
-        yield ast_to_offset(node), _fix_six_b
     elif (
             isinstance(parent, ast.Expr) and
             is_name_attr(
diff --git a/pyupgrade/_plugins/typing_text.py b/pyupgrade/_plugins/typing_text.py
new file mode 100644
index 0000000..639868a
--- /dev/null
+++ b/pyupgrade/_plugins/typing_text.py
@@ -0,0 +1,51 @@
+import ast
+import functools
+from typing import Iterable
+from typing import Tuple
+
+from tokenize_rt import Offset
+
+from pyupgrade._ast_helpers import ast_to_offset
+from pyupgrade._data import register
+from pyupgrade._data import State
+from pyupgrade._data import TokenFunc
+from pyupgrade._token_helpers import replace_name
+
+
+@register(ast.Attribute)
+def visit_Attribute(
+        state: State,
+        node: ast.Attribute,
+        parent: ast.AST,
+) -> Iterable[Tuple[Offset, TokenFunc]]:
+    if (
+            state.settings.min_version >= (3,) and
+            isinstance(node.value, ast.Name) and
+            node.value.id == 'typing' and
+            node.attr == 'Text'
+    ):
+        func = functools.partial(
+            replace_name,
+            name=node.attr,
+            new='str',
+        )
+        yield ast_to_offset(node), func
+
+
+@register(ast.Name)
+def visit_Name(
+        state: State,
+        node: ast.Name,
+        parent: ast.AST,
+) -> Iterable[Tuple[Offset, TokenFunc]]:
+    if (
+            state.settings.min_version >= (3,) and
+            node.id in state.from_imports['typing'] and
+            node.id == 'Text'
+    ):
+        func = functools.partial(
+            replace_name,
+            name=node.id,
+            new='str',
+        )
+        yield ast_to_offset(node), func


Test patch:
diff --git a/tests/features/binary_literals_test.py b/tests/binary_literals_test.py
similarity index 100%
rename from tests/features/binary_literals_test.py
rename to tests/binary_literals_test.py
diff --git a/tests/features/default_encoding_test.py b/tests/default_encoding_test.py
similarity index 100%
rename from tests/features/default_encoding_test.py
rename to tests/default_encoding_test.py
diff --git a/tests/features/dict_literals_test.py b/tests/dict_literals_test.py
similarity index 100%
rename from tests/features/dict_literals_test.py
rename to tests/dict_literals_test.py
diff --git a/tests/features/encoding_cookie_test.py b/tests/encoding_cookie_test.py
similarity index 100%
rename from tests/features/encoding_cookie_test.py
rename to tests/encoding_cookie_test.py
diff --git a/tests/features/escape_sequences_test.py b/tests/escape_sequences_test.py
similarity index 100%
rename from tests/features/escape_sequences_test.py
rename to tests/escape_sequences_test.py
diff --git a/tests/features/extra_parens_test.py b/tests/extra_parens_test.py
similarity index 100%
rename from tests/features/extra_parens_test.py
rename to tests/extra_parens_test.py
diff --git a/tests/features/typing_text_test.py b/tests/features/typing_text_test.py
new file mode 100644
index 0000000..be9745a
--- /dev/null
+++ b/tests/features/typing_text_test.py
@@ -0,0 +1,60 @@
+import pytest
+
+from pyupgrade._data import Settings
+from pyupgrade._main import _fix_plugins
+
+
+@pytest.mark.parametrize(
+    ('s', 'version'),
+    (
+        pytest.param(
+            'from typing import Text\n'
+            'x: Text\n',
+            (2, 7),
+            id='not python 3+',
+        ),
+        pytest.param(
+            'class Text: ...\n'
+            'text = Text()\n',
+            (3,),
+            id='not a type annotation',
+        ),
+    ),
+)
+def test_fix_typing_text_noop(s, version):
+    assert _fix_plugins(s, settings=Settings(min_version=version)) == s
+
+
+@pytest.mark.parametrize(
+    ('s', 'expected'),
+    (
+        pytest.param(
+            'from typing import Text\n'
+            'x: Text\n',
+
+            'from typing import Text\n'
+            'x: str\n',
+
+            id='from import of Text',
+        ),
+        pytest.param(
+            'import typing\n'
+            'x: typing.Text\n',
+
+            'import typing\n'
+            'x: str\n',
+
+            id='import of typing + typing.Text',
+        ),
+        pytest.param(
+            'from typing import Text\n'
+            'SomeAlias = Text\n',
+            'from typing import Text\n'
+            'SomeAlias = str\n',
+            id='not in a type annotation context',
+        ),
+    ),
+)
+def test_fix_typing_text(s, expected):
+    ret = _fix_plugins(s, settings=Settings(min_version=(3,)))
+    assert ret == expected