Skip to content

Commit

Permalink
Merge pull request #585 from asottile/format-locals
Browse files Browse the repository at this point in the history
rewrite string formatting with **locals()
  • Loading branch information
asottile authored Jan 1, 2022
2 parents 88fe1b5 + 416deab commit 718e7c0
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 2 deletions.
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

0 comments on commit 718e7c0

Please sign in to comment.