Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement inference for JoinedStr and FormattedValue #2459

Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ What's New in astroid 3.2.3?
============================
Release date: TBA


* Implement inference for JoinedStr and FormattedValue
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved


Pierre-Sassoulas marked this conversation as resolved.
Show resolved Hide resolved
* Fix ``AssertionError`` when inferring a property consisting of a partial function.

Closes pylint-dev/pylint#9214
Expand Down
52 changes: 52 additions & 0 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4667,6 +4667,34 @@ 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
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
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,
)


class JoinedStr(NodeNG):
"""Represents a list of string expressions to be joined.
Expand Down Expand Up @@ -4728,6 +4756,30 @@ 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)
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def _infer_from_values(
cls, nodes: list[NodeNG], context: InferenceContext | None = None, **kwargs: Any
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
) -> Generator[InferenceResult, None, InferenceErrorInfo | None]:
if len(nodes) == 1:
yield from nodes[0]._infer(context, **kwargs)
return
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 node is util.Uninferable:
result += "{Uninferable}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we should exit early here and not continue to product "half" matching values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's philosophical, depending on the strategy for dealing with partially inferred collections of values

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would help to make a decision about this if we could see a test case. At the moment, I do not think we should manufacture a constant that would never exist at runtime with the substring "Uninferable" in it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's "coincidental" coverage of this line in test_typing_extensions_types based on the current implementation of typing, but it would be better to have explicit coverage.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Jacob. I think Uninferable makes sense to those that have worked on astroid and pylint, but this will also be shown to users who might have no idea what it means. I would be in favour of exiting early and not creating partially correct values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

@ericvergnaud ericvergnaud Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is useful to know which fragment(s) of a JoinedStr cause inference failure, but I agree that half-baked strings can be puzzling.

elif isinstance(node, Const):
result += str(node.value)
else:
result += node.as_string()
jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
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!")

jacobtylerwalls marked this conversation as resolved.
Show resolved Hide resolved
def test_float_complex_ambiguity(self) -> None:
code = '''
def no_conjugate_member(magic_flag): #@
Expand Down
Loading