Skip to content

Commit a9aad89

Browse files
Merge pull request #709 from dimitri-yatsenko/master
fix #690 -- blob packing/unpacking of native python bool, int, float, and complex.
2 parents c127a11 + 9be1115 commit a9aad89

File tree

8 files changed

+148
-69
lines changed

8 files changed

+148
-69
lines changed

CHANGELOG.md

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
## Release notes
22

3-
## 0.12.3 -- Nov 22, 2019
4-
* Bugfix #675 (PR #705) networkx 2.4+ is now supported
5-
* Bugfix #698 and #699 (PR #706) display table definition in doc string and help
6-
* Bugfix #701 (PR #702) job reservation works with native python datatype support disabled
3+
### 0.12.4 -- Jan 14, 2020
4+
* Support for simple scalar datatypes in blobs (#690) PR #709
5+
* Add support for the `serial` data type in declarations: alias for `bigint unsigned auto_increment` PR #713
6+
* Improve the log table to avoid primary key collisions PR #713
7+
* Improve documentation in README PR #713
8+
9+
### 0.12.3 -- Nov 22, 2019
10+
* Bugfix - networkx 2.4 causes error in diagrams (#675) PR #705
11+
* Bugfix - include table definition in doc string and help (#698, #699) PR #706
12+
* Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
713

814
### 0.12.2 -- Nov 11, 2019
9-
* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691)
10-
* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692)
15+
* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691) PR #696
16+
* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692) PR #693
1117

