Skip to content

Commit 1dcb1df

Browse files
committed
Merge pull request #50 from msiemens/custom-serialization
Add support for custom serializers
2 parents 8977d5a + eb3068d commit 1dcb1df

File tree

5 files changed

+134
-6
lines changed

5 files changed

+134
-6
lines changed

docs/extend.rst

+36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
11
How to Extend TinyDB
22
====================
33

4+
Write a Serializer
5+
------------------
6+
7+
TinyDB's default storage is fairly limited when it comes to supported data types.
8+
If you need more flexibility, you can implement a Serializer. This allows TinyDB
9+
to handle classes it otherwise couldn't serialize. Let's see how a Serializer
10+
for ``datetime`` objects could look like:
11+
12+
.. code-block:: python
13+
14+
from datetime import datetime
15+
16+
class DateTimeSerializer(Serializer):
17+
OBJ_CLASS = datetime # The class this serializer handles
18+
19+
def encode(self, obj):
20+
return obj.strftime('%Y-%m-%dT%H:%M:%S')
21+
22+
def decode(self, s):
23+
return datetime.strptime(s, '%Y-%m-%dT%H:%M:%S')
24+
25+
To use the new serializer, we need to use the serialization middleware:
26+
27+
.. code-block:: python
28+
29+
>>> from tinydb.storages import JSONStorage
30+
>>> from tinydb.middlewares import SerializationMiddleware
31+
>>>
32+
>>> serialization = SerializationMiddleware()
33+
>>> serialization.register_serializer(DateTimeSerializer(), 'TinyDate')
34+
>>>
35+
>>> db = TinyDB('db.json', storage=serialization)
36+
>>> db.insert({'date': datetime(2000, 1, 1, 12, 0, 0)})
37+
>>> db.all()
38+
[{'date': datetime.datetime(2000, 1, 1, 12, 0)}]
39+
440
Write a custom Storage
541
----------------------
642

docs/usage.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,9 @@ What's next
427427
Congratulations, you've made through the user guide! Now go and build something
428428
awesome or dive deeper into TinyDB with these ressources:
429429

430-
- Want to learn how to customize TinyDB and what extensions exist?
431-
Check out :doc:`extend`.
430+
- Want to learn how to customize TinyDB (custom serializers, storages,
431+
middlewares) and what extensions exist? Check out :doc:`extend` and
432+
:doc:`extensions`.
432433
- Want to study the API in detail? Read :doc:`api`.
433434
- Interested in contributing to the TinyDB development guide? Go on to the
434435
:doc:`contribute`.

tinydb/database.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class TinyDB(object):
3030
and getting tables.
3131
"""
3232

33+
DEFAULT_STORAGE = JSONStorage
34+
3335
def __init__(self, *args, **kwargs):
3436
"""
3537
Create a new instance of TinyDB.
@@ -40,7 +42,7 @@ def __init__(self, *args, **kwargs):
4042
:param storage: The class of the storage to use. Will be initialized
4143
with ``args`` and ``kwargs``.
4244
"""
43-
storage = kwargs.pop('storage', JSONStorage)
45+
storage = kwargs.pop('storage', TinyDB.DEFAULT_STORAGE)
4446
#: :type: Storage
4547
self._storage = storage(*args, **kwargs)
4648

@@ -153,7 +155,6 @@ def _write_table(self, values, table):
153155
data[table] = values
154156

155157
self._write(data)
156-
return
157158

158159
# Methods that are executed on the default table
159160
# Because magic methods are not handlet by __getattr__ we need to forward

tinydb/middlewares.py

+76-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Contains the :class:`base class <tinydb.middlewares.Middleware>` for
33
middlewares and implementations.
44
"""
5+
from tinydb import TinyDB
6+
from tinydb.storages import JSONStorage
57

68

