122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
|
import json
|
||
|
from contextlib import suppress
|
||
|
from pathlib import PurePath
|
||
|
from typing import (
|
||
|
Any,
|
||
|
Callable,
|
||
|
ClassVar,
|
||
|
Dict,
|
||
|
List,
|
||
|
Mapping,
|
||
|
Optional,
|
||
|
Sequence,
|
||
|
Tuple,
|
||
|
)
|
||
|
|
||
|
from .registry import _import_class, get_filesystem_class
|
||
|
from .spec import AbstractFileSystem
|
||
|
|
||
|
|
||
|
class FilesystemJSONEncoder(json.JSONEncoder):
|
||
|
include_password: ClassVar[bool] = True
|
||
|
|
||
|
def default(self, o: Any) -> Any:
|
||
|
if isinstance(o, AbstractFileSystem):
|
||
|
return o.to_dict(include_password=self.include_password)
|
||
|
if isinstance(o, PurePath):
|
||
|
cls = type(o)
|
||
|
return {"cls": f"{cls.__module__}.{cls.__name__}", "str": str(o)}
|
||
|
|
||
|
return super().default(o)
|
||
|
|
||
|
def make_serializable(self, obj: Any) -> Any:
|
||
|
"""
|
||
|
Recursively converts an object so that it can be JSON serialized via
|
||
|
:func:`json.dumps` and :func:`json.dump`, without actually calling
|
||
|
said functions.
|
||
|
"""
|
||
|
if isinstance(obj, (str, int, float, bool)):
|
||
|
return obj
|
||
|
if isinstance(obj, Mapping):
|
||
|
return {k: self.make_serializable(v) for k, v in obj.items()}
|
||
|
if isinstance(obj, Sequence):
|
||
|
return [self.make_serializable(v) for v in obj]
|
||
|
|
||
|
return self.default(obj)
|
||
|
|
||
|
|
||
|
class FilesystemJSONDecoder(json.JSONDecoder):
|
||
|
def __init__(
|
||
|
self,
|
||
|
*,
|
||
|
object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
||
|
parse_float: Optional[Callable[[str], Any]] = None,
|
||
|
parse_int: Optional[Callable[[str], Any]] = None,
|
||
|
parse_constant: Optional[Callable[[str], Any]] = None,
|
||
|
strict: bool = True,
|
||
|
object_pairs_hook: Optional[Callable[[List[Tuple[str, Any]]], Any]] = None,
|
||
|
) -> None:
|
||
|
self.original_object_hook = object_hook
|
||
|
|
||
|
super().__init__(
|
||
|
object_hook=self.custom_object_hook,
|
||
|
parse_float=parse_float,
|
||
|
parse_int=parse_int,
|
||
|
parse_constant=parse_constant,
|
||
|
strict=strict,
|
||
|
object_pairs_hook=object_pairs_hook,
|
||
|
)
|
||
|
|
||
|
@classmethod
|
||
|
def try_resolve_path_cls(cls, dct: Dict[str, Any]):
|
||
|
with suppress(Exception):
|
||
|
fqp = dct["cls"]
|
||
|
|
||
|
path_cls = _import_class(fqp)
|
||
|
|
||
|
if issubclass(path_cls, PurePath):
|
||
|
return path_cls
|
||
|
|
||
|
return None
|
||
|
|
||
|
@classmethod
|
||
|
def try_resolve_fs_cls(cls, dct: Dict[str, Any]):
|
||
|
with suppress(Exception):
|
||
|
if "cls" in dct:
|
||
|
try:
|
||
|
fs_cls = _import_class(dct["cls"])
|
||
|
if issubclass(fs_cls, AbstractFileSystem):
|
||
|
return fs_cls
|
||
|
except Exception:
|
||
|
if "protocol" in dct: # Fallback if cls cannot be imported
|
||
|
return get_filesystem_class(dct["protocol"])
|
||
|
|
||
|
raise
|
||
|
|
||
|
return None
|
||
|
|
||
|
def custom_object_hook(self, dct: Dict[str, Any]):
|
||
|
if "cls" in dct:
|
||
|
if (obj_cls := self.try_resolve_fs_cls(dct)) is not None:
|
||
|
return AbstractFileSystem.from_dict(dct)
|
||
|
if (obj_cls := self.try_resolve_path_cls(dct)) is not None:
|
||
|
return obj_cls(dct["str"])
|
||
|
|
||
|
if self.original_object_hook is not None:
|
||
|
return self.original_object_hook(dct)
|
||
|
|
||
|
return dct
|
||
|
|
||
|
def unmake_serializable(self, obj: Any) -> Any:
|
||
|
"""
|
||
|
Inverse function of :meth:`FilesystemJSONEncoder.make_serializable`.
|
||
|
"""
|
||
|
if isinstance(obj, dict):
|
||
|
obj = self.custom_object_hook(obj)
|
||
|
if isinstance(obj, dict):
|
||
|
return {k: self.unmake_serializable(v) for k, v in obj.items()}
|
||
|
if isinstance(obj, (list, tuple)):
|
||
|
return [self.unmake_serializable(v) for v in obj]
|
||
|
|
||
|
return obj
|