Repository: ManimCommunity/manim
File: manim/utils/hashing.py (Python)

Code before:
            except (OSError, TypeError):
                # This happens when rendering videos included in the documentation
                # within doctests and should be replaced by a solution avoiding
                # hash collision (due to the same, empty, code strings) at some point.
                # See https://github.com/ManimCommunity/manim/pull/402.
                code = ""
            return self._cleaned_iterable({"code": code, "nonlocals": cvardict})
        elif isinstance(obj, np.ndarray):
            if obj.size > 1000:
                obj = np.resize(obj, (100, 100))
                return f"TRUNCATED ARRAY: {repr(obj)}"
            # We return the repr and not a list to avoid the JsonEncoder to iterate over it.
            return repr(obj)
        elif hasattr(obj, "__dict__"):
            temp = obj.__dict__
            # MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated.
            # Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side.
            if isinstance(temp, MappingProxyType):
                return "MappingProxy"
            return self._cleaned_iterable(temp)
        elif isinstance(obj, np.uint8):
            return int(obj)
        # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process.
        return str(type(obj))

    def _cleaned_iterable(self, iterable: Iterable[Any]) -> list[Any] | dict[Any, Any]:
        """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format.

        If a key with a bad format is found (i.e not a int, string, or float), it gets replaced byt its hash using the same process implemented here.
        If a circular reference is found within the iterable, it will be replaced by the value of ALREADY_PROCESSED_PLACEHOLDER.

        Parameters
        ----------
        iterable
            The iterable to check.
        """

        def _key_to_hash(key) -> int:
            return zlib.crc32(json.dumps(key, cls=_CustomEncoder).encode())

        def _iter_check_list(lst: list[Any]) -> list[Any]:
            processed_list = [None] * len(lst)
            for i, el in enumerate(lst):
                el = _Memoizer.check_already_processed(el)
                if isinstance(el, (list, tuple)):
                    new_value = _iter_check_list(el)
                elif isinstance(el, dict):
                    new_value = _iter_check_dict(el)
                else:
                    new_value = el
                processed_list[i] = new_value

Review comment (suggestion):
Despite the name, `iterable` can be either a `Sequence` or a `dict`, according to the code. Currently, `iterable` can't be really an `Iterable`, because `_iter_check_list` calculates its length, something which not all iterables have (like map, range or filter).

_EDIT: it is actually pretty easy to rewrite `_iter_check_list` to accept any iterables. See my suggestion below._

The name of this parameter (and function) should probably be changed.

Now, we could simply type `iterable` as `Sequence[Any] | dict[Any, Any]`, but may I suggest the following overloads to indicate that, if `iterable` is a `Sequence`, the function returns a `list` and, if it's a `dict`, it returns a `dict`:
```suggestion
    @overload
    def _cleaned_iterable(self, iterable: Sequence[Any]) -> list[Any]: ...
    
    @overload
    def _cleaned_iterable(self, iterable: dict[Any, Any]) -> dict[Any, Any]: ...
    
    def _cleaned_iterable(self, iterable):
```

Code after:
            except (OSError, TypeError):
                # This happens when rendering videos included in the documentation
                # within doctests and should be replaced by a solution avoiding
                # hash collision (due to the same, empty, code strings) at some point.
                # See https://github.com/ManimCommunity/manim/pull/402.
                code = ""
            return self._cleaned_iterable({"code": code, "nonlocals": cvardict})
        elif isinstance(obj, np.ndarray):
            if obj.size > 1000:
                obj = np.resize(obj, (100, 100))
                return f"TRUNCATED ARRAY: {repr(obj)}"
            # We return the repr and not a list to avoid the JSONEncoder to iterate over it.
            return repr(obj)
        elif hasattr(obj, "__dict__"):
            temp = obj.__dict__
            # MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated.
            # Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side.
            if isinstance(temp, MappingProxyType):
                return "MappingProxy"
            return self._cleaned_iterable(temp)
        elif isinstance(obj, np.uint8):
            return int(obj)
        # Serialize it with only the type of the object. You can change this to whatever string when debugging the serialization process.
        return str(type(obj))

    @overload
    def _cleaned_iterable(self, iterable: Sequence[Any]) -> list[Any]: ...

    @overload
    def _cleaned_iterable(self, iterable: dict[Any, Any]) -> dict[Any, Any]: ...

    def _cleaned_iterable(self, iterable):
        """Check for circular reference at each iterable that will go through the JSONEncoder, as well as key of the wrong format.

        If a key with a bad format is found (i.e not a int, string, or float), it gets replaced by its hash using the same process implemented here.
        If a circular reference is found within the iterable, it will be replaced by the value of ALREADY_PROCESSED_PLACEHOLDER.

        Parameters
        ----------
        iterable
            The iterable to check.
        """

        def _key_to_hash(key: Any) -> int:
            return zlib.crc32(json.dumps(key, cls=_CustomEncoder).encode())

        def _iter_check_list(lst: Sequence[Any]) -> list[Any]:
            processed_list = [None] * len(lst)
            for i, el in enumerate(lst):
                el = _Memoizer.check_already_processed(el)
                if isinstance(el, (list, tuple)):