From 9e22c0cf691f613712d0c15861e6aba41db14912 Mon Sep 17 00:00:00 2001 From: Lincoln Quirk Date: Sun, 21 May 2017 15:30:32 +0300 Subject: [PATCH 1/5] Parse each format-string component separately 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). --- mypy/fastparse.py | 35 ++++++++++++++++-------- test-data/unit/check-newsyntax.test | 17 +++++++++--- test-data/unit/fixtures/f_string.pyi | 41 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 test-data/unit/fixtures/f_string.pyi diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 1d6dec891dd9..e923813ff350 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -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 diff --git a/test-data/unit/check-newsyntax.test b/test-data/unit/check-newsyntax.test index 9f3f87853e63..74e52cbea4af 100644 --- a/test-data/unit/check-newsyntax.test +++ b/test-data/unit/check-newsyntax.test @@ -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") @@ -142,4 +142,13 @@ 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 +f'{v}' + '' +f'{1}' + '' +f' {v}' + '' +[builtins fixtures/f_string.pyi] + diff --git a/test-data/unit/fixtures/f_string.pyi b/test-data/unit/fixtures/f_string.pyi new file mode 100644 index 000000000000..aa496cdc8ec7 --- /dev/null +++ b/test-data/unit/fixtures/f_string.pyi @@ -0,0 +1,41 @@ +# 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, builtinclass, Iterable, Iterator, List, overload + +T = TypeVar('T') + +@builtinclass +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 __iter__(self) -> Iterator[T]: pass + def __add__(self, x: list[T]) -> list[T]: pass + def __mul__(self, x: int) -> list[T]: pass + def __getitem__(self, x: int) -> T: pass + def append(self, x: T) -> None: pass + def extend(self, x: Iterable[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 + From dada4e4f87ab397b8fbfe0247c9175e5a401fed1 Mon Sep 17 00:00:00 2001 From: Lincoln Quirk Date: Mon, 22 May 2017 08:12:38 +0300 Subject: [PATCH 2/5] test f-string type using reveal_type --- test-data/unit/check-newsyntax.test | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-newsyntax.test b/test-data/unit/check-newsyntax.test index 74e52cbea4af..645fbe525358 100644 --- a/test-data/unit/check-newsyntax.test +++ b/test-data/unit/check-newsyntax.test @@ -147,8 +147,7 @@ f'result: {value:{width}.{precision}}' [case testNewSyntaxFStringSingleField] # flags: --python-version 3.6 v = 1 -f'{v}' + '' -f'{1}' + '' -f' {v}' + '' +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] From 06321c1e39dc004b14944d8f5ff3f0e618a6428d Mon Sep 17 00:00:00 2001 From: Lincoln Quirk Date: Mon, 22 May 2017 08:16:51 +0300 Subject: [PATCH 3/5] remove unnecessary @builtinclass decorator --- test-data/unit/fixtures/f_string.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/fixtures/f_string.pyi b/test-data/unit/fixtures/f_string.pyi index aa496cdc8ec7..81acc12fa92c 100644 --- a/test-data/unit/fixtures/f_string.pyi +++ b/test-data/unit/fixtures/f_string.pyi @@ -1,11 +1,10 @@ # 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, builtinclass, Iterable, Iterator, List, overload +from typing import TypeVar, Generic, Iterable, Iterator, List, overload T = TypeVar('T') -@builtinclass class object: def __init__(self): pass @@ -31,6 +30,7 @@ class tuple(Generic[T]): pass class function: pass class int: def __add__(self, i: int) -> int: pass + class float: pass class bool(int): pass From 4822afd648518ce004247cbd20b7d75ba090108f Mon Sep 17 00:00:00 2001 From: Lincoln Quirk Date: Sat, 27 May 2017 11:50:37 +0300 Subject: [PATCH 4/5] change docstrings to comments --- mypy/fastparse.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/fastparse.py b/mypy/fastparse.py index e923813ff350..10ad642dcdf0 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -858,8 +858,8 @@ def visit_Str(self, n: ast3.Str) -> Union[UnicodeExpr, StrExpr]: # JoinedStr(expr* values) @with_line def visit_JoinedStr(self, n: ast3.JoinedStr) -> Expression: - """Each of n.values is a str or FormattedValue; we just concatenate - them all using ''.join.""" + # 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)) @@ -874,9 +874,9 @@ def visit_JoinedStr(self, n: ast3.JoinedStr) -> Expression: # FormattedValue(expr value) @with_line def visit_FormattedValue(self, n: ast3.FormattedValue) -> Expression: - """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.""" + # 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('{}') From f586e2d72afb1a862c1f64d361eeeab3bbb60d7f Mon Sep 17 00:00:00 2001 From: Lincoln Quirk Date: Sat, 27 May 2017 11:46:09 +0300 Subject: [PATCH 5/5] remove unneeded functions from list class in fixture --- test-data/unit/fixtures/f_string.pyi | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test-data/unit/fixtures/f_string.pyi b/test-data/unit/fixtures/f_string.pyi index 81acc12fa92c..78d39aee85b8 100644 --- a/test-data/unit/fixtures/f_string.pyi +++ b/test-data/unit/fixtures/f_string.pyi @@ -18,12 +18,7 @@ class list(Iterable[T], Generic[T]): def __init__(self) -> None: pass @overload def __init__(self, x: Iterable[T]) -> None: pass - def __iter__(self) -> Iterator[T]: pass - def __add__(self, x: list[T]) -> list[T]: pass - def __mul__(self, x: int) -> list[T]: pass - def __getitem__(self, x: int) -> T: pass def append(self, x: T) -> None: pass - def extend(self, x: Iterable[T]) -> None: pass class tuple(Generic[T]): pass