update wakatime cli to pre-release v4.1.1

This commit is contained in:
Alan Hamlett 2015-08-22 19:59:23 -07:00
parent 7e9aebaeb7
commit a9e199f61c
13 changed files with 221 additions and 95 deletions

View File

@ -22,7 +22,7 @@ import traceback
import socket import socket
try: try:
import ConfigParser as configparser import ConfigParser as configparser
except ImportError: except ImportError: # pragma: nocover
import configparser import configparser
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -33,15 +33,18 @@ from .compat import u, open, is_py3
from .logger import setup_logging from .logger import setup_logging
from .offlinequeue import Queue from .offlinequeue import Queue
from .packages import argparse from .packages import argparse
from .packages import simplejson as json
from .packages.requests.exceptions import RequestException from .packages.requests.exceptions import RequestException
from .project import get_project_info from .project import get_project_info
from .session_cache import SessionCache from .session_cache import SessionCache
from .stats import get_file_stats from .stats import get_file_stats
try: try:
from .packages import tzlocal from .packages import simplejson as json # pragma: nocover
except: except (ImportError, SyntaxError):
from .packages import tzlocal3 as tzlocal import json # pragma: nocover
try:
from .packages import tzlocal # pragma: nocover
except: # pragma: nocover
from .packages import tzlocal3 as tzlocal # pragma: nocover
log = logging.getLogger('WakaTime') log = logging.getLogger('WakaTime')
@ -54,45 +57,6 @@ class FileAction(argparse.Action):
setattr(namespace, self.dest, values) setattr(namespace, self.dest, values)
def upgradeConfigFile(configFile):
"""For backwards-compatibility, upgrade the existing config file
to work with configparser and rename from .wakatime.conf to .wakatime.cfg.
"""
if os.path.isfile(configFile):
# if upgraded cfg file already exists, don't overwrite it
return
oldConfig = os.path.join(os.path.expanduser('~'), '.wakatime.conf')
try:
configs = {
'ignore': [],
}
with open(oldConfig, 'r', encoding='utf-8') as fh:
for line in fh.readlines():
line = line.split('=', 1)
if len(line) == 2 and line[0].strip() and line[1].strip():
if line[0].strip() == 'ignore':
configs['ignore'].append(line[1].strip())
else:
configs[line[0].strip()] = line[1].strip()
with open(configFile, 'w', encoding='utf-8') as fh:
fh.write("[settings]\n")
for name, value in configs.items():
if isinstance(value, list):
fh.write("%s=\n" % name)
for item in value:
fh.write(" %s\n" % item)
else:
fh.write("%s = %s\n" % (name, value))
os.remove(oldConfig)
except IOError:
pass
def parseConfigFile(configFile=None): def parseConfigFile(configFile=None):
"""Returns a configparser.SafeConfigParser instance with configs """Returns a configparser.SafeConfigParser instance with configs
read from the config file. Default location of the config file is read from the config file. Default location of the config file is
@ -102,8 +66,6 @@ def parseConfigFile(configFile=None):
if not configFile: if not configFile:
configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg') configFile = os.path.join(os.path.expanduser('~'), '.wakatime.cfg')
upgradeConfigFile(configFile)
configs = configparser.SafeConfigParser() configs = configparser.SafeConfigParser()
try: try:
with open(configFile, 'r', encoding='utf-8') as fh: with open(configFile, 'r', encoding='utf-8') as fh:
@ -117,17 +79,12 @@ def parseConfigFile(configFile=None):
return configs return configs
def parseArguments(argv): def parseArguments():
"""Parse command line arguments and configs from ~/.wakatime.cfg. """Parse command line arguments and configs from ~/.wakatime.cfg.
Command line arguments take precedence over config file settings. Command line arguments take precedence over config file settings.
Returns instances of ArgumentParser and SafeConfigParser. Returns instances of ArgumentParser and SafeConfigParser.
""" """
try:
sys.argv
except AttributeError:
sys.argv = argv
# define supported command line arguments # define supported command line arguments
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Common interface for the WakaTime api.') description='Common interface for the WakaTime api.')
@ -189,7 +146,7 @@ def parseArguments(argv):
parser.add_argument('--version', action='version', version=__version__) parser.add_argument('--version', action='version', version=__version__)
# parse command line arguments # parse command line arguments
args = parser.parse_args(args=argv[1:]) args = parser.parse_args()
# use current unix epoch timestamp by default # use current unix epoch timestamp by default
if not args.timestamp: if not args.timestamp:
@ -267,7 +224,7 @@ def should_exclude(fileName, include, exclude):
msg=u(ex), msg=u(ex),
pattern=u(pattern), pattern=u(pattern),
)) ))
except TypeError: except TypeError: # pragma: nocover
pass pass
try: try:
for pattern in exclude: for pattern in exclude:
@ -280,7 +237,7 @@ def should_exclude(fileName, include, exclude):
msg=u(ex), msg=u(ex),
pattern=u(pattern), pattern=u(pattern),
)) ))
except TypeError: except TypeError: # pragma: nocover
pass pass
return False return False
@ -422,11 +379,10 @@ def send_heartbeat(project=None, branch=None, hostname=None, stats={}, key=None,
return False return False
def main(argv=None): def main(argv):
if not argv: sys.argv = ['wakatime'] + argv
argv = sys.argv
args, configs = parseArguments(argv) args, configs = parseArguments()
if configs is None: if configs is None:
return 103 # config file parsing error return 103 # config file parsing error

View File

@ -32,4 +32,4 @@ except TypeError:
if __name__ == '__main__': if __name__ == '__main__':
sys.exit(wakatime.main(sys.argv)) sys.exit(wakatime.main(sys.argv[1:]))

View File

@ -17,7 +17,7 @@ is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3) is_py3 = (sys.version_info[0] == 3)
if is_py2: if is_py2: # pragma: nocover
def u(text): def u(text):
try: try:
@ -31,7 +31,7 @@ if is_py2:
basestring = basestring basestring = basestring
elif is_py3: elif is_py3: # pragma: nocover
def u(text): def u(text):
if isinstance(text, bytes): if isinstance(text, bytes):
@ -40,9 +40,10 @@ elif is_py3:
open = open open = open
basestring = (str, bytes) basestring = (str, bytes)
try: try:
from importlib import import_module from importlib import import_module
except ImportError: except ImportError: # pragma: nocover
def _resolve_name(name, package, level): def _resolve_name(name, package, level):
"""Return the absolute name of the module to be imported.""" """Return the absolute name of the module to be imported."""
if not hasattr(package, 'rindex'): if not hasattr(package, 'rindex'):

