json.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import json
  2. from contextlib import suppress
  3. from pathlib import PurePath
  4. from typing import (
  5. Any,
  6. Callable,
  7. ClassVar,
  8. Dict,
  9. List,
  10. Mapping,
  11. Optional,
  12. Sequence,
  13. Tuple,
  14. )
  15. from .registry import _import_class, get_filesystem_class
  16. from .spec import AbstractFileSystem
  17. class FilesystemJSONEncoder(json.JSONEncoder):
  18. include_password: ClassVar[bool] = True
  19. def default(self, o: Any) -> Any:
  20. if isinstance(o, AbstractFileSystem):
  21. return o.to_dict(include_password=self.include_password)
  22. if isinstance(o, PurePath):
  23. cls = type(o)
  24. return {"cls": f"{cls.__module__}.{cls.__name__}", "str": str(o)}
  25. return super().default(o)
  26. def make_serializable(self, obj: Any) -> Any:
  27. """
  28. Recursively converts an object so that it can be JSON serialized via
  29. :func:`json.dumps` and :func:`json.dump`, without actually calling
  30. said functions.
  31. """
  32. if isinstance(obj, (str, int, float, bool)):
  33. return obj
  34. if isinstance(obj, Mapping):
  35. return {k: self.make_serializable(v) for k, v in obj.items()}
  36. if isinstance(obj, Sequence):
  37. return [self.make_serializable(v) for v in obj]
  38. return self.default(obj)
  39. class FilesystemJSONDecoder(json.JSONDecoder):
  40. def __init__(
  41. self,
  42. *,
  43. object_hook: Optional[Callable[[Dict[str, Any]], Any]] = None,
  44. parse_float: Optional[Callable[[str], Any]] = None,
  45. parse_int: Optional[Callable[[str], Any]] = None,
  46. parse_constant: Optional[Callable[[str], Any]] = None,
  47. strict: bool = True,
  48. object_pairs_hook: Optional[Callable[[List[Tuple[str, Any]]], Any]] = None,
  49. ) -> None:
  50. self.original_object_hook = object_hook
  51. super().__init__(
  52. object_hook=self.custom_object_hook,
  53. parse_float=parse_float,
  54. parse_int=parse_int,
  55. parse_constant=parse_constant,
  56. strict=strict,
  57. object_pairs_hook=object_pairs_hook,
  58. )
  59. @classmethod
  60. def try_resolve_path_cls(cls, dct: Dict[str, Any]):
  61. with suppress(Exception):
  62. fqp = dct["cls"]
  63. path_cls = _import_class(fqp)
  64. if issubclass(path_cls, PurePath):
  65. return path_cls
  66. return None
  67. @classmethod
  68. def try_resolve_fs_cls(cls, dct: Dict[str, Any]):
  69. with suppress(Exception):
  70. if "cls" in dct:
  71. try:
  72. fs_cls = _import_class(dct["cls"])
  73. if issubclass(fs_cls, AbstractFileSystem):
  74. return fs_cls
  75. except Exception:
  76. if "protocol" in dct: # Fallback if cls cannot be imported
  77. return get_filesystem_class(dct["protocol"])
  78. raise
  79. return None
  80. def custom_object_hook(self, dct: Dict[str, Any]):
  81. if "cls" in dct:
  82. if (obj_cls := self.try_resolve_fs_cls(dct)) is not None:
  83. return AbstractFileSystem.from_dict(dct)
  84. if (obj_cls := self.try_resolve_path_cls(dct)) is not None:
  85. return obj_cls(dct["str"])
  86. if self.original_object_hook is not None:
  87. return self.original_object_hook(dct)
  88. return dct
  89. def unmake_serializable(self, obj: Any) -> Any:
  90. """
  91. Inverse function of :meth:`FilesystemJSONEncoder.make_serializable`.
  92. """
  93. if isinstance(obj, dict):
  94. obj = self.custom_object_hook(obj)
  95. if isinstance(obj, dict):
  96. return {k: self.unmake_serializable(v) for k, v in obj.items()}
  97. if isinstance(obj, (list, tuple)):
  98. return [self.unmake_serializable(v) for v in obj]
  99. return obj