Skip to content

Commit

Permalink
Implement inference for JoinedStr and FormattedValue (#2459)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericvergnaud authored Aug 2, 2024
1 parent 3bcf5ec commit 3dbf139
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 0 deletions.
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Release date: TBA

Closes pylint-dev/pylint#7126

* Implement inference for JoinedStr and FormattedValue



What's New in astroid 3.2.5?
============================
Expand Down
59 changes: 59 additions & 0 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4673,6 +4673,37 @@ def get_children(self):
if self.format_spec is not None:
yield self.format_spec

def _infer(
self, context: InferenceContext | None = None, **kwargs: Any
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
if self.format_spec is None:
yield from self.value.infer(context, **kwargs)
return
uninferable_already_generated = False
for format_spec in self.format_spec.infer(context, **kwargs):
if not isinstance(format_spec, Const):
if not uninferable_already_generated:
yield util.Uninferable
uninferable_already_generated = True
continue
for value in self.value.infer(context, **kwargs):
if not isinstance(value, Const):
if not uninferable_already_generated:
yield util.Uninferable
uninferable_already_generated = True
continue
formatted = format(value.value, format_spec.value)
yield Const(
formatted,
lineno=self.lineno,
col_offset=self.col_offset,
end_lineno=self.end_lineno,
end_col_offset=self.end_col_offset,
)


MISSING_VALUE = "{MISSING_VALUE}"


class JoinedStr(NodeNG):
"""Represents a list of string expressions to be joined.
Expand Down Expand Up @@ -4734,6 +4765,34 @@ def postinit(self, values: list[NodeNG] | None = None) -> None:
def get_children(self):
yield from self.values

def _infer(
self, context: InferenceContext | None = None, **kwargs: Any
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
yield from self._infer_from_values(self.values, context)

@classmethod
def _infer_from_values(
cls, nodes: list[NodeNG], context: InferenceContext | None = None, **kwargs: Any
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
if len(nodes) == 1:
yield from nodes[0]._infer(context, **kwargs)
return
uninferable_already_generated = False
for prefix in nodes[0]._infer(context, **kwargs):
for suffix in cls._infer_from_values(nodes[1:], context, **kwargs):
result = ""
for node in (prefix, suffix):
if isinstance(node, Const):
result += str(node.value)
continue
result += MISSING_VALUE
if MISSING_VALUE in result:
if not uninferable_already_generated:
uninferable_already_generated = True
yield util.Uninferable
else:
yield Const(result)


class NamedExpr(_base_nodes.AssignTypeNode):
"""Represents the assignment from the assignment expression
Expand Down
29 changes: 29 additions & 0 deletions tests/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pytest

from astroid import (
Const,
Slice,
Uninferable,
arguments,
Expand Down Expand Up @@ -652,6 +653,34 @@ def process_line(word_pos):
)
)

def test_fstring_inference(self) -> None:
code = """
name = "John"
result = f"Hello {name}!"
"""
ast = parse(code, __name__)
node = ast["result"]
inferred = node.inferred()
self.assertEqual(len(inferred), 1)
value_node = inferred[0]
self.assertIsInstance(value_node, Const)
self.assertEqual(value_node.value, "Hello John!")

def test_formatted_fstring_inference(self) -> None:
code = """
width = 10
precision = 4
value = 12.34567
result = f"result: {value:{width}.{precision}}!"
"""
ast = parse(code, __name__)
node = ast["result"]
inferred = node.inferred()
self.assertEqual(len(inferred), 1)
value_node = inferred[0]
self.assertIsInstance(value_node, Const)
self.assertEqual(value_node.value, "result: 12.35!")

def test_float_complex_ambiguity(self) -> None:
code = '''
def no_conjugate_member(magic_flag): #@
Expand Down

0 comments on commit 3dbf139

Please sign in to comment.