Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Release date: TBA

Refs #3107

* Bound the field width and precision when inferring ``str.format`` calls.
A template such as ``"{:>2000000000}".format("x")`` (or a width/precision
supplied through a nested ``{}`` field) previously built the padded
multi-gigabyte string eagerly during inference; such calls now infer as
``Uninferable`` when the field size would be oversized.

Refs #3131

* Remove the ``asname`` keyword argument from ``Import._infer`` and
``ImportFrom._infer``. The alias-to-real-name translation that
``asname=True`` performed is now hoisted into ``ImportNode._infer_name``,
Expand Down
33 changes: 32 additions & 1 deletion astroid/brain/brain_builtin_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from __future__ import annotations

import itertools
import re
import string
from collections.abc import Callable, Iterable, Iterator
from functools import partial
from typing import TYPE_CHECKING, Any, NoReturn, cast
Expand Down Expand Up @@ -43,6 +45,33 @@

OBJECT_DUNDER_NEW = "object.__new__"

# Largest field width/precision we are willing to materialize when inferring a
# str.format() call. A crafted template such as "{:>2000000000}" would otherwise
# eagerly build a multi-gigabyte string during inference.
_MAX_FORMAT_FIELD_SIZE = int(1e8)
_FORMAT_SPEC_RE = re.compile(
r"(?:.?[<>=^])?[-+ ]?z?#?0?(?P<width>\d+)?[_,]?(?:\.(?P<precision>\d+))?.?$"
)


class _BoundedFormatter(string.Formatter):
"""A ``str.format`` implementation that refuses oversized width/precision.

Nested replacement fields inside a format spec ("{:>{}}") are already
resolved by ``Formatter`` before ``format_field`` runs, so the check here
also covers widths supplied through arguments.
"""

def format_field(self, value: object, format_spec: str) -> str:
match = _FORMAT_SPEC_RE.match(format_spec)
if match:
for field in ("width", "precision"):
size = match.group(field)
if size is not None and int(size) > _MAX_FORMAT_FIELD_SIZE:
raise ValueError("format field size exceeds inference limit")
return cast(str, super().format_field(value, format_spec))


STR_CLASS = """
class whatever(object):
def join(self, iterable):
Expand Down Expand Up @@ -1048,7 +1077,9 @@ def _infer_str_format_call(
keyword_values: dict[str, str] = {k: v.value for k, v in inferred_keyword.items()}

try:
formatted_string = format_template.format(*pos_values, **keyword_values)
formatted_string = _BoundedFormatter().format(
format_template, *pos_values, **keyword_values
)
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
# AttributeError: named field in format string was not found in the arguments
# IndexError: there are too few arguments to interpolate
Expand Down
5 changes: 5 additions & 0 deletions tests/brain/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ def test_string_format(self, format_string: str) -> None:
daniel_age = 12
"My name is {0.name}".format(daniel_age)
""",
pytest.param(""""{:>2000000000}".format("x")""", id="oversized-width"),
pytest.param(""""{:.2000000000f}".format(1.0)""", id="oversized-precision"),
pytest.param(
""""{:>{}}".format("x", 2000000000)""", id="oversized-nested-width"
),
],
)
def test_string_format_uninferable(self, format_string: str) -> None:
Expand Down