Skip to content

Commit 913f258

Browse files
committed
Merge branch 'attrs'
2 parents f49ae89 + 7ae5afa commit 913f258

File tree

5 files changed

+177
-1
lines changed

5 files changed

+177
-1
lines changed

zarr/attrs.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import absolute_import, print_function, division
3+
4+
5+
import json
6+
import os
7+
from threading import Lock
8+
import fasteners
9+
10+
11+
class PersistentAttributes(object):
12+
13+
def __init__(self, path, mode):
14+
self._path = path
15+
self._mode = mode
16+
17+
def __getitem__(self, item):
18+
19+
if not os.path.exists(self._path):
20+
raise KeyError(item)
21+
22+
with open(self._path, mode='r') as f:
23+
return json.load(f)[item]
24+
25+
def __setitem__(self, key, value):
26+
27+
# handle read-only state
28+
if self._mode == 'r':
29+
raise ValueError('array is read-only')
30+
31+
# load existing data
32+
if not os.path.exists(self._path):
33+
d = dict()
34+
else:
35+
with open(self._path, mode='r') as f:
36+
d = json.load(f)
37+
38+
# set key value
39+
d[key] = value
40+
41+
# write modified data
42+
with open(self._path, mode='w') as f:
43+
json.dump(d, f)
44+
45+
def __delitem__(self, key):
46+
47+
# handle read-only state
48+
if self._mode == 'r':
49+
raise ValueError('array is read-only')
50+
51+
# load existing data
52+
if not os.path.exists(self._path):
53+
d = dict()
54+
else:
55+
with open(self._path, mode='r') as f:
56+
d = json.load(f)
57+
58+
# delete key value
59+
del d[key]
60+
61+
# write modified data
62+
with open(self._path, mode='w') as f:
63+
json.dump(d, f)
64+
65+
def asdict(self):
66+
if not os.path.exists(self._path):
67+
d = dict()
68+
else:
69+
with open(self._path, mode='r') as f:
70+
d = json.load(f)
71+
return d
72+
73+
def __iter__(self):
74+
return iter(self.asdict())
75+
76+
def __len__(self):
77+
return len(self.asdict())
78+
79+
def keys(self):
80+
return self.asdict().keys()
81+
82+
def values(self):
83+
return self.asdict().values()
84+
85+
def items(self):
86+
return self.asdict().items()
87+
88+
89+
class SynchronizedPersistentAttributes(PersistentAttributes):
90+
91+
def __init__(self, path, mode):
92+
super(SynchronizedPersistentAttributes, self).__init__(path, mode)
93+
lock_path = self._path + '.lock'
94+
self._thread_lock = Lock()
95+
self._file_lock = fasteners.InterProcessLock(lock_path)
96+
97+
def __getitem__(self, item):
98+
with self._thread_lock:
99+
with self._file_lock:
100+
v = super(SynchronizedPersistentAttributes, self).__getitem__(item)
101+
return v
102+
103+
def __setitem__(self, key, value):
104+
with self._thread_lock:
105+
with self._file_lock:
106+
super(SynchronizedPersistentAttributes, self).__setitem__(key, value)
107+
108+
def __delitem__(self, key):
109+
with self._thread_lock:
110+
with self._file_lock:
111+
super(SynchronizedPersistentAttributes, self).__delitem__(key)
112+
113+
def asdict(self):
114+
with self._thread_lock:
115+
with self._file_lock:
116+
return super(SynchronizedPersistentAttributes, self).asdict()

zarr/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
shuffle = 1 # byte shuffle
88

99
# for persistence
10+
attrpath = '__zattr__'
1011
metapath = '__zmeta__'
1112
datapath = '__zdata__'
1213
datasuffix = '.blosc'

zarr/ext.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ cdef class BaseArray:
5353
cdef int _clevel
5454
cdef int _shuffle
5555
cdef object _fill_value
56+
cdef object _attrs
5657
cdef object _cdata
5758
# abstract methods
5859
cdef BaseChunk create_chunk(self, tuple cidx)

zarr/ext.pyx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import multiprocessing
2020
import fasteners
2121

2222

23-
from zarr import util as _util, meta as _meta, defaults as _defaults
23+
from zarr import util as _util, meta as _meta, defaults as _defaults, \
24+
attrs as _attrs
2425

2526

2627
###############################################################################
@@ -846,6 +847,10 @@ cdef class BaseArray:
846847
def __get__(self):
847848
return self._shuffle
848849

850+
property attrs:
851+
def __get__(self):
852+
return self._attrs
853+
849854
# derived properties
850855

851856
property size:
@@ -1130,6 +1135,9 @@ cdef class Array(BaseArray):
11301135
# instantiate chunks
11311136
self._cdata.flat = [self.create_chunk(None) for _ in self._cdata.flat]
11321137