View File

@ -13,12 +13,15 @@ import logging
import os import os
import sys import sys
from .packages import simplejson as json
from .compat import u from .compat import u
try: try:
from collections import OrderedDict from collections import OrderedDict # pragma: nocover
except ImportError: except ImportError:
from .packages.ordereddict import OrderedDict from .packages.ordereddict import OrderedDict # pragma: nocover
try:
from .packages import simplejson as json # pragma: nocover
except (ImportError, SyntaxError):
import json # pragma: nocover
class CustomEncoder(json.JSONEncoder): class CustomEncoder(json.JSONEncoder):

View File

@ -11,6 +11,7 @@ If you are packaging Requests, e.g., for a Linux distribution or a managed
environment, you can change the definition of where() to return a separately environment, you can change the definition of where() to return a separately
packaged CA bundle. packaged CA bundle.
""" """
import sys
import os.path import os.path
try: try:
@ -19,7 +20,13 @@ except ImportError:
def where(): def where():
"""Return the preferred certificate bundle.""" """Return the preferred certificate bundle."""
# vendored bundle inside Requests # vendored bundle inside Requests
return os.path.join(os.path.dirname(__file__), 'cacert.pem') is_py3 = (sys.version_info[0] == 3)
certdir = os.path.dirname(
__file__
if is_py3 else
__file__.decode(sys.getfilesystemencoding())
)
return os.path.join(certdir, 'cacert.pem')
if __name__ == '__main__': if __name__ == '__main__':
print(where()) print(where())

View File

