Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed
-----

- Fix a ``RecursionError`` that occurs with deeply-nested request bodies. (:pr:`NUMBER`)
67 changes: 58 additions & 9 deletions src/globus_sdk/transport/representation_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import enum
import typing as t
import uuid
from collections import deque

from globus_sdk._internal import orjson_compat
from globus_sdk._missing import MISSING, filter_missing
Expand Down Expand Up @@ -106,18 +107,66 @@ def _prepare_data(self, data: t.Any) -> t.Any:
"""
Prepare the data (body) for a request.

If the body is a dict, list, or tuple, it will be recursively processed to
filter out MISSING and format primitives.

Otherwise, it is returned as-is.
MISSING values are filtered out of dicts, lists, and tuples.
Scalar elements, like UUIDs and enums, and converted to strings.
"""
if isinstance(data, dict):
return filter_missing({k: self._prepare_data(v) for k, v in data.items()})
elif isinstance(data, (list, tuple)):
return [self._prepare_data(x) for x in data if x is not MISSING]
else:

if not isinstance(data, (dict, list, tuple)):
return self._format_primitive(data)

# Strategy: Rely on object mutability to iteratively prepare data
# after it has already been appended to a list / assigned in a dict.
data = self._create_mutable_copy(data)
unprocessed_data: deque[dict[str, t.Any] | list[t.Any]] = deque((data,))

while unprocessed_data:
current_data = unprocessed_data.popleft()

if isinstance(current_data, dict):
prepared_dict: dict[str, t.Any] = {
k: self._prepare_element(v, unprocessed_data)
for k, v in current_data.items()
if v is not MISSING
}
current_data.clear()
current_data.update(prepared_dict)

else: # isinstance(current_data, list)
prepared_list: list[t.Any] = [
self._prepare_element(v, unprocessed_data)
for v in current_data
if v is not MISSING
]
current_data.clear()
current_data.extend(prepared_list)

return data

def _prepare_element(
self, element: t.Any, unprocessed_data: deque[dict[str, t.Any] | list[t.Any]]
) -> t.Any:
if not isinstance(element, (dict, list, tuple)):
return self._format_primitive(element)
element = self._create_mutable_copy(element)
unprocessed_data.append(element)
return element

@staticmethod
def _create_mutable_copy(
data: dict[str, t.Any] | list[t.Any] | tuple[t.Any, ...],
) -> dict[str, t.Any] | list[t.Any]:
"""
Make a mutable copy of a non-scalar value.

Tuples will be converted to lists.
"""

if isinstance(data, tuple):
return list(data)
if isinstance(data, list):
return data.copy()
return data.copy()
Comment thread
kurtmckee marked this conversation as resolved.


class RequestsPlainTextProvider(RequestsRepresentationProvider):
"""The plain-text provider ensures that the body is text."""
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/transport/test_representation_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from globus_sdk import MISSING
from globus_sdk.transport.representation_providers import RequestsRepresentationProvider


def test_deeply_nested_body():
"""Verify that `RecursionError` doesn't manifest with a deeply-nested POST body."""

deepest = {
"value": MISSING,
"list": [MISSING],
"dict": {"missing": MISSING},
}

deep = deepest
for _ in range(1_000):
deep = {
"deep": [deep],
"missing": MISSING,
}
pass

provider = RequestsRepresentationProvider()
result = provider._prepare_data(deep)

# * The original `deepest` variable must not have been modified.
assert "value" in deepest
assert deepest["list"] == [MISSING]
assert deepest["dict"] == {"missing": MISSING}

# * "missing" must not appear in the result value.
assert "missing" not in result
assert "missing" not in result["deep"][0]["deep"][0]["deep"][0]
Loading