Skip to content

Commit ba3c682

Browse files
committed
[_707] support for subclassing iRODSMeta
Example: iRODSBinOrStringMeta allows storing arbitrary octet strings in metadata. [_707] test of iRODSMeta subclass which stores bytestring value and/or unit This required some more corrections and tweaks of the iRODSMeta_type handling. [_707] iRODSMeta name could contain Unicode, ergo should not be included in translations [_707] do remove properly
1 parent c51532f commit ba3c682

File tree

3 files changed

+88
-23
lines changed

3 files changed

+88
-23
lines changed

irods/manager/metadata_manager.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ class InvalidAtomicAVURequest(Exception):
3030
class MetadataManager(Manager):
3131

3232
def __init__(self, *_):
33-
self._opts = {'admin':False, 'timestamps':False}
33+
self._opts = {
34+
'admin':False,
35+
'timestamps':False,
36+
'iRODSMeta_type':iRODSMeta
37+
}
3438
super().__init__(*_)
3539

3640
@property
@@ -39,6 +43,7 @@ def use_timestamps(self):
3943

4044
__kw : Dict[str, Any] = {} # default (empty) keywords
4145

46+
4247
def _updated_keywords(self, opts):
4348
kw_ = self.__kw.copy()
4449
kw_.update(opts)
@@ -115,9 +120,9 @@ def meta_opts(row):
115120
return opts
116121

117122
return [
118-
iRODSMeta(
119-
row[model.name], row[model.value], row[model.units], **meta_opts(row)
120-
)
123+
self._opts['iRODSMeta_type'](None,None,None)._from_column_triple(
124+
row[model.name], row[model.value], row[model.units],
125+
**meta_opts(row))
121126
for row in results
122127
]
123128

