Repository: asottile/pyupgrade

Issue:
rewrite bytes(b'foo') -> b'foo'
looks like this was a somewhat common mistake when doing a python2/python3 port

similar to the `str('string literal')` fix that's already happening in pyupgrade

Discussion:
theendlessriver13: for the coverage issue:
I could either make it just `else` instead of the `elif` checking for `bytes` or add another `else` at the end with raising `NotImplementedError`.

I think the former with just else is fine, since it __should__ only ever be `str` or `bytes`.

Patch:
diff --git a/pyupgrade/_plugins/native_literals.py b/pyupgrade/_plugins/native_literals.py
index fce35cb..e95dfe7 100644
--- a/pyupgrade/_plugins/native_literals.py
+++ b/pyupgrade/_plugins/native_literals.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import ast
+import functools
 from typing import Iterable
 
 from tokenize_rt import Offset
@@ -19,7 +20,7 @@ from pyupgrade._token_helpers import replace_call
 SIX_NATIVE_STR = frozenset(('ensure_str', 'ensure_text', 'text_type'))
 
 
-def _fix_native_str(i: int, tokens: list[Token]) -> None:
+def _fix_literal(i: int, tokens: list[Token], *, empty: str) -> None:
     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]):
@@ -27,7 +28,7 @@ def _fix_native_str(i: int, tokens: list[Token]) -> None:
     if func_args:
         replace_call(tokens, i, end, func_args, '{args[0]}')
     else:
-        tokens[i:end] = [tokens[i]._replace(name='STRING', src="''")]
+        tokens[i:end] = [tokens[i]._replace(name='STRING', src=empty)]
 
 
 def is_a_native_literal_call(
@@ -58,4 +59,16 @@ def visit_Call(
             state.settings.min_version >= (3,) and
             is_a_native_literal_call(node, state.from_imports)
     ):
-        yield ast_to_offset(node), _fix_native_str
+        func = functools.partial(_fix_literal, empty="''")
+        yield ast_to_offset(node), func
+    elif (
+            state.settings.min_version >= (3,) and
+            isinstance(node.func, ast.Name) and node.func.id == 'bytes' and
+            not node.keywords and not has_starargs(node) and
+            (
+                len(node.args) == 0 or
+                (len(node.args) == 1 and isinstance(node.args[0], ast.Bytes))
+            )
+    ):
+        func = functools.partial(_fix_literal, empty="b''")
+        yield ast_to_offset(node), func
diff --git a/pyupgrade/_token_helpers.py b/pyupgrade/_token_helpers.py
index ec0a8dd..36e7f79 100644
--- a/pyupgrade/_token_helpers.py
+++ b/pyupgrade/_token_helpers.py
@@ -420,7 +420,7 @@ def replace_call(
 
     # there are a few edge cases which cause syntax errors when the first
     # argument contains newlines (especially when moved outside of a natural
-    # contiunuation context)
+    # continuation context)
     if _arg_contains_newline(tokens, *args[0]) and 0 not in parens:
         # this attempts to preserve more of the whitespace by using the
         # original non-stripped argument string


Test patch:
diff --git a/tests/features/native_literals_test.py b/tests/features/native_literals_test.py
index f6b3bfb..944d8ea 100644
--- a/tests/features/native_literals_test.py
+++ b/tests/features/native_literals_test.py
@@ -14,6 +14,11 @@ from pyupgrade._main import _fix_plugins
         'str(*a)', 'str("foo", *a)',
         'str(**k)', 'str("foo", **k)',
         'str("foo", encoding="UTF-8")',
+        'bytes("foo", encoding="UTF-8")',
+        'bytes(b"foo"\nb"bar")',
+        'bytes("foo"\n"bar")',
+        'bytes(*a)', 'bytes("foo", *a)',
+        'bytes("foo", **a)',
     ),
 )
 def test_fix_native_literals_noop(s):
@@ -38,6 +43,9 @@ def test_fix_native_literals_noop(s):
 
             id='from import of rewritten name',
         ),
+        ('bytes()', "b''"),
+        ('bytes(b"foo")', 'b"foo"'),
+        ('bytes(b"""\nfoo""")', 'b"""\nfoo"""'),
     ),
 )
 def test_fix_native_literals(s, expected):