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
« 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
14from dateutil.parser import parse as _dateutil_parse
15from dateutil.tz import tzlocal
17next_attr_name = "__next__" # Not sure what downstream library uses this, but left it to be safe
19# -----------------------------------------------------------------------------
20# Globals and constants
21# -----------------------------------------------------------------------------
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)
29# holy crap, strptime is not threadsafe.
30# Calling it once at import seems to help.
31datetime.strptime("1", "%d") # noqa
33# -----------------------------------------------------------------------------
34# Classes and functions
35# -----------------------------------------------------------------------------
38def _ensure_tzinfo(dt: datetime) -> datetime:
39 """Ensure a datetime object has tzinfo
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
54def parse_date(s: Optional[str]) -> Optional[Union[str, datetime]]:
55 """parse an ISO8601 date string
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
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
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
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)
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')
113 if isinstance(obj, bytes):
114 return b2a_base64(obj, newline=False).decode('ascii')
116 if isinstance(obj, Iterable):
117 return list(obj)
119 if isinstance(obj, numbers.Integral):
120 return int(obj)
122 if isinstance(obj, numbers.Real):
123 return float(obj)
125 raise TypeError("%r is not JSON serializable" % obj)
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))
135 # containers that we need to convert into lists
136 container_to_list = (tuple, set, types.GeneratorType)
138 # Since bools are a subtype of Integrals, which are a subtype of Reals,
139 # we have to check them in that order.
141 if isinstance(obj, bool):
142 return obj
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)
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)
154 if isinstance(obj, atomic_ok):
155 return obj
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')
162 if isinstance(obj, container_to_list) or (
163 hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)
164 ):
165 obj = list(obj)
167 if isinstance(obj, list):
168 return [json_clean(x) for x in obj]
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
188 if isinstance(obj, datetime):
189 return obj.strftime(ISO8601)
191 # we don't understand it, it's probably an unserializable object
192 raise ValueError("Can't clean for JSON: %r" % obj)