@@ -128,9 +133,7 @@ def add(self, model_cls, path, meta, **opts):
128133
"add",
129134
"-" + resource_type,
130135
path,
131-
meta.name,
132-
meta.value,
133-
meta.units,
136+
*meta._to_column_triple(),
134137
**self._updated_keywords(opts)
135138
)
136139
request = iRODSMessage(
@@ -147,9 +150,7 @@ def remove(self, model_cls, path, meta, **opts):
147150
"rm",
148151
"-" + resource_type,
149152
path,
150-
meta.name,
151-
meta.value,
152-
meta.units,
153+
*meta._to_column_triple(),
153154
**self._updated_keywords(opts)
154155
)
155156
request = iRODSMessage(
@@ -186,9 +187,7 @@ def set(self, model_cls, path, meta, **opts):
186187
"set",
187188
"-" + resource_type,
188189
path,
189-
meta.name,
190-
meta.value,
191-
meta.units,
190+
*meta._to_column_triple(),
192191
**self._updated_keywords(opts)
193192
)
194193
request = iRODSMessage(

irods/meta.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
1+
import base64
2+
import copy
3+
4+
15
class iRODSMeta:
26

7+
def _to_column_triple(self):
8+
return (self.name ,self.FX(self.value)) + (('',) if not self.units else (self.FX(self.units),))
9+
10+
def _from_column_triple(self, name, value, units, **kw):
11+
self.__low_level_init(name,
12+
self.RX(value),
13+
units=None if not units else self.RX(units),
14+
**kw)
15+
return self
16+
17+
RX = FX = staticmethod(lambda _:_)
18+
INIT_KW_ARGS = 'units avu_id create_time modify_time'.split()
19+
320
def __init__(
4-
self, name, value, units=None, avu_id=None, create_time=None, modify_time=None
21+
self, name, value, /, units=None, *, avu_id=None, create_time=None, modify_time=None,
522
):
6-
self.avu_id = avu_id
23+
# Defer initialization for iRODSMeta(attribute,value,...) if neither attribute nor value is True under
24+
# a 'bool' transformation. In so doing we streamline initialization for iRODSMeta (and any subclasses)
25+
# for alternatively populating via _from_column_triple(...).
26+
# This is the pathway for allowing user-defined encodings of the iRODSMeta (byte-)string AVU components.)
27+
if name or value:
28+
# Note: calling locals() inside the dict comprehension would not access variables in this frame.
29+
local_vars = locals()
30+
kw = {name:local_vars.get(name) for name in self.INIT_KW_ARGS}
31+
self.__low_level_init(name, value, **kw)
32+
33+
def __low_level_init(self, name, value, **kw):
734
self.name = name
835
self.value = value
9-
self.units = units
10-
self.create_time = create_time
11-
self.modify_time = modify_time
36+
for attr in self.INIT_KW_ARGS:
37+
setattr(self, attr, kw.get(attr))
1238

1339
def __eq__(self, other):
1440
return tuple(self) == tuple(other)
@@ -20,7 +46,20 @@ def __iter__(self):
2046
yield self.units
2147

2248
def __repr__(self):
23-
return "<iRODSMeta {avu_id} {name} {value} {units}>".format(**vars(self))
49+
return f"<{self.__class__.__name__} {self.avu_id} {self.name} {self.value} {self.units}>"
50+
51+
def __hash__(self):
52+
return hash(tuple(self))
53+
54+
class iRODSBinOrStringMeta(iRODSMeta):
55+
56+
@staticmethod
57+
def RX(value):
58+
return value if value[0] != '\\' else base64.decodebytes(value[1:].encode('utf8'))
59+
60+
@staticmethod
61+
def FX(value):
62+
return b'\\' + base64.encodebytes(value).strip() if isinstance(value,(bytes,bytearray)) else value
2463

2564

2665
class BadAVUOperationKeyword(Exception):
@@ -84,9 +123,6 @@ def __init__(self, operation, avu, **kw):
84123
setattr(self, atr, locals()[atr])
85124

86125

87-
import copy
88-
89-
90126
class iRODSMetaCollection:
91127

92128
def __call__(self, **opts):
@@ -138,7 +174,7 @@ def get_one(self, key):
138174
def _get_meta(self, *args):
139175
if not len(args):
140176
raise ValueError("Must specify an iRODSMeta object or key, value, units)")
141-
return args[0] if len(args) == 1 else iRODSMeta(*args)
177+
return args[0] if len(args) == 1 else self._manager._opts['iRODSMeta_type'](*args)
142178

143179
def apply_atomic_operations(self, *avu_ops):
144180
self._manager.apply_atomic_operations(self._model_cls, self._path, *avu_ops)

irods/test/meta_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
AVUOperation,
1515
BadAVUOperationValue,
1616
BadAVUOperationKeyword,
17+
iRODSBinOrStringMeta,
1718
)
1819
from irods.models import DataObject, Collection, Resource, CollectionMeta
1920
import irods.test.helpers as helpers
@@ -798,6 +799,35 @@ def test_xml_mode_addresses_odd_metadata_characters__issue_582(self):
798799
# in use, with the "odd" characters being present in the metadata value.
799800
del obj.metadata[attr_str]
800801

802+
def test_binary_avu_fields__issue_707(self):
803+
meta_coll = self.obj.metadata(iRODSMeta_type=iRODSBinOrStringMeta)
804+
illegal_unicode_sequence = '\u1000'.encode('utf8')[:2]
805+
avu_name = 'issue709'
806+
meta_coll.set(
807+
avu_name,
808+
(value:=b'value_'+illegal_unicode_sequence),
809+
(units:=b'units_'+illegal_unicode_sequence)
810+
)
811+
812+
self.assertEqual(
813+
meta_coll.get_one(avu_name),
814+
(avu_name, value, units)
815+
)
816+
meta_coll.add(*(new_avu:=iRODSMeta(avu_name, '\u1000', '\u1001')))
817+
all_relevant_avus = meta_coll.get_all(avu_name)
818+
self.assertIn(new_avu, all_relevant_avus)
819+
820+
# these could be in a separate test. TODO - make issue regarding hash and sets of iRODSMeta
821+
all_avus = meta_coll.items()
822+
self.assertLessEqual(
823+
set(all_relevant_avus),
824+
set(all_avus),
825+
)
826+
self.assertIn(
827+
(avu_name,
828+
(value:=b'value_'+illegal_unicode_sequence),
829+
(units:=b'units_'+illegal_unicode_sequence)), all_relevant_avus)
830+
801831
def test_cascading_changes_of_metadata_manager_options__issue_709(self):
802832
d = None
803833
get_option = lambda metacoll, key: metacoll._manager._opts[key]

0 commit comments

Comments
 (0)