[Python-checkins] cpython: Close #18264: int- and float-derived enums now converted to int or float.

ethan.furman python-checkins at python.org
Sat Aug 10 22:01:55 CEST 2013


http://hg.python.org/cpython/rev/ae1a7c420f08
changeset:   85103:ae1a7c420f08
user:        Ethan Furman <ethan at stoneleaf.us>
date:        Sat Aug 10 13:01:45 2013 -0700
summary:
  Close #18264: int- and float-derived enums now converted to int or float.

files:
  Doc/library/json.rst            |  37 +++++----
  Lib/json/encoder.py             |  27 ++++--
  Lib/test/test_json/test_enum.py |  81 +++++++++++++++++++++
  Modules/_json.c                 |  65 +++++++++++++++-
  4 files changed, 178 insertions(+), 32 deletions(-)


diff --git a/Doc/library/json.rst b/Doc/library/json.rst
--- a/Doc/library/json.rst
+++ b/Doc/library/json.rst
@@ -349,23 +349,26 @@
 
    .. _py-to-json-table:
 
-   +-------------------+---------------+
-   | Python            | JSON          |
-   +===================+===============+
-   | dict              | object        |
-   +-------------------+---------------+
-   | list, tuple       | array         |
-   +-------------------+---------------+
-   | str               | string        |
-   +-------------------+---------------+
-   | int, float        | number        |
-   +-------------------+---------------+
-   | True              | true          |
-   +-------------------+---------------+
-   | False             | false         |
-   +-------------------+---------------+
-   | None              | null          |
-   +-------------------+---------------+
+   +----------------------------------------+---------------+
+   | Python                                 | JSON          |
+   +========================================+===============+
+   | dict                                   | object        |
+   +----------------------------------------+---------------+
+   | list, tuple                            | array         |
+   +----------------------------------------+---------------+
+   | str                                    | string        |
+   +----------------------------------------+---------------+
+   | int, float, int- & float-derived Enums | number        |
+   +----------------------------------------+---------------+
+   | True                                   | true          |
+   +----------------------------------------+---------------+
+   | False                                  | false         |
+   +----------------------------------------+---------------+
+   | None                                   | null          |
+   +----------------------------------------+---------------+
+
+   .. versionchanged:: 3.4
+      Added support for int- and float-derived Enum classes.
 
    To extend this to recognize other objects, subclass and implement a
    :meth:`default` method with another method that returns a serializable object
