json.py 3.7 KB

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