Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jupyter_client/jsonutil.py: 23%

97 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +0000

1"""Utilities to manipulate JSON objects.""" 

2# Copyright (c) Jupyter Development Team. 

3# Distributed under the terms of the Modified BSD License. 

4import math 

5import numbers 

6import re 

7import types 

8import warnings 

9from binascii import b2a_base64 

10from collections.abc import Iterable 

11from datetime import datetime 

12from typing import Optional, Union 

13 

14from dateutil.parser import parse as _dateutil_parse 

15from dateutil.tz import tzlocal 

16 

17next_attr_name = "__next__" # Not sure what downstream library uses this, but left it to be safe 

18 

19# ----------------------------------------------------------------------------- 

20# Globals and constants 

21# ----------------------------------------------------------------------------- 

22 

23# timestamp formats 

24ISO8601 = "%Y-%m-%dT%H:%M:%S.%f" 

25ISO8601_PAT = re.compile( 

26 r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d{1,6})?(Z|([\+\-]\d{2}:?\d{2}))?$" 

27) 

28 

29# holy crap, strptime is not threadsafe. 

30# Calling it once at import seems to help. 

31datetime.strptime("1", "%d") # noqa 

32 

33# ----------------------------------------------------------------------------- 

34# Classes and functions 

35# ----------------------------------------------------------------------------- 

36 

37 

38def _ensure_tzinfo(dt: datetime) -> datetime: 

39 """Ensure a datetime object has tzinfo 

40 

41 If no tzinfo is present, add tzlocal 

42 """ 

43 if not dt.tzinfo: 

44 # No more naïve datetime objects! 

45 warnings.warn( 

46 "Interpreting naive datetime as local %s. Please add timezone info to timestamps." % dt, 

47 DeprecationWarning, 

48 stacklevel=4, 

49 ) 

50 dt = dt.replace(tzinfo=tzlocal()) 

51 return dt 

52 

53 

54def parse_date(s: Optional[str]) -> Optional[Union[str, datetime]]: 

55 """parse an ISO8601 date string 

56 

57 If it is None or not a valid ISO8601 timestamp, 

58 it will be returned unmodified. 

59 Otherwise, it will return a datetime object. 

60 """ 

61 if s is None: 

62 return s 

63 m = ISO8601_PAT.match(s) 

64 if m: 

65 dt = _dateutil_parse(s) 

66 return _ensure_tzinfo(dt) 

67 return s 

68 

69 

70def extract_dates(obj): 

71 """extract ISO8601 dates from unpacked JSON""" 

72 if isinstance(obj, dict): 

73 new_obj = {} # don't clobber 

74 for k, v in obj.items(): 

75 new_obj[k] = extract_dates(v) 

76 obj = new_obj 

77 elif isinstance(obj, (list, tuple)): 

78 obj = [extract_dates(o) for o in obj] 

79 elif isinstance(obj, str): 

80 obj = parse_date(obj) 

81 return obj 

82 

83 

84def squash_dates(obj): 

85 """squash datetime objects into ISO8601 strings""" 

86 if isinstance(obj, dict): 

87 obj = dict(obj) # don't clobber 

88 for k, v in obj.items(): 

89 obj[k] = squash_dates(v) 

90 elif isinstance(obj, (list, tuple)): 

91 obj = [squash_dates(o) for o in obj] 

92 elif isinstance(obj, datetime): 

93 obj = obj.isoformat() 

94 return obj 

95 

96 

97def date_default(obj): 

98 """DEPRECATED: Use jupyter_client.jsonutil.json_default""" 

99 warnings.warn( 

100 "date_default is deprecated since jupyter_client 7.0.0." 

101 " Use jupyter_client.jsonutil.json_default.", 

102 stacklevel=2, 

103 ) 

104 return json_default(obj) 

105 

106 

107def json_default(obj): 

108 """default function for packing objects in JSON.""" 

109 if isinstance(obj, datetime): 

110 obj = _ensure_tzinfo(obj) 

111 return obj.isoformat().replace('+00:00', 'Z') 

112 

113 if isinstance(obj, bytes): 

114 return b2a_base64(obj, newline=False).decode('ascii') 

115 

116 if isinstance(obj, Iterable): 

117 return list(obj) 

118 

119 if isinstance(obj, numbers.Integral): 

120 return int(obj) 

121 

122 if isinstance(obj, numbers.Real): 

123 return float(obj) 

124 

125 raise TypeError("%r is not JSON serializable" % obj) 

126 

127 

128# Copy of the old ipykernel's json_clean 

129# This is temporary, it should be removed when we deprecate support for 

130# non-valid JSON messages 

131def json_clean(obj): 

132 # types that are 'atomic' and ok in json as-is. 

133 atomic_ok = (str, type(None)) 

134 

135 # containers that we need to convert into lists 

136 container_to_list = (tuple, set, types.GeneratorType) 

137 

138 # Since bools are a subtype of Integrals, which are a subtype of Reals, 

139 # we have to check them in that order. 

140 

141 if isinstance(obj, bool): 

142 return obj 

143 

144 if isinstance(obj, numbers.Integral): 

145 # cast int to int, in case subclasses override __str__ (e.g. boost enum, #4598) 

146 return int(obj) 

147 

148 if isinstance(obj, numbers.Real): 

149 # cast out-of-range floats to their reprs 

150 if math.isnan(obj) or math.isinf(obj): 

151 return repr(obj) 

152 return float(obj) 

153 

154 if isinstance(obj, atomic_ok): 

155 return obj 

156 

157 if isinstance(obj, bytes): 

158 # unanmbiguous binary data is base64-encoded 

159 # (this probably should have happened upstream) 

160 return b2a_base64(obj, newline=False).decode('ascii') 

161 

162 if isinstance(obj, container_to_list) or ( 

163 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name) 

164 ): 

165 obj = list(obj) 

166 

167 if isinstance(obj, list): 

168 return [json_clean(x) for x in obj] 

169 

170 if isinstance(obj, dict): 

171 # First, validate that the dict won't lose data in conversion due to 

172 # key collisions after stringification. This can happen with keys like 

173 # True and 'true' or 1 and '1', which collide in JSON. 

174 nkeys = len(obj) 

175 nkeys_collapsed = len(set(map(str, obj))) 

176 if nkeys != nkeys_collapsed: 

177 msg = ( 

178 'dict cannot be safely converted to JSON: ' 

179 'key collision would lead to dropped values' 

180 ) 

181 raise ValueError(msg) 

182 # If all OK, proceed by making the new dict that will be json-safe 

183 out = {} 

184 for k, v in obj.items(): 

185 out[str(k)] = json_clean(v) 

186 return out 

187 

188 if isinstance(obj, datetime): 

189 return obj.strftime(ISO8601) 

190 

191 # we don't understand it, it's probably an unserializable object 

192 raise ValueError("Can't clean for JSON: %r" % obj)