diff --git a/Lib/json/encoder.py b/Lib/json/encoder.py
--- a/Lib/json/encoder.py
+++ b/Lib/json/encoder.py
@@ -175,6 +175,7 @@
     def encode(self, o):
         """Return a JSON string representation of a Python data structure.
 
+        >>> from json.encoder import JSONEncoder
         >>> JSONEncoder().encode({"foo": ["bar", "baz"]})
         '{"foo": ["bar", "baz"]}'
 
@@ -298,9 +299,13 @@
             elif value is False:
                 yield buf + 'false'
             elif isinstance(value, int):
-                yield buf + str(value)
+                # Subclasses of int/float may override __str__, but we still
+                # want to encode them as integers/floats in JSON. One example
+                # within the standard library is IntEnum.
+                yield buf + str(int(value))
             elif isinstance(value, float):
-                yield buf + _floatstr(value)
+                # see comment above for int
+                yield buf + _floatstr(float(value))
             else:
                 yield buf
                 if isinstance(value, (list, tuple)):
@@ -346,7 +351,8 @@
             # JavaScript is weakly typed for these, so it makes sense to
             # also allow them.  Many encoders seem to do something like this.
             elif isinstance(key, float):
-                key = _floatstr(key)
+                # see comment for int/float in _make_iterencode
+                key = _floatstr(float(key))
             elif key is True:
                 key = 'true'
             elif key is False:
@@ -354,7 +360,8 @@
             elif key is None:
                 key = 'null'
             elif isinstance(key, int):
-                key = str(key)
+                # see comment for int/float in _make_iterencode
+                key = str(int(key))
             elif _skipkeys:
                 continue
             else:
@@ -374,9 +381,11 @@
             elif value is False:
                 yield 'false'
             elif isinstance(value, int):
-                yield str(value)
+                # see comment for int/float in _make_iterencode
+                yield str(int(value))
             elif isinstance(value, float):
-                yield _floatstr(value)
+                # see comment for int/float in _make_iterencode
+                yield _floatstr(float(value))
             else:
                 if isinstance(value, (list, tuple)):
                     chunks = _iterencode_list(value, _current_indent_level)
@@ -402,9 +411,11 @@
         elif o is False:
             yield 'false'
         elif isinstance(o, int):
-            yield str(o)
+            # see comment for int/float in _make_iterencode
+            yield str(int(o))
         elif isinstance(o, float):
-            yield _floatstr(o)
+            # see comment for int/float in _make_iterencode
+            yield _floatstr(float(o))
         elif isinstance(o, (list, tuple)):
             yield from _iterencode_list(o, _current_indent_level)
         elif isinstance(o, dict):
diff --git a/Lib/test/test_json/test_enum.py b/Lib/test/test_json/test_enum.py
new file mode 100644
--- /dev/null
+++ b/Lib/test/test_json/test_enum.py
@@ -0,0 +1,81 @@
+from enum import Enum, IntEnum
+from test.test_json import PyTest, CTest
+
+SMALL = 1
+BIG = 1<<32
+HUGE = 1<<64
+REALLY_HUGE = 1<<96
+
+class BigNum(IntEnum):
+    small = SMALL
+    big = BIG
+    huge = HUGE
+    really_huge = REALLY_HUGE
+
+E = 2.718281
+PI = 3.141593
+TAU = 2 * PI
+
+class FloatNum(float, Enum):
+    e = E
+    pi = PI
+    tau = TAU
+
+class TestEnum:
+
+    def test_floats(self):
+        for enum in FloatNum:
+            self.assertEqual(self.dumps(enum), repr(enum.value))
+            self.assertEqual(float(self.dumps(enum)), enum)
+            self.assertEqual(self.loads(self.dumps(enum)), enum)
+
+    def test_ints(self):
+        for enum in BigNum:
+            self.assertEqual(self.dumps(enum), str(enum.value))
+            self.assertEqual(int(self.dumps(enum)), enum)
+            self.assertEqual(self.loads(self.dumps(enum)), enum)
+
+    def test_list(self):
+        self.assertEqual(
+                self.dumps(list(BigNum)),
+                str([SMALL, BIG, HUGE, REALLY_HUGE]),
+                )
+        self.assertEqual(self.dumps(list(FloatNum)), str([E, PI, TAU]))
+
+    def test_dict_keys(self):
+        s, b, h, r = BigNum
+        e, p, t = FloatNum
+        d = {
+            s:'tiny', b:'large', h:'larger', r:'largest',
+            e:"Euler's number", p:'pi', t:'tau',
+            }
+        nd = self.loads(self.dumps(d))
+        self.assertEqual(nd[str(SMALL)], 'tiny')
+        self.assertEqual(nd[str(BIG)], 'large')
+        self.assertEqual(nd[str(HUGE)], 'larger')
+        self.assertEqual(nd[str(REALLY_HUGE)], 'largest')
+        self.assertEqual(nd[repr(E)], "Euler's number")
+        self.assertEqual(nd[repr(PI)], 'pi')
+        self.assertEqual(nd[repr(TAU)], 'tau')
+
+    def test_dict_values(self):
+        d = dict(
+                tiny=BigNum.small,
+                large=BigNum.big,
+                larger=BigNum.huge,
+                largest=BigNum.really_huge,
+                e=FloatNum.e,
+                pi=FloatNum.pi,
+                tau=FloatNum.tau,
+                )
+        nd = self.loads(self.dumps(d))
+        self.assertEqual(nd['tiny'], SMALL)
+        self.assertEqual(nd['large'], BIG)
+        self.assertEqual(nd['larger'], HUGE)
+        self.assertEqual(nd['largest'], REALLY_HUGE)
+        self.assertEqual(nd['e'], E)
+        self.assertEqual(nd['pi'], PI)
+        self.assertEqual(nd['tau'], TAU)
+
+class TestPyEnum(TestEnum, PyTest): pass
+class TestCEnum(TestEnum, CTest): pass
diff --git a/Modules/_json.c b/Modules/_json.c
--- a/Modules/_json.c
+++ b/Modules/_json.c
@@ -116,6 +116,8 @@
 static PyObject *
 encoder_encode_string(PyEncoderObject *s, PyObject *obj);
 static PyObject *
+encoder_encode_long(PyEncoderObject* s UNUSED, PyObject *obj);
+static PyObject *
 encoder_encode_float(PyEncoderObject *s, PyObject *obj);
 
 #define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"')
@@ -1302,13 +1304,45 @@
 }
 
 static PyObject *
+encoder_encode_long(PyEncoderObject* s UNUSED, PyObject *obj)
+{
+    /* Return the JSON representation of a PyLong and PyLong subclasses.
+       Calls int() on PyLong subclasses in case the str() was changed.
+       Added specifically to deal with IntEnum.  See Issue18264. */
+    PyObject *encoded, *longobj;
+    if (PyLong_CheckExact(obj)) {
+        encoded = PyObject_Str(obj);
+    }
+    else {
+        longobj = PyNumber_Long(obj);
+        if (longobj == NULL) {
+            PyErr_SetString(
+                    PyExc_ValueError,
+                    "Unable to coerce int subclass to int"
+                    );
+            return NULL;
+        }
+        encoded = PyObject_Str(longobj);
+        Py_DECREF(longobj);
+    }
+    return encoded;
+}
+
+
+static PyObject *
 encoder_encode_float(PyEncoderObject *s, PyObject *obj)
 {
-    /* Return the JSON representation of a PyFloat */
+    /* Return the JSON representation of a PyFloat.
+       Modified to call float() on float subclasses in case the subclass
+       changes the repr.  See Issue18264.  */
+    PyObject *encoded, *floatobj;
     double i = PyFloat_AS_DOUBLE(obj);
     if (!Py_IS_FINITE(i)) {
         if (!s->allow_nan) {
-            PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant");
+            PyErr_SetString(
+                    PyExc_ValueError,
+                    "Out of range float values are not JSON compliant"
+                    );
             return NULL;
         }
         if (i > 0) {
@@ -1321,8 +1355,24 @@
             return PyUnicode_FromString("NaN");
         }
     }
-    /* Use a better float format here? */
-    return PyObject_Repr(obj);
+    /* coerce float subclasses to float (primarily for Enum) */
+    if (PyFloat_CheckExact(obj)) {
+        /* Use a better float format here? */
+        encoded = PyObject_Repr(obj);
+    }
+    else {
+        floatobj = PyNumber_Float(obj);
+        if (floatobj == NULL) {
+            PyErr_SetString(
+                    PyExc_ValueError,
+                    "Unable to coerce float subclass to float"
+                    );
+            return NULL;
+        }
+        encoded = PyObject_Repr(floatobj);
+        Py_DECREF(floatobj);
+    }
+    return encoded;
 }
 
 static PyObject *
@@ -1366,7 +1416,7 @@
         return _steal_accumulate(acc, encoded);
     }
     else if (PyLong_Check(obj)) {
-        PyObject *encoded = PyObject_Str(obj);
+        PyObject *encoded = encoder_encode_long(s, obj);
         if (encoded == NULL)
             return -1;
         return _steal_accumulate(acc, encoded);
@@ -1551,9 +1601,10 @@
                 goto bail;
         }
         else if (PyLong_Check(key)) {
-            kstr = PyObject_Str(key);
-            if (kstr == NULL)
+            kstr = encoder_encode_long(s, key);
+            if (kstr == NULL) {
                 goto bail;
+            }
         }
         else if (skipkeys) {
             Py_DECREF(item);

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list