Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rewrite string formatting with **locals() #585

Merged
merged 1 commit into from
Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ Availability:
+f'{foo.bar} {baz.womp}'
-'{} {}'.format(f(), g())
+f'{f()} {g()}'
-'{x}'.format(**locals())
+f'{x}'
```

_note_: `pyupgrade` is intentionally timid and will not create an f-string
Expand Down
49 changes: 49 additions & 0 deletions pyupgrade/_plugins/format_locals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import ast
from typing import Iterable
from typing import List
from typing import Tuple

from tokenize_rt import Offset
from tokenize_rt import rfind_string_parts
from tokenize_rt import Token

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 find_closing_bracket
from pyupgrade._token_helpers import find_open_paren
from pyupgrade._token_helpers import find_token


def _fix(i: int, tokens: List[Token]) -> None:
dot_pos = find_token(tokens, i, '.')
open_pos = find_open_paren(tokens, dot_pos)
close_pos = find_closing_bracket(tokens, open_pos)
for string_idx in rfind_string_parts(tokens, dot_pos - 1):
tok = tokens[string_idx]
tokens[string_idx] = tok._replace(src=f'f{tok.src}')
del tokens[dot_pos:close_pos + 1]


@register(ast.Call)
def visit_Call(
state: State,
node: ast.Call,
parent: ast.AST,
) -> Iterable[Tuple[Offset, TokenFunc]]:
if (
state.settings.min_version >= (3, 6) and
isinstance(node.func, ast.Attribute) and
isinstance(node.func.value, ast.Str) and
node.func.attr == 'format' and
len(node.args) == 0 and
len(node.keywords) == 1 and
node.keywords[0].arg is None and
isinstance(node.keywords[0].value, ast.Call) and
isinstance(node.keywords[0].value.func, ast.Name) and
node.keywords[0].value.func.id == 'locals' and
len(node.keywords[0].value.args) == 0 and
len(node.keywords[0].value.keywords) == 0
):
yield ast_to_offset(node), _fix
53 changes: 53 additions & 0 deletions tests/features/format_locals_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pytest

from pyupgrade._data import Settings
from pyupgrade._main import _fix_plugins


@pytest.mark.parametrize(
('s', 'version'),
(
pytest.param(
'"{x}".format(**locals())',
(3,),
id='not 3.6+',
),
pytest.param(
'"{x} {y}".format(x, **locals())',
(3, 6),
id='mixed locals() and params',
),
),
)
def test_fix_format_locals_noop(s, version):
assert _fix_plugins(s, settings=Settings(min_version=version)) == s


@pytest.mark.parametrize(
('s', 'expected'),
(
pytest.param(
'"{x}".format(**locals())',
'f"{x}"',
id='normal case',
),
pytest.param(
'"{x}" "{y}".format(**locals())',
'f"{x}" f"{y}"',
id='joined strings',
),
pytest.param(
'(\n'
' "{x}"\n'
' "{y}"\n'
').format(**locals())\n',
'(\n'
' f"{x}"\n'
' f"{y}"\n'
')\n',
id='joined strings with parens',
),
),
)
def test_fix_format_locals(s, expected):
assert _fix_plugins(s, settings=Settings(min_version=(3, 6))) == expected
2 changes: 0 additions & 2 deletions tests/features/fstrings_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ def test_fix_fstrings_noop(s):
r'f"\N{snowman} {a}"',
id='named escape sequences',
),
# TODO: poor man's f-strings?
# '"{foo}".format(**locals())'
),
)
def test_fix_fstrings(s, expected):
Expand Down