]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Correct name for json_serializer / json_deserializer, document and test
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 9 Aug 2019 03:34:20 +0000 (23:34 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 9 Aug 2019 03:52:51 +0000 (23:52 -0400)
The dialects that support json are supposed to take arguments
``json_serializer`` and ``json_deserializer`` at the create_engine() level,
however the SQLite dialect calls them ``_json_serilizer`` and
``_json_deserilalizer``.  The names have been corrected, the old names are
accepted with a change warning, and these parameters are now documented as
:paramref:`.create_engine.json_serializer` and
:paramref:`.create_engine.json_deserializer`.

Fixes: #4798
Change-Id: I1dbfe439b421fe9bb7ff3594ef455af8156f8851

doc/build/changelog/unreleased_13/4798.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/engine/create.py
lib/sqlalchemy/sql/sqltypes.py
lib/sqlalchemy/testing/suite/test_types.py
test/dialect/test_sqlite.py

diff --git a/doc/build/changelog/unreleased_13/4798.rst b/doc/build/changelog/unreleased_13/4798.rst
new file mode 100644 (file)
index 0000000..6d63811
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: bug, sqlite
+    :tickets: 4798
+
+    The dialects that support json are supposed to take arguments
+    ``json_serializer`` and ``json_deserializer`` at the create_engine() level,
+    however the SQLite dialect calls them ``_json_serilizer`` and
+    ``_json_deserilalizer``.  The names have been corrected, the old names are
+    accepted with a change warning, and these parameters are now documented as
+    :paramref:`.create_engine.json_serializer` and
+    :paramref:`.create_engine.json_deserializer`.
+
index ef8507d0522bc933e3ffb3a1cbd0b8a9ffd23579..78ce18ac6415e3a45d30a07f42c582661e6a60a9 100644 (file)
@@ -1426,18 +1426,39 @@ class SQLiteDialect(default.DefaultDialect):
     _broken_fk_pragma_quotes = False
     _broken_dotted_colnames = False
 
+    @util.deprecated_params(
+        _json_serializer=(
+            "1.3.7",
+            "The _json_serializer argument to the SQLite dialect has "
+            "been renamed to the correct name of json_serializer.  The old "
+            "argument name will be removed in a future release.",
+        ),
+        _json_deserializer=(
+            "1.3.7",
+            "The _json_deserializer argument to the SQLite dialect has "
+            "been renamed to the correct name of json_deserializer.  The old "
+            "argument name will be removed in a future release.",
+        ),
+    )
     def __init__(
         self,
         isolation_level=None,
         native_datetime=False,
+        json_serializer=None,
+        json_deserializer=None,
         _json_serializer=None,
         _json_deserializer=None,
         **kwargs
     ):
         default.DefaultDialect.__init__(self, **kwargs)
         self.isolation_level = isolation_level
-        self._json_serializer = _json_serializer
-        self._json_deserializer = _json_deserializer
+
+        if _json_serializer:
+            json_serializer = _json_serializer
+        if _json_deserializer:
+            json_deserializer = _json_deserializer
+        self._json_serializer = json_serializer
+        self._json_deserializer = json_deserializer
 
         # this flag used by pysqlite dialect, and perhaps others in the
         # future, to indicate the driver is handling date/timestamp
index 035953e990834a562fa071d5ce8b160e510aa6cc..cc83041319569db7e4ba09fe770ee71f168c5343 100644 (file)
@@ -234,6 +234,22 @@ def create_engine(url, **kwargs):
 
             :ref:`session_transaction_isolation` - for the ORM
 
+    :param json_deserializer: for dialects that support the :class:`.JSON`
+        datatype, this is a Python callable that will convert a JSON string
+        to a Python object.  By default, the Python ``json.loads`` function is
+        used.
+
+        .. versionchanged:: 1.3.7  The SQLite dialect renamed this from
+           ``_json_deserializer``.
+
+    :param json_serializer: for dialects that support the :class:`.JSON`
+        datatype, this is a Python callable that will render a given object
+        as JSON.   By default, the Python ``json.dumps`` function is used.
+
+        .. versionchanged:: 1.3.7  The SQLite dialect renamed this from
+           ``_json_serializer``.
+
+
     :param label_length=None: optional integer value which limits
         the size of dynamically generated column labels to that many
         characters. If less than 6, labels are generated as
index 38731fcbe54c886db0158e9546f3d83a67ff4acd..631352ceb7986d05de70c34cd8df7354fc34ff48 100644 (file)
@@ -2042,6 +2042,26 @@ class JSON(Indexable, TypeEngine):
     values, but care must be taken as to the value of the
     :paramref:`.JSON.none_as_null` in these cases.
 
+    The JSON serializer and deserializer used by :class:`.JSON` defaults to
+    Python's ``json.dumps`` and ``json.loads`` functions; in the case of the
+    psycopg2 dialect, psycopg2 may be using its own custom loader function.
+
+    In order to affect the serializer / deserializer, they are currently
+    configurable at the :func:`.create_engine` level via the
+    :paramref:`.create_engine.json_serializer` and
+    :paramref:`.create_engine.json_deserializer` parameters.  For example,
+    to turn off ``ensure_ascii``::
+
+        engine = create_engine(
+            "sqlite://",
+            json_serializer=lambda obj: json.dumps(obj, ensure_ascii=False))
+
+    .. versionchanged:: 1.3.7
+
+        SQLite dialect's ``json_serializer`` and ``json_deserializer``
+        parameters renamed from ``_json_serializer`` and
+        ``_json_deserializer``.
+
     .. seealso::
 
         :class:`.postgresql.JSON`
index 1e02c0e74246ce56358a649418f27ae91bd86f39..3320dd93c7f52addf4f596ae5a44d3080ad667e9 100644 (file)
@@ -2,8 +2,12 @@
 
 import datetime
 import decimal
+import json
+
+import mock
 
 from .. import config
+from .. import engines
 from .. import fixtures
 from ..assertions import eq_
 from ..config import requirements
@@ -727,6 +731,28 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest):
 
         eq_(row, (data_element,))
 
