Repository: asottile/pyupgrade

Issue:
--py3-plus: `str('native literal')` -> 'native literal'
In 2+3 code, a native literal (py2: bytes, py3: str) is often written as `str('foo')` -- this can be cleaned up automatically

Discussion:

Patch:
diff --git a/pyupgrade.py b/pyupgrade.py
index f1d25ec..2b251f4 100644
--- a/pyupgrade.py
+++ b/pyupgrade.py
@@ -904,7 +904,7 @@ SIX_TYPE_CTX_ATTRS = {
 }
 SIX_CALLS = {
     'u': '{args[0]}',
-    'byte2int': '{arg0}[0]',
+    'byte2int': '{args[0]}[0]',
     'indexbytes': '{args[0]}[{rest}]',
     'iteritems': '{args[0]}.items()',
     'iterkeys': '{args[0]}.keys()',
@@ -943,6 +943,8 @@ class FindPy3Plus(ast.NodeVisitor):
     def __init__(self):
         self.bases_to_remove = set()
 
+        self.native_literals = set()
+
         self._six_from_imports = set()
         self.six_b = set()
         self.six_calls = {}
@@ -1055,6 +1057,15 @@ class FindPy3Plus(ast.NodeVisitor):
                 node.args[1].id == self._class_info_stack[-1].first_arg_name
         ):
             self.super_calls[_ast_to_offset(node)] = node
+        elif (
+                isinstance(node.func, ast.Name) and
+                node.func.id == 'str' and
+                len(node.args) == 1 and
+                isinstance(node.args[0], ast.Str) and
+                not node.keywords and
+                not _starargs(node)
+        ):
+            self.native_literals.add(_ast_to_offset(node))
 
         self.generic_visit(node)
 
@@ -1172,6 +1183,7 @@ def _fix_py3_plus(contents_text):
 
     if not any((
             visitor.bases_to_remove,
+            visitor.native_literals,
             visitor.six_b,
             visitor.six_calls,
             visitor.six_raises,
@@ -1243,6 +1255,12 @@ def _fix_py3_plus(contents_text):
             call = visitor.super_calls[token.offset]
             victims = _victims(tokens, i, call, gen=False)
             del tokens[victims.starts[0] + 1:victims.ends[-1]]
+        elif token.offset in visitor.native_literals:
+            j = _find_open_paren(tokens, i)
+            func_args, end = _parse_call_args(tokens, j)
+            if any(tok.name == 'NL' for tok in tokens[i:end]):
+                continue
+            _replace_call(tokens, i, end, func_args, '{args[0]}')
 
     return tokens_to_src(tokens)
 


Test patch:
diff --git a/tests/pyupgrade_test.py b/tests/pyupgrade_test.py
index a3f75cb..8de1903 100644
--- a/tests/pyupgrade_test.py
+++ b/tests/pyupgrade_test.py
@@ -1158,6 +1158,10 @@ def test_fix_six_noop(s):
             'from six import b\n\n' r'b("\x12\xef")',
             'from six import b\n\n' r'b"\x12\xef"',
         ),
+        (
+            'six.byte2int(b"f")',
+            'b"f"[0]',
+        ),
         (
             '@six.python_2_unicode_compatible\n'
             'class C: pass',
@@ -1353,6 +1357,31 @@ def test_fix_classes_py3only(s, expected):
     assert _fix_py3_plus(s) == expected
 
 
+@pytest.mark.parametrize(
+    's',
+    (
+        'str(1)',
+        'str("foo"\n"bar")',  # creates a syntax error
+        'str(*a)', 'str("foo", *a)',
+        'str(**k)', 'str("foo", **k)',
+        'str("foo", encoding="UTF-8")',
+    ),
+)
+def test_fix_native_literals_noop(s):
+    assert _fix_py3_plus(s) == s
+
+
+@pytest.mark.parametrize(
+    ('s', 'expected'),
+    (
+        ('str("foo")', '"foo"'),
+        ('str("""\nfoo""")', '"""\nfoo"""'),
+    ),
+)
+def test_fix_native_literals(s, expected):
+    assert _fix_py3_plus(s) == expected
+
+
 @pytest.mark.parametrize(
     's',
     (