from __future__ import annotations import copyreg from io import BytesIO from pickle import PicklingError from typing import Any, Callable import dill loads = dill.loads class CouldNotBePickled: def __init__(self, class_name: str) -> None: self.class_name = class_name def __repr__(self) -> str: return f"Item of type '{self.class_name}' could not be pickled and was omitted." @classmethod def reduce(cls, item: Any) -> tuple[Callable[[str], CouldNotBePickled], tuple[str]]: return cls, (type(item).__name__,) class Pickler(dill.Pickler): """ Overload dill to ignore unpicklable parts of objects. You probably didn't want to use these parts anyhow. However, if you did, you'll have to find some way to make them picklable. """ def save(self, obj: Any, save_persistent_id: bool = True) -> None: """ Copied from pickle and amended. """ self.framer.commit_frame() # Check for persistent id (defined by a subclass) pid = self.persistent_id(obj) if pid is not None and save_persistent_id: self.save_pers(pid) return # Check the memo x = self.memo.get(id(obj)) if x is not None: self.write(self.get(x[0])) return rv = NotImplemented reduce = getattr(self, "reducer_override", None) if reduce is not None: rv = reduce(obj) if rv is NotImplemented: # Check the type dispatch table t = type(obj) f = self.dispatch.get(t) if f is not None: f(self, obj) # Call unbound method with explicit self return # Check private dispatch table if any, or else # copyreg.dispatch_table reduce = getattr(self, 'dispatch_table', copyreg.dispatch_table).get(t) if reduce is not None: rv = reduce(obj) else: # Check for a class with a custom metaclass; treat as regular # class if issubclass(t, type): self.save_global(obj) return # Check for a __reduce_ex__ method, fall back to __reduce__ reduce = getattr(obj, "__reduce_ex__", None) try: if reduce is not None: rv = reduce(self.proto) else: reduce = getattr(obj, "__reduce__", None) if reduce is not None: rv = reduce() else: raise PicklingError("Can't pickle %r object: %r" % (t.__name__, obj)) except Exception: # noqa rv = CouldNotBePickled.reduce(obj) # Check for string returned by reduce(), meaning "save as global" if isinstance(rv, str): try: self.save_global(obj, rv) except Exception: # noqa self.save_global(obj, CouldNotBePickled.reduce(obj)) return # Assert that reduce() returned a tuple if not isinstance(rv, tuple): raise PicklingError("%s must return string or tuple" % reduce) # Assert that it returned an appropriately sized tuple length = len(rv) if not (2 <= length <= 6): raise PicklingError("Tuple returned by %s must have " "two to six elements" % reduce) # Save the reduce() output and finally memoize the object try: self.save_reduce(obj=obj, *rv) except Exception: # noqa self.save_reduce(obj=obj, *CouldNotBePickled.reduce(obj)) def dumps(obj: Any, protocol: str = None, byref: bool = None, fmode: str = None, recurse: bool = True, **kwds: Any) -> bytes: """pickle an object to a string""" protocol = dill.settings['protocol'] if protocol is None else int(protocol) _kwds = kwds.copy() _kwds.update(dict(byref=byref, fmode=fmode, recurse=recurse)) with BytesIO() as file: Pickler(file, protocol, **_kwds).dump(obj) return file.getvalue()