+    def test_round_trip_custom_json(self):
+        data_table = self.tables.data_table
+        data_element = self.data1
+
+        js = mock.Mock(side_effect=json.dumps)
+        jd = mock.Mock(side_effect=json.loads)
+        engine = engines.testing_engine(
+            options=dict(json_serializer=js, json_deserializer=jd)
+        )
+
+        # support sqlite :memory: database...
+        data_table.create(engine, checkfirst=True)
+        engine.execute(
+            data_table.insert(), {"name": "row1", "data": data_element}
+        )
+
+        row = engine.execute(select([data_table.c.data])).first()
+
+        eq_(row, (data_element,))
+        eq_(js.mock_calls, [mock.call(data_element)])
+        eq_(jd.mock_calls, [mock.call(json.dumps(data_element))])
+
     def test_round_trip_none_as_sql_null(self):
         col = self.tables.data_table.c["nulldata"]
 
index e727005103dfa3d138c4ad128b02999e880b6b91..1e894da5566e0ffbc6af239bf4f617fd63255194 100644 (file)
@@ -2,6 +2,7 @@
 
 """SQLite-specific tests."""
 import datetime
+import json
 import os
 
 from sqlalchemy import and_
@@ -318,6 +319,35 @@ class JSONTest(fixtures.TestBase):
                 conn.scalar(select([sqlite_json.c.foo["json"]])), value["json"]
             )
 
+    @testing.provide_metadata
+    def test_deprecated_serializer_args(self):
+        sqlite_json = Table(
+            "json_test", self.metadata, Column("foo", sqlite.JSON)
+        )
+        data_element = {"foo": "bar"}
+
+        js = mock.Mock(side_effect=json.dumps)
+        jd = mock.Mock(side_effect=json.loads)
+
+        with testing.expect_deprecated(
+            "The _json_deserializer argument to the SQLite "
+            "dialect has been renamed",
+            "The _json_serializer argument to the SQLite "
+            "dialect has been renamed",
+        ):
+            engine = engines.testing_engine(
+                options=dict(_json_serializer=js, _json_deserializer=jd)
+            )
+        self.metadata.create_all(engine)
+
+        engine.execute(sqlite_json.insert(), {"foo": data_element})
+
+        row = engine.execute(select([sqlite_json.c.foo])).first()
+
+        eq_(row, (data_element,))
+        eq_(js.mock_calls, [mock.call(data_element)])
+        eq_(jd.mock_calls, [mock.call(json.dumps(data_element))])
+
 
 class DateTimeTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_time_microseconds(self):