79
class Middleware(object):
@@ -17,7 +19,7 @@ class Middleware(object):
1719
example).
1820
"""
1921

20-
def __init__(self, storage_cls):
22+
def __init__(self, storage_cls=TinyDB.DEFAULT_STORAGE):
2123
self._storage_cls = storage_cls
2224
self.storage = None
2325

@@ -84,7 +86,7 @@ class CachingMiddleware(Middleware):
8486
#: The number of write operations to cache before writing to disc
8587
WRITE_CACHE_SIZE = 1000
8688

87-
def __init__(self, storage_cls):
89+
def __init__(self, storage_cls=TinyDB.DEFAULT_STORAGE):
8890
super(CachingMiddleware, self).__init__(storage_cls)
8991

9092
self.cache = None
@@ -116,3 +118,75 @@ def flush(self):
116118
def close(self):
117119
self.flush()
118120
self.storage.close()
121+
122+
123+
class SerializationMiddleware(Middleware):
124+
"""
125+
Provide custom serialization for TinyDB.
126+
127+
This middleware allows users of TinyDB to register custom serializations.
128+
The serialized data will be passed to the wrapped storage and data that
129+
is read from the storage will be deserialized.
130+
"""
131+
132+
def __init__(self, storage_cls=TinyDB.DEFAULT_STORAGE):
133+
super(SerializationMiddleware, self).__init__(storage_cls)
134+
135+
self._serializers = {}
136+
137+
def register_serializer(self, serializer, name):
138+
"""
139+
Register a new Serializer.
140+
141+
When reading from/writing to the underlying storage, TinyDB
142+
will run all objects through the list of registered serializers
143+
allowing each one to handle objects it recognizes.
144+
145+
.. note:: The name has to be unique among this database instance.
146+
Re-using the same name will overwrite the old serializer.
147+
Also, registering a serializer will be reflected in all
148+
tables when reading/writing them.
149+
150+
:param serializer: an instance of the serializer
151+
:type serializer: tinydb.serialize.Serializer
152+
"""
153+
self._serializers[name] = serializer
154+
155+
def read(self):
156+
data = self.storage.read()
157+
158+
for serializer_name in self._serializers:
159+
serializer = self._serializers[serializer_name]
160+
tag = '{{{}}}:'.format(serializer_name) # E.g: '{TinyDate}:'
161+
162+
for eid in data:
163+
for field in data[eid]:
164+
try:
165+
if data[eid][field].startswith(tag):
166+
encoded = data[eid][field][len(tag):]
167+
data[eid][field] = serializer.decode(encoded)
168+
except AttributeError:
169+
pass # Not a string
170+
171+
return data
172+
173+
def write(self, data):
174+
for serializer_name in self._serializers:
175+
# If no serializers are registered, this code will just look up
176+
# the serializer list and continue. But if there are serializers,
177+
# the inner loop will run very often.
178+
# For that reason, the lookup of the serialized class is pulled
179+
# out into the outer loop:
180+
181+
serializer = self._serializers[serializer_name]
182+
serializer_class = serializer.OBJ_CLASS
183+
184+
for eid in data:
185+
for field in data[eid]:
186+
if isinstance(data[eid][field], serializer_class):
187+
encoded = serializer.encode(data[eid][field])
188+
tagged = '{{{}}}:{}'.format(serializer_name, encoded)
189+
190+
data[eid][field] = tagged
191+
192+
self.storage.write(data)

tinydb/utils.py

+16
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,19 @@ def catch_warning(warning_cls):
103103
warnings.filterwarnings('error', category=warning_cls)
104104

105105
yield
106+
107+
108+
class AbstractAttribute(object):
109+
"""An abstract class attribute.
110+
111+
Use this instead of an abstract property when you don't expect the
112+
attribute to be implemented by a property.
113+
"""
114+
115+
__isabstractmethod__ = True
116+
117+
def __init__(self, doc=""):
118+
self.__doc__ = doc
119+
120+
def __get__(self, obj, cls):
121+
return self

0 commit comments

Comments
 (0)