Coverage for configzen/formats/std_json.py: 70%

38 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-15 02:42 +0000

1"""`configzen.formats.std_json`: The JSON data format.""" 

2 

3from __future__ import annotations 1abcdefgh

4 

5from json import JSONDecoder, JSONEncoder, dump, load 1abcdefgh

6from typing import IO, TYPE_CHECKING, ClassVar, cast 1abcdefgh

7 

8from runtime_generics import runtime_generic 1abcdefgh

9 

10from configzen.data import DataFormatOptions, TextDataFormat 1abcdefgh

11 

12if TYPE_CHECKING: 1abcdefgh

13 from collections.abc import Callable 

14 

15 from typing_extensions import Unpack 

16 

17 from configzen.data import Data 

18 

19 

20__all__ = ( 1abcdefgh

21 "JSONDataFormat", 

22 "JSONOptions", 

23) 

24 

25 

26class JSONOptions(DataFormatOptions, total=False): 1abcdefgh

27 """Prototype of the allowed options for the JSON data format.""" 

28 

29 object_hook: Callable[[dict[str, object]], object] | None # JSONDecoder 1abcdefgh

30 parse_float: Callable[[str], float] | None # JSONDecoder 1abcdefgh

31 parse_int: Callable[[str], int] | None # JSONDecoder 1abcdefgh

32 parse_constant: Callable[[str], object] | None # JSONDecoder 1abcdefgh

33 strict: bool # JSONDecoder 1abcdefgh

34 object_pairs_hook: ( 1abcdefgh

35 Callable[ 

36 [list[tuple[str, object]]], 

37 object, 

38 ] 

39 | None 

40 ) # JSONDecoder 

41 

42 skipkeys: bool # JSONEncoder 1abcdefgh

43 ensure_ascii: bool # JSONEncoder 1abcdefgh

44 check_circular: bool # JSONEncoder 1abcdefgh

45 allow_nan: bool # JSONEncoder 1abcdefgh

46 sort_keys: bool # JSONEncoder 1abcdefgh

47 indent: int | str | None # JSONEncoder 1abcdefgh

48 separators: tuple[str, str] | None # JSONEncoder 1abcdefgh

49 default: Callable[..., object] | None # JSONEncoder 1abcdefgh

50 

51 

52@runtime_generic 1abcdefgh

53class JSONDataFormat(TextDataFormat[JSONOptions]): 1abcdefgh

54 """The JSON data format.""" 

55 

56 option_name: ClassVar[str] = "json" 1abcdefgh

57 

58 # Subclass and override for global effect. 

59 json_encoder: JSONEncoder = JSONEncoder() 1abcdefgh

60 json_decoder: JSONDecoder = JSONDecoder() 1abcdefgh

61 

62 default_extension: ClassVar[str] = "json" 1abcdefgh

63 

64 def configure(self, **options: Unpack[JSONOptions]) -> None: 1abcdefgh

65 """For the documentation of the options, see the JSONOptions class.""" 

66 self.json_encoder = JSONEncoder( 

67 skipkeys=options.get("skipkeys") or self.json_encoder.skipkeys, 

68 ensure_ascii=options.get("ensure_ascii") or self.json_encoder.ensure_ascii, 

69 check_circular=options.get("check_circular") 

70 or self.json_encoder.check_circular, 

71 allow_nan=options.get("allow_nan") or self.json_encoder.allow_nan, 

72 indent=options.get("indent") or self.json_encoder.indent, 

73 separators=options.get("separators") 

74 or ( 

75 self.json_encoder.item_separator, 

76 self.json_encoder.key_separator, 

77 ), 

78 default=options.get("default") or self.json_encoder.default, 

79 ) 

80 self.json_decoder = JSONDecoder( 

81 object_hook=options.get("object_hook") or self.json_decoder.object_hook, 

82 parse_float=options.get("parse_float") or self.json_decoder.parse_float, 

83 parse_int=options.get("parse_int") or self.json_decoder.parse_int, 

84 parse_constant=options.get("parse_constant") 

85 or self.json_decoder.parse_constant, 

86 strict=options.get("strict") or self.json_decoder.strict, 

87 object_pairs_hook=options.get("object_pairs_hook") 

88 or self.json_decoder.object_pairs_hook, 

89 ) 

90 

91 def load(self, stream: IO[str]) -> Data: 1abcdefgh

92 """Load the JSON data from the given stream.""" 

93 document = ( 

94 load( 

95 stream, 

96 cls=cast("type[JSONDecoder]", lambda **_: self.json_decoder), 

97 ) 

98 or {} 

99 ) 

100 if not isinstance(document, dict): 

101 msg = ( 

102 f"Expected a dict mapping, " 

103 f"but got {type(document).__name__} instead." 

104 ) 

105 raise TypeError(msg) 

106 return document 

107 

108 def dump(self, data: Data, stream: IO[str]) -> None: 1abcdefgh

109 """Dump the given JSON data to the given stream.""" 

110 dump( 

111 data, 

112 stream, 

113 cls=cast("type[JSONEncoder]", lambda **_: self.json_encoder), 

114 )