1218
### 0.12.1 -- Nov 2, 2019
13-
* Bugfix - AttributeAdapter converts into a string (#684)
19+
* Bugfix - AttributeAdapter converts into a string (#684) PR #688
1420

1521
### 0.12.0 -- Oct 31, 2019
1622
* Dropped support for Python 3.4

LNX-docker-compose.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ services:
3333
"
3434
pip install --user nose nose-cov coveralls .;
3535
pip freeze | grep datajoint;
36-
nosetests -vsw tests --with-coverage --cover-package=datajoint;
3736
coveralls;
37+
nosetests -vsw tests --with-coverage --cover-package=datajoint;
3838
# jupyter notebook;
3939
"
4040
# ports:
@@ -92,4 +92,4 @@ services:
9292
- ./tests/nginx/fullchain.pem:/certs/fullchain.pem
9393
- ./tests/nginx/privkey.pem:/certs/privkey.pem
9494
networks:
95-
main:
95+
main:

datajoint/blob.py

+61-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
(De)serialization methods for python datatypes and numpy.ndarrays with provisions for mutual
2+
(De)serialization methods for basic datatypes and numpy.ndarrays with provisions for mutual
33
compatibility with Matlab-based serialization implemented by mYm.
44
"""
55

@@ -115,21 +115,25 @@ def read_blob(self, n_bytes=None):
115115
"P": self.read_sparse_array, # matlab sparse array -- not supported yet
116116
"S": self.read_struct, # matlab struct array
117117
"C": self.read_cell_array, # matlab cell array
118-
# Python-native
119-
"\xFF": self.read_none, # None
120-
"\1": self.read_tuple, # a Sequence
121-
"\2": self.read_list, # a MutableSequence
122-
"\3": self.read_set, # a Set
123-
"\4": self.read_dict, # a Mapping
124-
"\5": self.read_string, # a UTF8-encoded string
125-
"\6": self.read_bytes, # a ByteString
126-
"F": self.read_recarray, # numpy array with fields, including recarrays
127-
"d": self.read_decimal, # a decimal
128-
"t": self.read_datetime, # date, time, or datetime
129-
"u": self.read_uuid, # UUID
118+
# basic data types
119+
"\xFF": self.read_none, # None
120+
"\x01": self.read_tuple, # a Sequence (e.g. tuple)
121+
"\x02": self.read_list, # a MutableSequence (e.g. list)
122+
"\x03": self.read_set, # a Set
123+
"\x04": self.read_dict, # a Mapping (e.g. dict)
124+
"\x05": self.read_string, # a UTF8-encoded string
125+
"\x06": self.read_bytes, # a ByteString
126+
"\x0a": self.read_int, # unbounded scalar int
127+
"\x0b": self.read_bool, # scalar boolean
128+
"\x0c": self.read_complex, # scalar 128-bit complex number
129+
"\x0d": self.read_float, # scalar 64-bit float
130+
"F": self.read_recarray, # numpy array with fields, including recarrays
131+
"d": self.read_decimal, # a decimal
132+
"t": self.read_datetime, # date, time, or datetime
133+
"u": self.read_uuid, # UUID
130134
}[data_structure_code]
131135
except KeyError:
132-
raise DataJointError('Unknown data structure code "%s"' % data_structure_code)
136+
raise DataJointError('Unknown data structure code "%s". Upgrade datajoint.' % data_structure_code)
133137
v = call()
134138
if n_bytes is not None and self._pos - start != n_bytes:
135139
raise DataJointError('Blob length check failed! Invalid blob')
@@ -146,13 +150,21 @@ def pack_blob(self, obj):
146150

147151
# blob types in the expanded dj0 blob format
148152
self.set_dj0()
153+
if not isinstance(obj, (np.ndarray, np.number)):
154+
# python built-in data types
155+
if isinstance(obj, bool):
156+
return self.pack_bool(obj)
157+
if isinstance(obj, int):
158+
return self.pack_int(obj)
159+
if isinstance(obj, complex):
160+
return self.pack_complex(obj)
161+
if isinstance(obj, float):
162+
return self.pack_float(obj)
149163
if isinstance(obj, np.ndarray) and obj.dtype.fields:
150164
return self.pack_recarray(np.array(obj))
151165
if isinstance(obj, np.number):
152166
return self.pack_array(np.array(obj))
153-
if isinstance(obj, (bool, np.bool, np.bool_)):
154-
return self.pack_array(np.array(obj))
155-
if isinstance(obj, (float, int, complex)):
167+
if isinstance(obj, (np.bool, np.bool_)):
156168
return self.pack_array(np.array(obj))
157169
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
158170
return self.pack_datetime(obj)
@@ -209,7 +221,7 @@ def pack_array(self, array):
209221
if is_complex:
210222
array, imaginary = np.real(array), np.imag(array)
211223
type_id = (rev_class_id[array.dtype] if array.dtype.char != 'U'
212-
else rev_class_id[np.dtype('O')])
224+
else rev_class_id[np.dtype('O')])
213225
if dtype_list[type_id] is None:
214226
raise DataJointError("Type %s is ambiguous or unknown" % array.dtype)
215227

@@ -251,6 +263,36 @@ def pack_recarray(self, array):
251263
def read_sparse_array(self):
252264
raise DataJointError('datajoint-python does not yet support sparse arrays. Issue (#590)')
253265

266+
def read_int(self):
267+
return int.from_bytes(self.read_binary(self.read_value('uint16')), byteorder='little', signed=True)
268+
269+
@staticmethod
270+
def pack_int(v):
271+
n_bytes = v.bit_length() // 8 + 1
272+
assert 0 < n_bytes <= 0xFFFF, 'Integers are limited to 65535 bytes'
273+
return b"\x0a" + np.uint16(n_bytes).tobytes() + v.to_bytes(n_bytes, byteorder='little', signed=True)
274+
275+
def read_bool(self):
276+
return bool(self.read_value('bool'))
277+
278+
@staticmethod
279+
def pack_bool(v):
280+
return b"\x0b" + np.array(v, dtype='bool').tobytes()
281+
282+
def read_complex(self):
283+
return complex(self.read_value('complex128'))
284+
285+
@staticmethod
286+
def pack_complex(v):
287+
return b"\x0c" + np.array(v, dtype='complex128').tobytes()
288+
289+
def read_float(self):
290+
return float(self.read_value('float64'))
291+
292+
@staticmethod
293+
def pack_float(v):
294+
return b"\x0d" + np.array(v, dtype='float64').tobytes()
295+
254296
def read_decimal(self):
255297
return Decimal(self.read_string())
256298

@@ -269,7 +311,7 @@ def pack_string(s):
269311

270312
def read_bytes(self):
271313
return self.read_binary(self.read_value())
272-
314+
273315
@staticmethod
274316
def pack_bytes(s):
275317
return b"\6" + len_u64(s) + s

datajoint/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "0.12.3"
1+
__version__ = "0.12.4"
22

33
assert len(__version__) <= 10 # The log table limits version to the 10 characters

docs-parts/intro/Releases_lang1.rst

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
0.12.4 -- Jan 14, 2020
2+
* Support for simple scalar datatypes in blobs (#690) PR #709
3+
* Add support for the `serial` data type in declarations: alias for `bigint unsigned auto_increment` PR #713
4+
* Improve the log table to avoid primary key collisions PR #713
5+
* Improve documentation in README PR #713
6+
17
0.12.3 -- Nov 22, 2019
28
----------------------
39
* Bugfix - networkx 2.4 causes error in diagrams (#675) PR #705
4-
* Bugfix - include definition in doc string and help (#698, #699) PR #706
5-
* Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
6-
10+
* Bugfix - include table definition in doc string and help (#698, #699) PR #706
11+
* Bugfix - job reservation fails when native python datatype support is disabled (#701) PR #702
712

813
0.12.2 -- Nov 11, 2019
914
-------------------------
10-
* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691)
11-
* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692)
15+
* Bugfix - Convoluted error thrown if there is a reference to a non-existent table attribute (#691) PR #696
16+
* Bugfix - Insert into external does not trim leading slash if defined in `dj.config['stores']['<store>']['location']` (#692) PR #693
1217

1318
0.12.1 -- Nov 2, 2019
1419
-------------------------
15-
* Bugfix - AttributeAdapter converts into a string (#684)
20+
* Bugfix - AttributeAdapter converts into a string (#684) PR #688
1621

1722
0.12.0 -- Oct 31, 2019
1823
-------------------------

tests/schema_adapted.py

+19-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datajoint as dj
22
import networkx as nx
3+
import json
34
from pathlib import Path
45
import tempfile
56
from datajoint import errors
@@ -11,8 +12,8 @@
1112
S3_CONN_INFO,
1213
protocol='s3',
1314
location='adapted/repo',
14-
stage=tempfile.mkdtemp())
15-
}
15+
stage=tempfile.mkdtemp())}
16+
1617
dj.config['stores'] = stores_config
1718

1819
schema_name = PREFIX + '_test_custom_datatype'
@@ -53,37 +54,36 @@ class Connectivity(dj.Manual):
5354
errors._switch_filepath_types(True)
5455

5556

56-
class Filepath2GraphAdapter(dj.AttributeAdapter):
57+
class LayoutToFilepath(dj.AttributeAdapter):
58+
"""
59+
An adapted data type that saves a graph layout into fixed filepath
60+
"""
5761

5862
attribute_type = 'filepath@repo_s3'
5963

6064
@staticmethod
61-
def get(obj):
62-
s = open(obj, "r").read()
63-
return nx.spring_layout(
64-
nx.lollipop_graph(4, 2), seed=int(s))
65+
def get(path):
66+
with open(path, "r") as f:
67+
return json.load(f)
6568

6669
@staticmethod
67-
def put(obj):
68-
path = Path(
69-
dj.config['stores']['repo_s3']['stage'], 'sample.txt')
70-
71-
f = open(path, "w")
72-
f.write(str(obj*obj))
73-
f.close()
74-
70+
def put(layout):
71+
path = Path(dj.config['stores']['repo_s3']['stage'], 'layout.json')
72+
with open(str(path), "w") as f:
73+
json.dump(layout, f)
7574
return path
7675

7776

78-
file2graph = Filepath2GraphAdapter()
77+
layout_to_filepath = LayoutToFilepath()
7978

8079

8180
@schema
82-
class Position(dj.Manual):
81+
class Layout(dj.Manual):
8382
definition = """
84-
pos_id : int
83+
# stores graph layout
84+
-> Connectivity
8585
---
86-
seed_root: <file2graph>
86+
layout: <layout_to_filepath>
8787
"""
8888

8989
errors._switch_filepath_types(False)

tests/test_adapted_attributes.py

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import datajoint as dj
22
import networkx as nx
33
from itertools import zip_longest
4-
from nose.tools import assert_true, assert_equal
4+
from nose.tools import assert_true, assert_equal, assert_dict_equal
55
from . import schema_adapted as adapted
6-
from .schema_adapted import graph, file2graph
6+
from .schema_adapted import graph, layout_to_filepath
77

88

99
def test_adapted_type():
@@ -22,17 +22,25 @@ def test_adapted_type():
2222

2323
def test_adapted_filepath_type():
2424
# https://github.com/datajoint/datajoint-python/issues/684
25+
2526
dj.errors._switch_adapted_types(True)
2627
dj.errors._switch_filepath_types(True)
27-
c = adapted.Position()
28-
Position.insert([{'pos_id': 0, 'seed_root': 3}])
29-
result = (Position & 'pos_id=0').fetch1('seed_root')
3028

31-
assert_true(isinstance(result, dict))
32-
assert_equal(0.3761992090175474, result[1][0])
33-
assert_true(6 == len(result))
29+
c = adapted.Connectivity()
30+
c.delete()
31+
c.insert1((0, nx.lollipop_graph(4, 2)))
3432

33+
layout = nx.spring_layout(c.fetch1('conn_graph'))
34+
# make json friendly
35+
layout = {str(k): [round(r, ndigits=4) for r in v] for k, v in layout.items()}
36+
t = adapted.Layout()
37+
t.insert1((0, layout))
38+
result = t.fetch1('layout')
39+
assert_dict_equal(result, layout)
40+
41+
t.delete()
3542
c.delete()
43+
3644
dj.errors._switch_filepath_types(False)
3745
dj.errors._switch_adapted_types(False)
3846

tests/test_blob.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from datetime import datetime
55
from datajoint.blob import pack, unpack
66
from numpy.testing import assert_array_equal
7-
from nose.tools import assert_equal, assert_true, \
7+
from nose.tools import assert_equal, assert_true, assert_false, \
88
assert_list_equal, assert_set_equal, assert_tuple_equal, assert_dict_equal
99

1010

@@ -23,23 +23,41 @@ def test_pack():
2323
x = np.random.randn(10)
2424
assert_array_equal(x, unpack(pack(x)), "Arrays do not match!")
2525

26-
x = 7j
27-
assert_equal(x, unpack(pack(x)), "Complex scalar does not match")
28-
2926
x = np.float32(np.random.randn(3, 4, 5))
3027
assert_array_equal(x, unpack(pack(x)), "Arrays do not match!")
3128

3229
x = np.int16(np.random.randn(1, 2, 3))
3330
assert_array_equal(x, unpack(pack(x)), "Arrays do not match!")
3431

3532
x = None
36-
assert_true(x is None, "None did not match")
33+
assert_true(unpack(pack(x)) is None, "None did not match")
34+
35+
x = -255
36+
y = unpack(pack(x))
37+
assert_true(x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Scalar int did not match")
38+
39+
x = -25523987234234287910987234987098245697129798713407812347
40+
y = unpack(pack(x))
41+
assert_true(x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Unbounded int did not match")
42+
43+
x = 7.
44+
y = unpack(pack(x))
45+
assert_true(x == y and isinstance(y, float) and not isinstance(y, np.ndarray), "Scalar float did not match")
46+
47+
x = 7j
48+
y = unpack(pack(x))
49+
assert_true(x == y and isinstance(y, complex) and not isinstance(y, np.ndarray), "Complex scalar did not match")
50+
51+
x = True
52+
assert_true(unpack(pack(x)) is True, "Scalar bool did not match")
3753

3854
x = [None]
3955
assert_list_equal(x, unpack(pack(x)))
4056

41-
x = {'name': 'Anonymous', 'age': 15, 99: datetime.now(), 'range': [110, 190], (11,12): None}
42-
assert_dict_equal(x, unpack(pack(x)), "Dict do not match!")
57+
x = {'name': 'Anonymous', 'age': 15, 99: datetime.now(), 'range': [110, 190], (11, 12): None}
58+
y = unpack(pack(x))
59+
assert_dict_equal(x, y, "Dict do not match!")
60+
assert_false(isinstance(['range'][0], np.ndarray), "Scalar int was coerced into arrray.")
4361

4462
x = uuid.uuid4()
4563
assert_equal(x, unpack(pack(x)), 'UUID did not match')

0 commit comments

Comments
 (0)