1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
# -*- coding: utf-8 -*-
import collections
import inspect
import json
import sys
__version__ = '1.4'
SUPPORTED_FUNCS = ['loads', 'load', 'dumps', 'dump']
json_module = collections.namedtuple('json_module', ' '.join(SUPPORTED_FUNCS))
try:
import ujson # https://github.com/esnme/ultrajson
except ImportError:
ujson = False
try:
import rapidjson # https://github.com/python-rapidjson/python-rapidjson
except ImportError:
rapidjson = False
try:
import simplejson # https://github.com/simplejson/simplejson
simplejson_slow = False
if not simplejson.encoder.c_make_encoder:
simplejson, simplejson_slow = simplejson_slow, simplejson
except ImportError:
simplejson = simplejson_slow = False
try:
import nssjson # https://github.com/lelit/nssjson
nssjson_slow = False
if not nssjson.encoder.c_make_encoder:
nssjson, nssjson_slow = nssjson_slow, nssjson
except ImportError:
nssjson = nssjson_slow = False
try:
import yajl # https://github.com/rtyler/py-yajl
except ImportError:
yajl = False
try:
import cjson # https://github.com/AGProjects/python-cjson
cjson = json_module(
dump=None, dumps=cjson.encode, load=None, loads=cjson.decode)
except ImportError:
cjson = None
try:
import metamagic.json as mjson # https://github.com/sprymix/metamagic.json
except ImportError:
mjson = False
try:
import orjson # https://github.com/ijl/orjson
except ImportError:
orjson = False
try:
pypy = sys.pypy_version_info is not None
except AttributeError:
pypy = False
try:
assert basestring is not None
except NameError:
basestring = str
if pypy:
# NOTE(mattgiles): in benchmarks, using pypy, the standard library's json
# module outperforms all third party libraries.
DEFAULT_RANKINGS = {
'dump': [],
'dumps': [],
'load': [],
'loads': []}
elif sys.version_info.major == 3:
DEFAULT_RANKINGS = {
'dump': [rapidjson, ujson, yajl, json, nssjson, simplejson, nssjson_slow, simplejson_slow],
'dumps': [orjson, mjson, rapidjson, ujson, yajl, json, nssjson, simplejson, nssjson_slow, simplejson_slow],
'load': [ujson, simplejson, rapidjson, json, nssjson, yajl, nssjson_slow, simplejson_slow],
'loads': [orjson, ujson, simplejson, rapidjson, json, nssjson, yajl, nssjson_slow, simplejson_slow]}
else:
DEFAULT_RANKINGS = {
'dump': [ujson, yajl, json, nssjson, simplejson, nssjson_slow, simplejson_slow],
'dumps': [ujson, yajl, json, cjson, nssjson, simplejson, nssjson_slow, simplejson_slow],
'load': [ujson, simplejson, nssjson, yajl, json, simplejson_slow, nssjson_slow],
'loads': [cjson, ujson, simplejson, nssjson, yajl, json, simplejson_slow, nssjson_slow]}
def _get_kwarg_names(func):
try:
# NOTE(mattgiles): there are compatibility issues between Python2 and
# Python3 when using `inspect.getargspec`. Here we elect to try
# Python27-compliant code first, falling back to Python36. This order
# is important because of the behavior of intermediate Python versions.
return inspect.getargspec(func)[0]
except (AttributeError, TypeError, ValueError):
return inspect.getfullargspec(func).kwonlyargs
def _best_available_json_func(func_name, ranking, **kwargs):
_available = []
for module in ranking:
if isinstance(module, basestring):
if module not in globals():
try:
globals()[module] = __import__(module)
except ModuleNotFoundError:
globals()[module] = False
if globals()[module]:
_available.append(globals()[module])
else:
if module:
_available.append(module)
if not kwargs:
return getattr(_available[0], func_name)
for module in _available:
try:
func = getattr(module, func_name)
if set(kwargs.keys()).issubset(_get_kwarg_names(func)):
return func
except (AttributeError, TypeError, ValueError):
continue
raise TypeError('mujson {}() got an unexpected kwarg'.format(func_name))
def mujson_function(name, alias_for=None, ranking=None):
"""Return the "best" available version of some JSON function.
The true performance ranking of different JSON libraries varies widely
based on the actual JSON data being encoded or decoded. Therefore, it may
be desirable to pass your own `ranking` based on your knowledge of the
common shape or characteristics of the JSON data relevant to your project.
Args:
name (str): the global name of your mujson function. Must have the same
string value as the variable to which you assign the output of
`mujson_function()`. mujson will infer the underlying json function
from the name if that function is in the name.
alias_for (str): the json function for which your mujson function is
an alias. Must be one of ["load", "loads", "dump", "dumps"].
ranking (list): if a list, will use ranking when evaluating
the best module available. If not passed, the default rankings will
be used.
NOTE(mattgiles): the returned function behaves differently the first time
it is invoked, compared to subsequent times. On first invocation, before
any output is returned, the "best" implementation of the desired json
function that is available for import is retrieved and made to replace the
temporary function returned by `mujson_function` in the global namespace.
The reason for this hackery is because, given the implicit desire for
speed, extra function calls are are needlessly slow.
"""
if alias_for is None:
for func in SUPPORTED_FUNCS:
if name.find(func) >= 0:
alias_for = func
break
if alias_for is None:
raise ValueError(
'mujson_function requires that either `name` contains a substring '
'in {} or that `alias_for` is specified.'.format(SUPPORTED_FUNCS))
if alias_for not in SUPPORTED_FUNCS:
raise ValueError(
'`alias_for` must be one of: {}'.format(SUPPORTED_FUNCS))
if ranking is None:
ranking = DEFAULT_RANKINGS[alias_for]
if 'json' not in ranking:
ranking.append('json')
def temp_json_func(*args, **kwargs):
func = _best_available_json_func(alias_for, ranking, **kwargs)
globals()[name] = func
return func(*args, **kwargs)
return temp_json_func
dump = mujson_function('dump')
dumps = mujson_function('dumps')
load = mujson_function('load')
loads = mujson_function('loads')
# NOTE(mattgiles): programmers can elect to explicitly import `compliant_*`
# versions of the standard functions to avoid run time errors that depend on
# what concrete uses of e.g. `mujson.dumps` hit `mujson_function:temp_json_func`
# first. Although `mujson_function` guarantees that the first time it is called
# it protects against choosing a JSON library which does not support invoked
# kwargs, this dynamic behavior can lead to NON-DETERMINISTIC behavior in
# larger or more complex libraries where mujson is used multiple places with
# varying signatures.
NON_COMPLIANT = [ujson, cjson, mjson, orjson]
compliant_dump = mujson_function(
'compliant_dump',
ranking=[m for m in DEFAULT_RANKINGS['dump'] if m not in NON_COMPLIANT])
compliant_dumps = mujson_function(
'compliant_dumps',
ranking=[m for m in DEFAULT_RANKINGS['dumps'] if m not in NON_COMPLIANT])
compliant_load = mujson_function(
'compliant_load',
ranking=[m for m in DEFAULT_RANKINGS['load'] if m not in NON_COMPLIANT])
compliant_loads = mujson_function(
'compliant_loads',
ranking=[m for m in DEFAULT_RANKINGS['loads'] if m not in NON_COMPLIANT])
|