Skip to content

Commit

Permalink
Correct name for json_serializer / json_deserializer, document and test
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zzzeek committed Aug 9, 2019
1 parent d8da7f5 commit 104e690
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 2 deletions.
12 changes: 12 additions & 0 deletions doc/build/changelog/unreleased_13/4798.rst
@@ -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`.

25 changes: 23 additions & 2 deletions lib/sqlalchemy/dialects/sqlite/base.py
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions lib/sqlalchemy/engine/create.py
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions lib/sqlalchemy/sql/sqltypes.py
Expand Up @@ -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`
Expand Down
26 changes: 26 additions & 0 deletions lib/sqlalchemy/testing/suite/test_types.py
Expand Up @@ -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
Expand Down Expand Up @@ -727,6 +731,28 @@ def _test_round_trip(self, data_element):

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"]

Expand Down
30 changes: 30 additions & 0 deletions test/dialect/test_sqlite.py
Expand Up @@ -2,6 +2,7 @@

"""SQLite-specific tests."""
import datetime
import json
import os

from sqlalchemy import and_
Expand Down Expand Up @@ -318,6 +319,35 @@ def test_extract_subobject(self):
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):
Expand Down

0 comments on commit 104e690

Please sign in to comment.