Skip to content

Commit 3426da8

Browse files
committed
fix(logger): no longer replaces object duplicates with "CIRCULAR"
1 parent e1110eb commit 3426da8

File tree

2 files changed

+109
-5
lines changed

2 files changed

+109
-5
lines changed

src/firebase_functions/logger.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,30 @@ def _remove_circular(obj: _typing.Any,
7575
if refs is None:
7676
refs = set()
7777

78+
# Check if the object is already in the current recursion stack
7879
if id(obj) in refs:
7980
return "[CIRCULAR]"
8081

82+
# For non-primitive objects, add the current object's id to the recursion stack
8183
if not isinstance(obj, (str, int, float, bool, type(None))):
8284
refs.add(id(obj))
8385

86+
# Recursively process the object based on its type
87+
result: _typing.Any
8488
if isinstance(obj, dict):
85-
return {key: _remove_circular(value, refs) for key, value in obj.items()}
89+
result = {key: _remove_circular(value, refs) for key, value in obj.items()}
8690
elif isinstance(obj, list):
87-
return [_remove_circular(value, refs) for _, value in enumerate(obj)]
91+
result = [_remove_circular(item, refs) for item in obj]
8892
elif isinstance(obj, tuple):
89-
return tuple(
90-
_remove_circular(value, refs) for _, value in enumerate(obj))
93+
result = tuple(_remove_circular(item, refs) for item in obj)
9194
else:
92-
return obj
95+
result = obj
96+
97+
# Remove the object's id from the recursion stack after processing
98+
if not isinstance(obj, (str, int, float, bool, type(None))):
99+
refs.remove(id(obj))
100+
101+
return result
93102

94103

95104
def _get_write_file(severity: LogSeverity) -> _typing.TextIO:

tests/test_logger.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
import json
77
from firebase_functions import logger
8+
from typing import Any
89

910

1011
class TestLogger:
@@ -79,3 +80,97 @@ def test_message_should_be_space_separated(
7980
raw_log_output = capsys.readouterr().out
8081
log_output = json.loads(raw_log_output)
8182
assert log_output["message"] == expected_message
83+
84+
def test_remove_circular_references(self, capsys: pytest.CaptureFixture[str]):
85+
# Create an object with a circular reference.
86+
circ: Any = {"b": "foo"}
87+
circ["circ"] = circ
88+
89+
entry = {
90+
"severity": "ERROR",
91+
"message": "testing circular",
92+
"circ": circ,
93+
}
94+
logger.write(entry)
95+
raw_log_output = capsys.readouterr().err
96+
log_output = json.loads(raw_log_output)
97+
98+
expected = {
99+
"severity": "ERROR",
100+
"message": "testing circular",
101+
"circ": {"b": "foo", "circ": "[CIRCULAR]"},
102+
}
103+
assert log_output == expected
104+
105+
def test_remove_circular_references_in_arrays(self, capsys: pytest.CaptureFixture[str]):
106+
# Create an object with a circular reference inside an array.
107+
circ: Any = {"b": "foo"}
108+
circ["circ"] = [circ]
109+
110+
entry = {
111+
"severity": "ERROR",
112+
"message": "testing circular",
113+
"circ": circ,
114+
}
115+
logger.write(entry)
116+
raw_log_output = capsys.readouterr().err
117+
log_output = json.loads(raw_log_output)
118+
119+
expected = {
120+
"severity": "ERROR",
121+
"message": "testing circular",
122+
"circ": {"b": "foo", "circ": ["[CIRCULAR]"]},
123+
}
124+
assert log_output == expected
125+
126+
def test_no_false_circular_for_duplicates(self, capsys: pytest.CaptureFixture[str]):
127+
# Ensure that duplicate objects (used in multiple keys) are not marked as circular.
128+
obj = {"a": "foo"}
129+
entry = {
130+
"severity": "ERROR",
131+
"message": "testing circular",
132+
"a": obj,
133+
"b": obj,
134+
}
135+
logger.write(entry)
136+
raw_log_output = capsys.readouterr().err
137+
log_output = json.loads(raw_log_output)
138+
139+
expected = {
140+
"severity": "ERROR",
141+
"message": "testing circular",
142+
"a": {"a": "foo"},
143+
"b": {"a": "foo"},
144+
}
145+
assert log_output == expected
146+
147+
def test_no_false_circular_in_array_duplicates(self, capsys: pytest.CaptureFixture[str]):
148+
# Ensure that duplicate objects in arrays are not falsely detected as circular.
149+
obj = {"a": "foo"}
150+
arr = [
151+
{"a": obj, "b": obj},
152+
{"a": obj, "b": obj},
153+
]
154+
entry = {
155+
"severity": "ERROR",
156+
"message": "testing circular",
157+
"a": arr,
158+
"b": arr,
159+
}
160+
logger.write(entry)
161+
raw_log_output = capsys.readouterr().err
162+
log_output = json.loads(raw_log_output)
163+
164+
expected = {
165+
"severity": "ERROR",
166+
"message": "testing circular",
167+
"a": [
168+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
169+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
170+
],
171+
"b": [
172+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
173+
{"a": {"a": "foo"}, "b": {"a": "foo"}},
174+
],
175+
}
176+
assert log_output == expected

0 commit comments

Comments
 (0)