Skip to content
Open
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
19 changes: 19 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments
from mypy.literals import literal
from mypy.lookup import lookup_fully_qualified
from mypy.maptype import map_instance_to_supertype
from mypy.meet import is_overlapping_types, narrow_declared_type
from mypy.message_registry import ErrorMessage
Expand Down Expand Up @@ -92,6 +93,7 @@
SuperExpr,
SymbolNode,
SymbolTableNode,
TemplateStrExpr,
TempNode,
TupleExpr,
TypeAlias,
Expand Down Expand Up @@ -5487,6 +5489,23 @@ def find_typeddict_context(
# No TypedDict type in context.
return [], False

def visit_template_str_expr(self, e: TemplateStrExpr) -> Type:
"""Type check a template string expression (t-string).

Type-checks all interpolated expressions but the result is always
string.templatelib.Template.
"""
for item in e.items:
if isinstance(item, tuple):
value_expr, _source, _conversion, format_spec = item
self.accept(value_expr)
if format_spec is not None:
self.accept(format_spec)
sym = lookup_fully_qualified("string.templatelib.Template", self.chk.modules)
if sym is not None and isinstance(sym.node, TypeInfo):
return Instance(sym.node, [])
return AnyType(TypeOfAny.from_error)

def visit_lambda_expr(self, e: LambdaExpr) -> Type:
"""Type check lambda expression."""
old_in_lambda = self.in_lambda_expr
Expand Down
3 changes: 3 additions & 0 deletions mypy/evalexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ def visit__promote_expr(self, o: mypy.nodes.PromoteExpr) -> object:
def visit_await_expr(self, o: mypy.nodes.AwaitExpr) -> object:
return UNKNOWN

def visit_template_str_expr(self, o: mypy.nodes.TemplateStrExpr) -> object:
return UNKNOWN

def visit_temp_node(self, o: mypy.nodes.TempNode) -> object:
return UNKNOWN

Expand Down
27 changes: 15 additions & 12 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
Statement,
StrExpr,
SuperExpr,
TemplateStrExpr,
TempNode,
TryStmt,
TupleExpr,
Expand Down Expand Up @@ -1685,20 +1686,22 @@ def visit_FormattedValue(self, n: ast3.FormattedValue) -> Expression:
return self.set_line(result_expression, n)

# TemplateStr(expr* values)
def visit_TemplateStr(self, n: ast_TemplateStr) -> Expression:
self.fail(
ErrorMessage("PEP 750 template strings are not yet supported"),
n.lineno,
n.col_offset,
blocker=False,
)
e = TempNode(AnyType(TypeOfAny.from_error))
def visit_TemplateStr(self, n: ast_TemplateStr) -> TemplateStrExpr:
items: list[Expression | tuple[Expression, str, str | None, Expression | None]] = []
for value in n.values:
if isinstance(value, ast_Interpolation): # type: ignore[misc]
val_expr = self.visit(value.value)
val_expr.set_line(value.lineno, value.col_offset)
conversion = None if value.conversion < 0 else chr(value.conversion)
format_spec = (
self.visit(value.format_spec) if value.format_spec is not None else None
)
items.append((val_expr, value.str, conversion, format_spec))
else:
items.append(self.visit(value))
e = TemplateStrExpr(items)
return self.set_line(e, n)

# Interpolation(expr value, constant str, int conversion, expr? format_spec)
def visit_Interpolation(self, n: ast_Interpolation) -> Expression:
assert False, "Unreachable"

# Attribute(expr value, identifier attr, expr_context ctx)
def visit_Attribute(self, n: Attribute) -> MemberExpr | SuperExpr:
value = n.value
Expand Down
4 changes: 4 additions & 0 deletions mypy/literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
StarExpr,
StrExpr,
SuperExpr,
TemplateStrExpr,
TempNode,
TupleExpr,
TypeAliasExpr,
Expand Down Expand Up @@ -316,6 +317,9 @@ def visit__promote_expr(self, e: PromoteExpr) -> None:
def visit_await_expr(self, e: AwaitExpr) -> None:
return None

def visit_template_str_expr(self, e: TemplateStrExpr) -> None:
return None

def visit_temp_node(self, e: TempNode) -> None:
return None

Expand Down
23 changes: 23 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2765,6 +2765,29 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_dict_expr(self)


class TemplateStrExpr(Expression):
"""Template string expression t'...'."""

__slots__ = ("items",)
__match_args__ = ("items",)

# Each item is either:
# - a StrExpr (literal string segment), or
# - a tuple (value_expr, source_text, conversion, format_spec_expr)
# where conversion is str | None ("r", "s", "a", or None)
# and format_spec_expr is Expression | None
items: list[Expression | tuple[Expression, str, str | None, Expression | None]]

def __init__(
self, items: list[Expression | tuple[Expression, str, str | None, Expression | None]]
) -> None:
super().__init__()
self.items = items

def accept(self, visitor: ExpressionVisitor[T]) -> T:
return visitor.visit_template_str_expr(self)


class TupleExpr(Expression):
"""Tuple literal expression (..., ...)

Expand Down
10 changes: 10 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
SymbolNode,
SymbolTable,
SymbolTableNode,
TemplateStrExpr,
TempNode,
TryStmt,
TupleExpr,
Expand Down Expand Up @@ -5850,6 +5851,15 @@ def visit_dict_expr(self, expr: DictExpr) -> None:
key.accept(self)
value.accept(self)

def visit_template_str_expr(self, expr: TemplateStrExpr) -> None:
for item in expr.items:
if isinstance(item, tuple):
item[0].accept(self)
if item[3] is not None:
item[3].accept(self)
else:
item.accept(self)

def visit_star_expr(self, expr: StarExpr) -> None:
if not expr.valid:
self.fail("can't use starred expression here", expr, blocker=True)
Expand Down
5 changes: 5 additions & 0 deletions mypy/server/subexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
SetExpr,
SliceExpr,
StarExpr,
TemplateStrExpr,
TupleExpr,
TypeApplication,
TypeFormExpr,
Expand Down Expand Up @@ -155,6 +156,10 @@ def visit_dict_expr(self, e: DictExpr) -> None:
self.add(e)
super().visit_dict_expr(e)

def visit_template_str_expr(self, e: TemplateStrExpr) -> None:
self.add(e)
super().visit_template_str_expr(e)

def visit_set_expr(self, e: SetExpr) -> None:
self.add(e)
super().visit_set_expr(e)
Expand Down
9 changes: 9 additions & 0 deletions mypy/strconv.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,15 @@ def visit_list_expr(self, o: mypy.nodes.ListExpr) -> str:
def visit_dict_expr(self, o: mypy.nodes.DictExpr) -> str:
return self.dump([[k, v] for k, v in o.items], o)

def visit_template_str_expr(self, o: mypy.nodes.TemplateStrExpr) -> str:
items_repr: list[object] = []
for item in o.items:
if isinstance(item, tuple):
items_repr.append(item[0]) # value expression
else:
items_repr.append(item)
return self.dump(items_repr, o)

def visit_set_expr(self, o: mypy.nodes.SetExpr) -> str:
return self.dump(o.items, o)

Expand Down
4 changes: 4 additions & 0 deletions mypy/stubgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
StarExpr,
Statement,
StrExpr,
TemplateStrExpr,
TempNode,
TupleExpr,
TypeAliasStmt,
Expand Down Expand Up @@ -373,6 +374,9 @@ def visit_dict_expr(self, o: DictExpr) -> str:
dict_items.append(f"{key.accept(self)}: {value.accept(self)}")
return f"{{{', '.join(dict_items)}}}"

def visit_template_str_expr(self, o: TemplateStrExpr) -> str:
return self.stubgen.add_name("_typeshed.Incomplete")

def visit_ellipsis(self, node: EllipsisExpr) -> str:
return "..."

Expand Down
15 changes: 15 additions & 0 deletions mypy/traverser.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
StarExpr,
StrExpr,
SuperExpr,
TemplateStrExpr,
TempNode,
TryStmt,
TupleExpr,
Expand Down Expand Up @@ -329,6 +330,15 @@ def visit_dict_expr(self, o: DictExpr, /) -> None:
k.accept(self)
v.accept(self)

def visit_template_str_expr(self, o: TemplateStrExpr, /) -> None:
for item in o.items:
if isinstance(item, tuple):
item[0].accept(self)
if item[3] is not None:
item[3].accept(self)
else:
item.accept(self)

def visit_set_expr(self, o: SetExpr, /) -> None:
for item in o.items:
item.accept(self)
Expand Down Expand Up @@ -785,6 +795,11 @@ def visit_dict_expr(self, o: DictExpr, /) -> None:
return
super().visit_dict_expr(o)

def visit_template_str_expr(self, o: TemplateStrExpr, /) -> None:
if not self.visit(o):
return
super().visit_template_str_expr(o)

def visit_tuple_expr(self, o: TupleExpr, /) -> None:
if not self.visit(o):
return
Expand Down
13 changes: 13 additions & 0 deletions mypy/treetransform.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
StrExpr,
SuperExpr,
SymbolTable,
TemplateStrExpr,
TempNode,
TryStmt,
TupleExpr,
Expand Down Expand Up @@ -578,6 +579,18 @@ def visit_dict_expr(self, node: DictExpr) -> DictExpr:
[(self.expr(key) if key else None, self.expr(value)) for key, value in node.items]
)

def visit_template_str_expr(self, node: TemplateStrExpr) -> TemplateStrExpr:
items: list[Expression | tuple[Expression, str, str | None, Expression | None]] = []
for item in node.items:
if isinstance(item, tuple):
value, source, conversion, format_spec = item
items.append(
(self.expr(value), source, conversion, self.optional_expr(format_spec))
)
else:
items.append(self.expr(item))
return TemplateStrExpr(items)

def visit_tuple_expr(self, node: TupleExpr) -> TupleExpr:
return TupleExpr(self.expressions(node.items))

Expand Down
7 changes: 7 additions & 0 deletions mypy/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def visit_list_expr(self, o: mypy.nodes.ListExpr, /) -> T:
def visit_dict_expr(self, o: mypy.nodes.DictExpr, /) -> T:
pass

@abstractmethod
def visit_template_str_expr(self, o: mypy.nodes.TemplateStrExpr, /) -> T:
pass

@abstractmethod
def visit_tuple_expr(self, o: mypy.nodes.TupleExpr, /) -> T:
pass
Expand Down Expand Up @@ -539,6 +543,9 @@ def visit_list_expr(self, o: mypy.nodes.ListExpr, /) -> T:
def visit_dict_expr(self, o: mypy.nodes.DictExpr, /) -> T:
raise NotImplementedError()

def visit_template_str_expr(self, o: mypy.nodes.TemplateStrExpr, /) -> T:
raise NotImplementedError()

def visit_tuple_expr(self, o: mypy.nodes.TupleExpr, /) -> T:
raise NotImplementedError()

Expand Down
4 changes: 4 additions & 0 deletions mypyc/irbuild/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
StarExpr,
StrExpr,
SuperExpr,
TemplateStrExpr,
TempNode,
TryStmt,
TupleExpr,
Expand Down Expand Up @@ -311,6 +312,9 @@ def visit_tuple_expr(self, expr: TupleExpr) -> Value:
def visit_dict_expr(self, expr: DictExpr) -> Value:
return transform_dict_expr(self.builder, expr)

def visit_template_str_expr(self, expr: TemplateStrExpr) -> Value:
assert False, "TemplateStrExpr should have been handled already"

def visit_set_expr(self, expr: SetExpr) -> Value:
return transform_set_expr(self.builder, expr)

Expand Down
35 changes: 32 additions & 3 deletions test-data/unit/check-python314.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
[case testTemplateString]
reveal_type(t"mypy") # E: PEP 750 template strings are not yet supported \
# N: Revealed type is "Any"
[case testTemplateStringBasics]
reveal_type(t"foobar") # N: Revealed type is "string.templatelib.Template"
t"{'foobar'}"
t"foo{'bar'}"
t".{1}."
t"{type(1)}"
t"{1!r}"
t"{1:03d}"
t"{1!r:03d}"

from string.templatelib import Template
a: Template
a = t"foobar"
a = t"{'foobar'}"
[builtins fixtures/f_string.pyi]

[case testTemplateStringExpressionsOk]
t".{1 + 1}."
t".{1 + 1}.{'foo' + 'bar'}"
[builtins fixtures/f_string.pyi]

[case testTemplateStringExpressionsErrors]
t"{1 + ''}" # E: Unsupported operand types for + ("int" and "str")
t".{1 + ''}" # E: Unsupported operand types for + ("int" and "str")
[builtins fixtures/f_string.pyi]

[case testTemplateStringParseFormatOptions]
value = 10.5142
width = 10
precision = 4
t"result: {value:{width}.{precision}}"
[builtins fixtures/f_string.pyi]
Loading