Skip to content

Commit

Permalink
Parse each format-string component separately (#3390)
Browse files Browse the repository at this point in the history
Fixes #3385.

The old code evaluated each FormattedValue as an expression potentially
returning the type of the value, rather than a string. But that's wrong,
because a FormattedValue can exist on its own when it's not being joined
to any other format strings. The new code evaluates each FormattedValue
by synthesizing '{}'.format(expr) and then, if necessary, joins them in
JoinedStr using ''.join(items).
  • Loading branch information
lincolnq authored and gvanrossum committed May 30, 2017
1 parent 35fbfb6 commit d4864a9
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 15 deletions.
35 changes: 24 additions & 11 deletions mypy/fastparse.py
Expand Up @@ -858,22 +858,35 @@ def visit_Str(self, n: ast3.Str) -> Union[UnicodeExpr, StrExpr]:
# JoinedStr(expr* values)
@with_line
def visit_JoinedStr(self, n: ast3.JoinedStr) -> Expression:
arg_count = len(n.values)
format_string = StrExpr('{}' * arg_count)
format_string.set_line(n.lineno, n.col_offset)
format_method = MemberExpr(format_string, 'format')
format_method.set_line(format_string)
format_args = self.translate_expr_list(n.values)
format_arg_kinds = [ARG_POS] * arg_count
result_expression = CallExpr(format_method,
format_args,
format_arg_kinds)
# Each of n.values is a str or FormattedValue; we just concatenate
# them all using ''.join.
empty_string = StrExpr('')
empty_string.set_line(n.lineno, n.col_offset)
strs_to_join = ListExpr(self.translate_expr_list(n.values))
strs_to_join.set_line(empty_string)
join_method = MemberExpr(empty_string, 'join')
join_method.set_line(empty_string)
result_expression = CallExpr(join_method,
[strs_to_join],
[ARG_POS])
return result_expression

# FormattedValue(expr value)
@with_line
def visit_FormattedValue(self, n: ast3.FormattedValue) -> Expression:
return self.visit(n.value)
# A FormattedValue is a component of a JoinedStr, or it can exist
# on its own. We translate them to individual '{}'.format(value)
# calls -- we don't bother with the conversion/format_spec fields.
exp = self.visit(n.value)
exp.set_line(n.lineno, n.col_offset)
format_string = StrExpr('{}')
format_string.set_line(n.lineno, n.col_offset)
format_method = MemberExpr(format_string, 'format')
format_method.set_line(format_string)
result_expression = CallExpr(format_method,
[exp],
[ARG_POS])
return result_expression

# Bytes(bytes s)
@with_line
Expand Down
16 changes: 12 additions & 4 deletions test-data/unit/check-newsyntax.test
Expand Up @@ -119,19 +119,19 @@ f'{type(1)}'
a: str
a = f'foobar'
a = f'{"foobar"}'
[builtins fixtures/primitives.pyi]
[builtins fixtures/f_string.pyi]

[case testNewSyntaxFStringExpressionsOk]
# flags: --python-version 3.6
f'.{1 + 1}.'
f'.{1 + 1}.{"foo" + "bar"}'
[builtins fixtures/primitives.pyi]
[builtins fixtures/f_string.pyi]

[case testNewSyntaxFStringExpressionsErrors]
# flags: --python-version 3.6
f'{1 + ""}'
f'.{1 + ""}'
[builtins fixtures/primitives.pyi]
[builtins fixtures/f_string.pyi]
[out]
main:2: error: Unsupported operand types for + ("int" and "str")
main:3: error: Unsupported operand types for + ("int" and "str")
Expand All @@ -142,4 +142,12 @@ value = 10.5142
width = 10
precision = 4
f'result: {value:{width}.{precision}}'
[builtins fixtures/primitives.pyi]
[builtins fixtures/f_string.pyi]

[case testNewSyntaxFStringSingleField]
# flags: --python-version 3.6
v = 1
reveal_type(f'{v}') # E: Revealed type is 'builtins.str'
reveal_type(f'{1}') # E: Revealed type is 'builtins.str'
[builtins fixtures/f_string.pyi]

36 changes: 36 additions & 0 deletions test-data/unit/fixtures/f_string.pyi
@@ -0,0 +1,36 @@
# Builtins stub used for format-string-related test cases.
# We need str and list, and str needs join and format methods.

from typing import TypeVar, Generic, Iterable, Iterator, List, overload

T = TypeVar('T')

class object:
def __init__(self): pass

class type:
def __init__(self, x) -> None: pass

class ellipsis: pass

class list(Iterable[T], Generic[T]):
@overload
def __init__(self) -> None: pass
@overload
def __init__(self, x: Iterable[T]) -> None: pass
def append(self, x: T) -> None: pass

class tuple(Generic[T]): pass

class function: pass
class int:
def __add__(self, i: int) -> int: pass

class float: pass
class bool(int): pass

class str:
def __add__(self, s: str) -> str: pass
def format(self, *args) -> str: pass
def join(self, l: List[str]) -> str: pass

0 comments on commit d4864a9

Please sign in to comment.