@ -5,9 +5,8 @@ interchange format.
:mod:`simplejson` exposes an API familiar to users of the standard library :mod:`simplejson` exposes an API familiar to users of the standard library
:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained :mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
version of the :mod:`json` library contained in Python 2.6, but maintains version of the :mod:`json` library contained in Python 2.6, but maintains
compatibility with Python 2.4 and Python 2.5 and (currently) has compatibility back to Python 2.5 and (currently) has significant performance
significant performance advantages, even without using the optional C advantages, even without using the optional C extension for speedups.
extension for speedups.
Encoding basic Python object hierarchies:: Encoding basic Python object hierarchies::
@ -98,7 +97,7 @@ Using simplejson.tool from the shell to validate and pretty-print::
Expecting property name: line 1 column 3 (char 2) Expecting property name: line 1 column 3 (char 2)
""" """
from __future__ import absolute_import from __future__ import absolute_import
__version__ = '3.6.5' __version__ = '3.8.0'
__all__ = [ __all__ = [
'dump', 'dumps', 'load', 'loads', 'dump', 'dumps', 'load', 'loads',
'JSONDecoder', 'JSONDecodeError', 'JSONEncoder', 'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',
@ -140,6 +139,7 @@ _default_encoder = JSONEncoder(
use_decimal=True, use_decimal=True,
namedtuple_as_object=True, namedtuple_as_object=True,
tuple_as_array=True, tuple_as_array=True,
iterable_as_array=False,
bigint_as_string=False, bigint_as_string=False,
item_sort_key=None, item_sort_key=None,
for_json=False, for_json=False,
@ -152,7 +152,8 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
encoding='utf-8', default=None, use_decimal=True, encoding='utf-8', default=None, use_decimal=True,
namedtuple_as_object=True, tuple_as_array=True, namedtuple_as_object=True, tuple_as_array=True,
bigint_as_string=False, sort_keys=False, item_sort_key=None, bigint_as_string=False, sort_keys=False, item_sort_key=None,
for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw): for_json=False, ignore_nan=False, int_as_string_bitcount=None,
iterable_as_array=False, **kw):
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
``.write()``-supporting file-like object). ``.write()``-supporting file-like object).
@ -204,6 +205,10 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
If *tuple_as_array* is true (default: ``True``), If *tuple_as_array* is true (default: ``True``),
:class:`tuple` (and subclasses) will be encoded as JSON arrays. :class:`tuple` (and subclasses) will be encoded as JSON arrays.
If *iterable_as_array* is true (default: ``False``),
any object not in the above table that implements ``__iter__()``
will be encoded as a JSON array.
If *bigint_as_string* is true (default: ``False``), ints 2**53 and higher If *bigint_as_string* is true (default: ``False``), ints 2**53 and higher
or lower than -2**53 will be encoded as strings. This is to avoid the or lower than -2**53 will be encoded as strings. This is to avoid the
rounding that happens in Javascript otherwise. Note that this is still a rounding that happens in Javascript otherwise. Note that this is still a
@ -242,7 +247,7 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
check_circular and allow_nan and check_circular and allow_nan and
cls is None and indent is None and separators is None and cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and use_decimal encoding == 'utf-8' and default is None and use_decimal
and namedtuple_as_object and tuple_as_array and namedtuple_as_object and tuple_as_array and not iterable_as_array
and not bigint_as_string and not sort_keys and not bigint_as_string and not sort_keys
and not item_sort_key and not for_json and not item_sort_key and not for_json
and not ignore_nan and int_as_string_bitcount is None and not ignore_nan and int_as_string_bitcount is None
@ -258,6 +263,7 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
default=default, use_decimal=use_decimal, default=default, use_decimal=use_decimal,
namedtuple_as_object=namedtuple_as_object, namedtuple_as_object=namedtuple_as_object,
tuple_as_array=tuple_as_array, tuple_as_array=tuple_as_array,
iterable_as_array=iterable_as_array,
bigint_as_string=bigint_as_string, bigint_as_string=bigint_as_string,
sort_keys=sort_keys, sort_keys=sort_keys,
item_sort_key=item_sort_key, item_sort_key=item_sort_key,
@ -276,7 +282,8 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
encoding='utf-8', default=None, use_decimal=True, encoding='utf-8', default=None, use_decimal=True,
namedtuple_as_object=True, tuple_as_array=True, namedtuple_as_object=True, tuple_as_array=True,
bigint_as_string=False, sort_keys=False, item_sort_key=None, bigint_as_string=False, sort_keys=False, item_sort_key=None,
for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw): for_json=False, ignore_nan=False, int_as_string_bitcount=None,
iterable_as_array=False, **kw):
"""Serialize ``obj`` to a JSON formatted ``str``. """Serialize ``obj`` to a JSON formatted ``str``.
If ``skipkeys`` is false then ``dict`` keys that are not basic types If ``skipkeys`` is false then ``dict`` keys that are not basic types
@ -324,6 +331,10 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
If *tuple_as_array* is true (default: ``True``), If *tuple_as_array* is true (default: ``True``),
:class:`tuple` (and subclasses) will be encoded as JSON arrays. :class:`tuple` (and subclasses) will be encoded as JSON arrays.
If *iterable_as_array* is true (default: ``False``),
any object not in the above table that implements ``__iter__()``
will be encoded as a JSON array.
If *bigint_as_string* is true (not the default), ints 2**53 and higher If *bigint_as_string* is true (not the default), ints 2**53 and higher
or lower than -2**53 will be encoded as strings. This is to avoid the or lower than -2**53 will be encoded as strings. This is to avoid the
rounding that happens in Javascript otherwise. rounding that happens in Javascript otherwise.
@ -356,12 +367,11 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
""" """
# cached encoder # cached encoder
if ( if (not skipkeys and ensure_ascii and
not skipkeys and ensure_ascii and
check_circular and allow_nan and check_circular and allow_nan and
cls is None and indent is None and separators is None and cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and use_decimal encoding == 'utf-8' and default is None and use_decimal
and namedtuple_as_object and tuple_as_array and namedtuple_as_object and tuple_as_array and not iterable_as_array
and not bigint_as_string and not sort_keys and not bigint_as_string and not sort_keys
and not item_sort_key and not for_json and not item_sort_key and not for_json
and not ignore_nan and int_as_string_bitcount is None and not ignore_nan and int_as_string_bitcount is None
@ -377,6 +387,7 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
use_decimal=use_decimal, use_decimal=use_decimal,
namedtuple_as_object=namedtuple_as_object, namedtuple_as_object=namedtuple_as_object,
tuple_as_array=tuple_as_array, tuple_as_array=tuple_as_array,
iterable_as_array=iterable_as_array,
bigint_as_string=bigint_as_string, bigint_as_string=bigint_as_string,
sort_keys=sort_keys, sort_keys=sort_keys,
item_sort_key=item_sort_key, item_sort_key=item_sort_key,

View File

@ -10,6 +10,7 @@
#define PyString_AS_STRING PyBytes_AS_STRING #define PyString_AS_STRING PyBytes_AS_STRING
#define PyString_FromStringAndSize PyBytes_FromStringAndSize #define PyString_FromStringAndSize PyBytes_FromStringAndSize
#define PyInt_Check(obj) 0 #define PyInt_Check(obj) 0
#define PyInt_CheckExact(obj) 0
#define JSON_UNICHR Py_UCS4 #define JSON_UNICHR Py_UCS4
#define JSON_InternFromString PyUnicode_InternFromString #define JSON_InternFromString PyUnicode_InternFromString
#define JSON_Intern_GET_SIZE PyUnicode_GET_SIZE #define JSON_Intern_GET_SIZE PyUnicode_GET_SIZE
@ -168,6 +169,7 @@ typedef struct _PyEncoderObject {
int use_decimal; int use_decimal;
int namedtuple_as_object; int namedtuple_as_object;
int tuple_as_array; int tuple_as_array;
int iterable_as_array;
PyObject *max_long_size; PyObject *max_long_size;
PyObject *min_long_size; PyObject *min_long_size;
PyObject *item_sort_key; PyObject *item_sort_key;
@ -660,7 +662,20 @@ encoder_stringify_key(PyEncoderObject *s, PyObject *key)
return _encoded_const(key); return _encoded_const(key);
} }
else if (PyInt_Check(key) || PyLong_Check(key)) { else if (PyInt_Check(key) || PyLong_Check(key)) {
return PyObject_Str(key); if (!(PyInt_CheckExact(key) || PyLong_CheckExact(key))) {
/* See #118, do not trust custom str/repr */
PyObject *res;
PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyLong_Type, key, NULL);
if (tmp == NULL) {
return NULL;
}
res = PyObject_Str(tmp);
Py_DECREF(tmp);
return res;
}
else {
return PyObject_Str(key);
}
} }
else if (s->use_decimal && PyObject_TypeCheck(key, (PyTypeObject *)s->Decimal)) { else if (s->use_decimal && PyObject_TypeCheck(key, (PyTypeObject *)s->Decimal)) {
return PyObject_Str(key); return PyObject_Str(key);
@ -2567,7 +2582,6 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
static int static int
encoder_init(PyObject *self, PyObject *args, PyObject *kwds) encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
{ {
/* initialize Encoder object */
static char *kwlist[] = { static char *kwlist[] = {
"markers", "markers",
"default", "default",
@ -2582,30 +2596,32 @@ encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
"use_decimal", "use_decimal",
"namedtuple_as_object", "namedtuple_as_object",
"tuple_as_array", "tuple_as_array",
"iterable_as_array"
"int_as_string_bitcount", "int_as_string_bitcount",
"item_sort_key", "item_sort_key",
"encoding", "encoding",
"for_json", "for_json",
"ignore_nan", "ignore_nan",
"Decimal", "Decimal",
"iterable_as_array",
NULL}; NULL};
PyEncoderObject *s; PyEncoderObject *s;
PyObject *markers, *defaultfn, *encoder, *indent, *key_separator; PyObject *markers, *defaultfn, *encoder, *indent, *key_separator;
PyObject *item_separator, *sort_keys, *skipkeys, *allow_nan, *key_memo; PyObject *item_separator, *sort_keys, *skipkeys, *allow_nan, *key_memo;
PyObject *use_decimal, *namedtuple_as_object, *tuple_as_array; PyObject *use_decimal, *namedtuple_as_object, *tuple_as_array, *iterable_as_array;
PyObject *int_as_string_bitcount, *item_sort_key, *encoding, *for_json; PyObject *int_as_string_bitcount, *item_sort_key, *encoding, *for_json;
PyObject *ignore_nan, *Decimal; PyObject *ignore_nan, *Decimal;
assert(PyEncoder_Check(self)); assert(PyEncoder_Check(self));
s = (PyEncoderObject *)self; s = (PyEncoderObject *)self;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOOOOOOOOOOOO:make_encoder", kwlist, if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOOOOOOOOOOOOO:make_encoder", kwlist,
&markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator, &markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator,
&sort_keys, &skipkeys, &allow_nan, &key_memo, &use_decimal, &sort_keys, &skipkeys, &allow_nan, &key_memo, &use_decimal,
&namedtuple_as_object, &tuple_as_array, &namedtuple_as_object, &tuple_as_array,
&int_as_string_bitcount, &item_sort_key, &encoding, &for_json, &int_as_string_bitcount, &item_sort_key, &encoding, &for_json,
&ignore_nan, &Decimal)) &ignore_nan, &Decimal, &iterable_as_array))
return -1; return -1;
Py_INCREF(markers); Py_INCREF(markers);
@ -2635,9 +2651,10 @@ encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
s->use_decimal = PyObject_IsTrue(use_decimal); s->use_decimal = PyObject_IsTrue(use_decimal);
s->namedtuple_as_object = PyObject_IsTrue(namedtuple_as_object); s->namedtuple_as_object = PyObject_IsTrue(namedtuple_as_object);
s->tuple_as_array = PyObject_IsTrue(tuple_as_array); s->tuple_as_array = PyObject_IsTrue(tuple_as_array);
s->iterable_as_array = PyObject_IsTrue(iterable_as_array);
if (PyInt_Check(int_as_string_bitcount) || PyLong_Check(int_as_string_bitcount)) { if (PyInt_Check(int_as_string_bitcount) || PyLong_Check(int_as_string_bitcount)) {
static const unsigned int long_long_bitsize = SIZEOF_LONG_LONG * 8; static const unsigned int long_long_bitsize = SIZEOF_LONG_LONG * 8;
int int_as_string_bitcount_val = PyLong_AsLong(int_as_string_bitcount); int int_as_string_bitcount_val = (int)PyLong_AsLong(int_as_string_bitcount);
if (int_as_string_bitcount_val > 0 && int_as_string_bitcount_val < long_long_bitsize) { if (int_as_string_bitcount_val > 0 && int_as_string_bitcount_val < long_long_bitsize) {
s->max_long_size = PyLong_FromUnsignedLongLong(1ULL << int_as_string_bitcount_val); s->max_long_size = PyLong_FromUnsignedLongLong(1ULL << int_as_string_bitcount_val);
s->min_long_size = PyLong_FromLongLong(-1LL << int_as_string_bitcount_val); s->min_long_size = PyLong_FromLongLong(-1LL << int_as_string_bitcount_val);
@ -2800,7 +2817,20 @@ encoder_encode_float(PyEncoderObject *s, PyObject *obj)
} }
} }
/* Use a better float format here? */ /* Use a better float format here? */
return PyObject_Repr(obj); if (PyFloat_CheckExact(obj)) {
return PyObject_Repr(obj);
}
else {
/* See #118, do not trust custom str/repr */
PyObject *res;
PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyFloat_Type, obj, NULL);
if (tmp == NULL) {
return NULL;
}
res = PyObject_Repr(tmp);
Py_DECREF(tmp);
return res;
}
} }
static PyObject * static PyObject *
@ -2840,7 +2870,21 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
rv = _steal_accumulate(rval, encoded); rv = _steal_accumulate(rval, encoded);
} }
else if (PyInt_Check(obj) || PyLong_Check(obj)) { else if (PyInt_Check(obj) || PyLong_Check(obj)) {
PyObject *encoded = PyObject_Str(obj); PyObject *encoded;
if (PyInt_CheckExact(obj) || PyLong_CheckExact(obj)) {
encoded = PyObject_Str(obj);
}
else {
/* See #118, do not trust custom str/repr */
PyObject *tmp = PyObject_CallFunctionObjArgs((PyObject *)&PyLong_Type, obj, NULL);
if (tmp == NULL) {
encoded = NULL;
}
else {
encoded = PyObject_Str(tmp);
Py_DECREF(tmp);
}
}
if (encoded != NULL) { if (encoded != NULL) {
encoded = maybe_quote_bigint(s, encoded, obj); encoded = maybe_quote_bigint(s, encoded, obj);
if (encoded == NULL) if (encoded == NULL)
@ -2895,6 +2939,16 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
else { else {
PyObject *ident = NULL; PyObject *ident = NULL;
PyObject *newobj; PyObject *newobj;
if (s->iterable_as_array) {
newobj = PyObject_GetIter(obj);
if (newobj == NULL)
PyErr_Clear();
else {
rv = encoder_listencode_list(s, rval, newobj, indent_level);
Py_DECREF(newobj);
break;
}
}
if (s->markers != Py_None) { if (s->markers != Py_None) {
int has_key; int has_key;
ident = PyLong_FromVoidPtr(obj); ident = PyLong_FromVoidPtr(obj);

View File

@ -3,7 +3,8 @@
from __future__ import absolute_import from __future__ import absolute_import
import re import re
from operator import itemgetter from operator import itemgetter
from decimal import Decimal # Do not import Decimal directly to avoid reload issues
import decimal
from .compat import u, unichr, binary_type, string_types, integer_types, PY3 from .compat import u, unichr, binary_type, string_types, integer_types, PY3
def _import_speedups(): def _import_speedups():
try: try:
@ -123,7 +124,7 @@ class JSONEncoder(object):
use_decimal=True, namedtuple_as_object=True, use_decimal=True, namedtuple_as_object=True,
tuple_as_array=True, bigint_as_string=False, tuple_as_array=True, bigint_as_string=False,
item_sort_key=None, for_json=False, ignore_nan=False, item_sort_key=None, for_json=False, ignore_nan=False,
int_as_string_bitcount=None): int_as_string_bitcount=None, iterable_as_array=False):
"""Constructor for JSONEncoder, with sensible defaults. """Constructor for JSONEncoder, with sensible defaults.
If skipkeys is false, then it is a TypeError to attempt If skipkeys is false, then it is a TypeError to attempt
@ -178,6 +179,10 @@ class JSONEncoder(object):
If tuple_as_array is true (the default), tuple (and subclasses) will If tuple_as_array is true (the default), tuple (and subclasses) will
be encoded as JSON arrays. be encoded as JSON arrays.
If *iterable_as_array* is true (default: ``False``),
any object not in the above table that implements ``__iter__()``
will be encoded as a JSON array.
If bigint_as_string is true (not the default), ints 2**53 and higher If bigint_as_string is true (not the default), ints 2**53 and higher
or lower than -2**53 will be encoded as strings. This is to avoid the or lower than -2**53 will be encoded as strings. This is to avoid the
rounding that happens in Javascript otherwise. rounding that happens in Javascript otherwise.
@ -209,6 +214,7 @@ class JSONEncoder(object):
self.use_decimal = use_decimal self.use_decimal = use_decimal
self.namedtuple_as_object = namedtuple_as_object self.namedtuple_as_object = namedtuple_as_object
self.tuple_as_array = tuple_as_array self.tuple_as_array = tuple_as_array
self.iterable_as_array = iterable_as_array
self.bigint_as_string = bigint_as_string self.bigint_as_string = bigint_as_string
self.item_sort_key = item_sort_key self.item_sort_key = item_sort_key
self.for_json = for_json self.for_json = for_json
@ -311,6 +317,9 @@ class JSONEncoder(object):
elif o == _neginf: elif o == _neginf:
text = '-Infinity' text = '-Infinity'
else: else:
if type(o) != float:
# See #118, do not trust custom str/repr
o = float(o)
return _repr(o) return _repr(o)
if ignore_nan: if ignore_nan:
@ -334,7 +343,7 @@ class JSONEncoder(object):
self.namedtuple_as_object, self.tuple_as_array, self.namedtuple_as_object, self.tuple_as_array,
int_as_string_bitcount, int_as_string_bitcount,
self.item_sort_key, self.encoding, self.for_json, self.item_sort_key, self.encoding, self.for_json,
self.ignore_nan, Decimal) self.ignore_nan, decimal.Decimal, self.iterable_as_array)
else: else:
_iterencode = _make_iterencode( _iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr, markers, self.default, _encoder, self.indent, floatstr,
@ -343,7 +352,7 @@ class JSONEncoder(object):
self.namedtuple_as_object, self.tuple_as_array, self.namedtuple_as_object, self.tuple_as_array,
int_as_string_bitcount, int_as_string_bitcount,
self.item_sort_key, self.encoding, self.for_json, self.item_sort_key, self.encoding, self.for_json,
Decimal=Decimal) self.iterable_as_array, Decimal=decimal.Decimal)
try: try:
return _iterencode(o, 0) return _iterencode(o, 0)
finally: finally:
@ -382,11 +391,12 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
_use_decimal, _namedtuple_as_object, _tuple_as_array, _use_decimal, _namedtuple_as_object, _tuple_as_array,
_int_as_string_bitcount, _item_sort_key, _int_as_string_bitcount, _item_sort_key,
_encoding,_for_json, _encoding,_for_json,
_iterable_as_array,
## HACK: hand-optimized bytecode; turn globals into locals ## HACK: hand-optimized bytecode; turn globals into locals
_PY3=PY3, _PY3=PY3,
ValueError=ValueError, ValueError=ValueError,
string_types=string_types, string_types=string_types,
Decimal=Decimal, Decimal=None,
dict=dict, dict=dict,
float=float, float=float,
id=id, id=id,
@ -395,7 +405,10 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
list=list, list=list,
str=str, str=str,
tuple=tuple, tuple=tuple,
iter=iter,
): ):
if _use_decimal and Decimal is None:
Decimal = decimal.Decimal
if _item_sort_key and not callable(_item_sort_key): if _item_sort_key and not callable(_item_sort_key):
raise TypeError("item_sort_key must be None or callable") raise TypeError("item_sort_key must be None or callable")
elif _sort_keys and not _item_sort_key: elif _sort_keys and not _item_sort_key:
@ -412,6 +425,9 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
or or
_int_as_string_bitcount < 1 _int_as_string_bitcount < 1
) )
if type(value) not in integer_types:
# See #118, do not trust custom str/repr
value = int(value)
if ( if (
skip_quoting or skip_quoting or
(-1 << _int_as_string_bitcount) (-1 << _int_as_string_bitcount)
@ -501,6 +517,9 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
elif key is None: elif key is None:
key = 'null' key = 'null'
elif isinstance(key, integer_types): elif isinstance(key, integer_types):
if type(key) not in integer_types:
# See #118, do not trust custom str/repr
key = int(key)
key = str(key) key = str(key)
elif _use_decimal and isinstance(key, Decimal): elif _use_decimal and isinstance(key, Decimal):
key = str(key) key = str(key)
@ -634,6 +653,16 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
elif _use_decimal and isinstance(o, Decimal): elif _use_decimal and isinstance(o, Decimal):
yield str(o) yield str(o)
else: else:
while _iterable_as_array:
# Markers are not checked here because it is valid for
# an iterable to return self.
try:
o = iter(o)
except TypeError:
break
for chunk in _iterencode_list(o, _current_indent_level):
yield chunk
return
if markers is not None: if markers is not None:
markerid = id(o) markerid = id(o)
if markerid in markers: if markerid in markers:

View File

@ -62,6 +62,7 @@ def all_tests_suite():
'simplejson.tests.test_namedtuple', 'simplejson.tests.test_namedtuple',
'simplejson.tests.test_tool', 'simplejson.tests.test_tool',
'simplejson.tests.test_for_json', 'simplejson.tests.test_for_json',
'simplejson.tests.test_subclass',
])) ]))
suite = get_suite() suite = get_suite()
import simplejson import simplejson