1138+
# setup attributes container
1139+
self._attrs = dict()
1140+
11331141
# N.B., in the current implementation, some chunks may overhang
11341142
# the edge of the array. This is handled during the __getitem__ and
11351143
# __setitem__ methods by setting appropriate slices on the chunks,
@@ -1227,6 +1235,9 @@ cdef class PersistentArray(BaseArray):
12271235
# initialize chunks
12281236
self._init_cdata()
12291237

1238+
# initialise attributes
1239+
self._init_attrs()
1240+
12301241
def _init_cdata(self):
12311242

12321243
# initialize an object array to hold pointers to chunk objects
@@ -1236,6 +1247,10 @@ cdef class PersistentArray(BaseArray):
12361247
for cidx in itertools.product(*(range(n) for n in self._cdata_shape)):
12371248
self._cdata[cidx] = self.create_chunk(cidx)
12381249

1250+
def _init_attrs(self):
1251+
attr_path = os.path.join(self._path, _defaults.attrpath)
1252+
self._attrs = _attrs.PersistentAttributes(attr_path, mode=self._mode)
1253+
12391254
def _create(self, path, shape=None, chunks=None, dtype=None,
12401255
cname=None, clevel=None, shuffle=None, fill_value=None):
12411256

@@ -1289,6 +1304,14 @@ cdef class PersistentArray(BaseArray):
12891304
raise ValueError('dtype %r not consistent with existing %r' %
12901305
(dtype, self._dtype))
12911306

1307+
property path:
1308+
def __get__(self):
1309+
return self._path
1310+
1311+
property mode:
1312+
def __get__(self):
1313+
return self._mode
1314+
12921315
property cbytes:
12931316
def __get__(self):
12941317
return sum(c.cbytes for c in self._cdata.flat)
@@ -1361,6 +1384,11 @@ cdef class SynchronizedPersistentArray(PersistentArray):
13611384
shuffle=self._shuffle, fill_value=self._fill_value
13621385
)
13631386

1387+
def _init_attrs(self):
1388+
attr_path = os.path.join(self._path, _defaults.attrpath)
1389+
self._attrs = _attrs.SynchronizedPersistentAttributes(attr_path,
1390+
mode=self._mode)
1391+
13641392

13651393
###############################################################################
13661394
# LAZY ARRAY CLASSES #
@@ -1443,6 +1471,9 @@ cdef class LazyArray(BaseArray):
14431471
# initialize a dictionary for chunk objects
14441472
self._cdata = dict()
14451473

1474+
# initialise attributes
1475+
self._attrs = dict()
1476+
14461477
# instantiate chunks - not now!
14471478

14481479
property cbytes:
@@ -1593,3 +1624,8 @@ cdef class SynchronizedLazyPersistentArray(LazyPersistentArray):
15931624
cname=self._cname, clevel=self._clevel, shuffle=self._shuffle,
15941625
fill_value=self._fill_value
15951626
)
1627+
1628+
def _init_attrs(self):
1629+
attr_path = os.path.join(self._path, _defaults.attrpath)
1630+
self._attrs = _attrs.SynchronizedPersistentAttributes(attr_path,
1631+
mode=self._mode)

zarr/tests/test_array.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,18 @@ def test_append_2d_axis(self):
276276
eq((10, 10), z.chunks)
277277
assert_array_equal(e, z[:])
278278

279+
def test_attrs(self):
280+
z = self.create_array(shape=(100, 100), chunks=(10, 10), dtype='i4')
281+
assert hasattr(z, 'attrs')
282+
with assert_raises(KeyError):
283+
v = z.attrs['foo']
284+
z.attrs['foo'] = 42
285+
eq(42, z.attrs['foo'])
286+
z.attrs['bar'] = 4.2
287+
eq(4.2, z.attrs['bar'])
288+
z.attrs['baz'] = 'quux'
289+
eq('quux', z.attrs['baz'])
290+
279291

280292
class TestArray(TestCase, ArrayTests):
281293

@@ -322,6 +334,11 @@ def _test_persistence(self, a, chunks):
322334
# set data
323335
z[:] = a
324336

337+
# set attributes
338+
z.attrs['foo'] = 42
339+
z.attrs['bar'] = 4.2
340+
z.attrs['baz'] = 'quux'
341+
325342
# open for reading
326343
z2 = self.create_array(path=path, mode='r')
327344
eq(a.shape, z2.shape)
@@ -335,12 +352,17 @@ def _test_persistence(self, a, chunks):
335352
assert_array_equal(z.is_initialized, z2.is_initialized)
336353
assert_true(np.count_nonzero(z2.is_initialized) > 0)
337354
assert_array_equal(a, z2[:])
355+
eq(42, z2.attrs['foo'])
356+
eq(4.2, z2.attrs['bar'])
357+
eq('quux', z2.attrs['baz'])
338358

339359
# check read-only
340360
with assert_raises(ValueError):
341361
z2[:] = 0
342362
with assert_raises(ValueError):
343363
z2.resize(100)
364+
with assert_raises(ValueError):
365+
z2.attrs['foo'] = 0
344366

345367
# open for read/write if exists
346368
z3 = self.create_array(path=path, mode='r+')

0 commit comments

Comments
 (0)