|
34 | 34 | CYCLE_MARKER = object() |
35 | 35 |
|
36 | 36 |
|
| 37 | +global_repr_processors = [] |
| 38 | + |
| 39 | + |
| 40 | +def add_global_repr_processor(processor): |
| 41 | + global_repr_processors.append(processor) |
| 42 | + |
| 43 | + |
37 | 44 | def _get_debug_hub(): |
38 | 45 | # This function is replaced by debug.py |
39 | 46 | pass |
@@ -307,22 +314,40 @@ def safe_repr(value): |
307 | 314 | return u"<broken repr>" |
308 | 315 |
|
309 | 316 |
|
310 | | -def object_to_json(obj): |
311 | | - def _walk(obj, depth): |
312 | | - if depth < 4: |
| 317 | +def object_to_json(obj, remaining_depth=4, memo=None): |
| 318 | + if memo is None: |
| 319 | + memo = Memo() |
| 320 | + if memo.memoize(obj): |
| 321 | + return CYCLE_MARKER |
| 322 | + |
| 323 | + try: |
| 324 | + if remaining_depth > 0: |
| 325 | + hints = {"memo": memo, "remaining_depth": remaining_depth} |
| 326 | + for processor in global_repr_processors: |
| 327 | + with capture_internal_exceptions(): |
| 328 | + result = processor(obj, hints) |
| 329 | + if result is not NotImplemented: |
| 330 | + return result |
| 331 | + |
313 | 332 | if isinstance(obj, (list, tuple)): |
314 | 333 | # It is not safe to iterate over another sequence types as this may raise errors or |
315 | 334 | # bring undesired side-effects (e.g. Django querysets are executed during iteration) |
316 | | - return [_walk(x, depth + 1) for x in obj] |
317 | | - if isinstance(obj, Mapping): |
318 | | - return {safe_str(k): _walk(v, depth + 1) for k, v in obj.items()} |
| 335 | + return [ |
| 336 | + object_to_json(x, remaining_depth=remaining_depth - 1, memo=memo) |
| 337 | + for x in obj |
| 338 | + ] |
319 | 339 |
|
320 | | - if obj is CYCLE_MARKER: |
321 | | - return obj |
| 340 | + if isinstance(obj, Mapping): |
| 341 | + return { |
| 342 | + safe_str(k): object_to_json( |
| 343 | + v, remaining_depth=remaining_depth - 1, memo=memo |
| 344 | + ) |
| 345 | + for k, v in obj.items() |
| 346 | + } |
322 | 347 |
|
323 | 348 | return safe_repr(obj) |
324 | | - |
325 | | - return _walk(break_cycles(obj), 0) |
| 349 | + finally: |
| 350 | + memo.unmemoize(obj) |
326 | 351 |
|
327 | 352 |
|
328 | 353 | def extract_locals(frame): |
@@ -645,23 +670,18 @@ def strip_frame_mut(frame): |
645 | 670 | frame["vars"] = strip_databag(frame["vars"]) |
646 | 671 |
|
647 | 672 |
|
648 | | -def break_cycles(obj, memo=None): |
649 | | - if memo is None: |
650 | | - memo = {} |
651 | | - if id(obj) in memo: |
652 | | - return CYCLE_MARKER |
653 | | - memo[id(obj)] = obj |
| 673 | +class Memo(object): |
| 674 | + def __init__(self): |
| 675 | + self._inner = {} |
654 | 676 |
|
655 | | - try: |
656 | | - if isinstance(obj, Mapping): |
657 | | - return {k: break_cycles(v, memo) for k, v in obj.items()} |
658 | | - if isinstance(obj, (list, tuple)): |
659 | | - # It is not safe to iterate over another sequence types as this may raise errors or |
660 | | - # bring undesired side-effects (e.g. Django querysets are executed during iteration) |
661 | | - return [break_cycles(v, memo) for v in obj] |
662 | | - return obj |
663 | | - finally: |
664 | | - del memo[id(obj)] |
| 677 | + def memoize(self, obj): |
| 678 | + if id(obj) in self._inner: |
| 679 | + return True |
| 680 | + self._inner[id(obj)] = obj |
| 681 | + return False |
| 682 | + |
| 683 | + def unmemoize(self, obj): |
| 684 | + self._inner.pop(id(obj), None) |
665 | 685 |
|
666 | 686 |
|
667 | 687 | def convert_types(obj): |
|
0 commit comments