View File

@ -0,0 +1,31 @@
import unittest
from StringIO import StringIO
import simplejson as json
def iter_dumps(obj, **kw):
return ''.join(json.JSONEncoder(**kw).iterencode(obj))
def sio_dump(obj, **kw):
sio = StringIO()
json.dumps(obj, **kw)
return sio.getvalue()
class TestIterable(unittest.TestCase):
def test_iterable(self):
l = [1, 2, 3]
for dumps in (json.dumps, iter_dumps, sio_dump):
expect = dumps(l)
default_expect = dumps(sum(l))
# Default is False
self.assertRaises(TypeError, dumps, iter(l))
self.assertRaises(TypeError, dumps, iter(l), iterable_as_array=False)
self.assertEqual(expect, dumps(iter(l), iterable_as_array=True))
# Ensure that the "default" gets called
self.assertEqual(default_expect, dumps(iter(l), default=sum))
self.assertEqual(default_expect, dumps(iter(l), iterable_as_array=False, default=sum))
# Ensure that the "default" does not get called
self.assertEqual(
default_expect,
dumps(iter(l), iterable_as_array=True, default=sum))

View File

@ -0,0 +1,37 @@
from unittest import TestCase
import simplejson as json
from decimal import Decimal
class AlternateInt(int):
def __repr__(self):
return 'invalid json'
__str__ = __repr__
class AlternateFloat(float):
def __repr__(self):
return 'invalid json'
__str__ = __repr__
# class AlternateDecimal(Decimal):
# def __repr__(self):
# return 'invalid json'
class TestSubclass(TestCase):
def test_int(self):
self.assertEqual(json.dumps(AlternateInt(1)), '1')
self.assertEqual(json.dumps(AlternateInt(-1)), '-1')
self.assertEqual(json.loads(json.dumps({AlternateInt(1): 1})), {'1': 1})
def test_float(self):
self.assertEqual(json.dumps(AlternateFloat(1.0)), '1.0')
self.assertEqual(json.dumps(AlternateFloat(-1.0)), '-1.0')
self.assertEqual(json.loads(json.dumps({AlternateFloat(1.0): 1})), {'1.0': 1})
# NOTE: Decimal subclasses are not supported as-is
# def test_decimal(self):
# self.assertEqual(json.dumps(AlternateDecimal('1.0')), '1.0')
# self.assertEqual(json.dumps(AlternateDecimal('-1.0')), '-1.0')

View File

@ -45,7 +45,3 @@ class TestTuples(unittest.TestCase):
self.assertEqual( self.assertEqual(
json.dumps(repr(t)), json.dumps(repr(t)),
sio.getvalue()) sio.getvalue())
class TestNamedTuple(unittest.TestCase):
def test_namedtuple_dump(self):
pass

View File

@ -52,7 +52,7 @@ class SessionCache(object):
conn, c = self.connect() conn, c = self.connect()
c.execute('DELETE FROM session') c.execute('DELETE FROM session')
values = { values = {
'value': pickle.dumps(session), 'value': pickle.dumps(session, protocol=2),
} }
c.execute('INSERT INTO session VALUES (:value)', values) c.execute('INSERT INTO session VALUES (:value)', values)
conn.commit() conn.commit()