Skip to content

Commit 8b27a1d

Browse files
[_795,SQUASH] allow options to be accessed as attrs of metadata obj
Co-authored-by: Kory Draughn <korydraughn@ymail.com>
1 parent 2ecc3a3 commit 8b27a1d

File tree

4 files changed

+97
-14
lines changed

4 files changed

+97
-14
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ Disabling AVU reloads from the iRODS server
936936

937937
With the default setting of `reload = True`, an `iRODSMetaCollection` will
938938
proactively read all current AVUs back from the iRODS server after any
939-
metadata write done by the client. This helps methods such as `items()`
939+
metadata write done by the client. This helps methods such as `keys()` and `items()`
940940
to return an up-to-date result. Setting `reload = False` can, however, greatly
941941
increase code efficiency if for example a lot of AVUs must be added or deleted
942942
at once without reading any back again.
@@ -952,6 +952,22 @@ current_metadata = obj.metadata().items()
952952
print(f"{current_metadata = }")
953953
```
954954

955+
By way of explanation, please note that calls of the form
956+
`obj.metadata([opt1=value1[,opt2=value2...]])` will always
957+
produce new `iRODSMetaCollection` objects - which nevertheless share the same
958+
session object as the original, as the copy is shallow in most respects.
959+
This avoids always mutating the current instance and thus prevents any need to
960+
implement context manager semantics when temporarily altering options such
961+
as `reload` and `admin`.
962+
963+
Additionally note that the call `obj.metadata()` without option parameters
964+
always syncs the AVU list within the resulting `iRODSMetaCollection` object to
965+
what is currently in the catalog, because the original object is unmutated with
966+
respect to all options (meaning `obj.metadata.reload` is always `True`) -- that
967+
is, absent any low-level meddling within reserved fields by the application.
968+
Thus, `obj.metadata().items()` will always agree with the in-catalog AVU list
969+
whereas `obj.metadata.items()` might not.
970+
955971
Subclassing `iRODSMeta`
956972
---------------------
957973
The keyword option `iRODSMeta_type` can be used to set up any `iRODSMeta`

irods/manager/metadata_manager.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ class InvalidAtomicAVURequest(Exception):
2727
pass
2828

2929

30+
# This was necessarily made separate from the MetadataManager definition
31+
# in order to avoid infinite recursion in iRODSMetaCollection.__getattr__
32+
_MetadataManager_opts_initializer = {'admin': False, 'timestamps': False, 'iRODSMeta_type': iRODSMeta, 'reload': True}
33+
34+
3035
class MetadataManager(Manager):
3136

3237
def __init__(self, *_):
33-
self._opts = {
34-
'admin':False,
35-
'timestamps':False,
36-
'iRODSMeta_type':iRODSMeta
37-
}
38+
self._opts = _MetadataManager_opts_initializer.copy()
3839
super().__init__(*_)
3940

4041
@property

irods/meta.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,64 @@ def __init__(self, operation, avu, **kw):
131131

132132

133133
class iRODSMetaCollection:
134+
def __setattr__(self, name, value):
135+
"""
136+
Override __setattr__.
137+
138+
Protect the virtual, read-only attributes such as 'admin', 'timestamps', etc.,
139+
from being written or created as concrete attributes, which would interfere with
140+
__getattr__'s intended operation for these cases.
141+
142+
Args:
143+
name: the name of the attribute to be written.
144+
value: the value to be written to the attribute.
145+
146+
Raises:
147+
AttributeError: on any attempt to write to these special attributes.
148+
"""
149+
from irods.manager.metadata_manager import _MetadataManager_opts_initializer
150+
151+
if name in _MetadataManager_opts_initializer:
152+
msg = (
153+
f"""The "{name}" attribute is a special one, settable only via a """
154+
f"""call on the object. For example: admin_view = data_obj.metadata({name}=<value>)"""
155+
)
156+
raise AttributeError(msg)
157+
158+
super().__setattr__(name, value)
159+
160+
def __getattr__(self, name):
161+
"""
162+
Override __getattr__.
163+
164+
Expose certain settable flags (e.g. "admin", "timestamps") as virtual, read-only
165+
"attributes." The names of these special attributes appear as the keys of the
166+
_MetadataManager_opts_initializer dictionary.
167+
168+
Args:
169+
name: the name of the attribute to be fetched.
170+
171+
Returns:
172+
the value of the named attribute.
173+
174+
Raises:
175+
AttributeError: because this is the protocol for deferring to __getattr__'s
176+
default behavior for the case in which none of the special attribute keys are
177+
a match for 'name'.
178+
"""
179+
from irods.manager.metadata_manager import _MetadataManager_opts_initializer
180+
181+
# Separating _MetadataManager_opts_initializer from the MetadataManager class
182+
# prevents the possibility of arbitrary access by copy.copy() to parts of
183+
# our object's state before they have been initialized, as it is known to do
184+
# by calling hasattr on the "__setstate__" attribute. The result of such
185+
# unfettered access is infinite recursion. See:
186+
# https://nedbatchelder.com/blog/201010/surprising_getattr_recursion
187+
188+
if name in _MetadataManager_opts_initializer:
189+
return self._manager._opts[name] # noqa: SLF001
190+
raise AttributeError
191+
134192
def __call__(self, **opts):
135193
"""
136194
Optional parameters in **opts are:

irods/test/meta_test.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
iRODSMeta,
2222
)
2323
from irods.models import Collection, CollectionMeta, DataObject, ModelBase, Resource
24+
from irods.path import iRODSPath
2425
from irods.session import iRODSSession
2526
from irods.test import helpers
2627

@@ -820,24 +821,22 @@ def test_binary_avu_fields__issue_707(self):
820821
def test_cascading_changes_of_metadata_manager_options__issue_709(self):
821822
d = None
822823

823-
def get_option(metacoll, key):
824-
return metacoll._manager._opts[key]
825824
try:
826825
d = self.sess.data_objects.create(f'{self.coll.path}/issue_709_test_1')
827826
m = d.metadata
828-
self.assertEqual(get_option(m, 'admin'), False)
827+
self.assertEqual(m.admin, False)
829828

830829
m2 = m(admin=True)
831-
self.assertEqual(get_option(m2, 'timestamps'), False)
832-
self.assertEqual(get_option(m2, 'admin'), True)
830+
self.assertEqual(m2.timestamps, False)
831+
self.assertEqual(m2.admin, True)
833832

834833
m3 = m2(timestamps=True)
835-
self.assertEqual(get_option(m3, 'timestamps'), True)
836-
self.assertEqual(get_option(m3, 'admin'), True)
834+
self.assertEqual(m3.timestamps, True)
835+
self.assertEqual(m3.admin, True)
837836
self.assertEqual(m3._manager.get_api_keywords().get(kw.ADMIN_KW), "")
838837

839838
m4 = m3(admin=False)
840-
self.assertEqual(get_option(m4, 'admin'), False)
839+
self.assertEqual(m4.admin, False)
841840
self.assertEqual(m4._manager.get_api_keywords().get(kw.ADMIN_KW), None)
842841
finally:
843842
if d:
@@ -863,6 +862,15 @@ def test_reload_can_be_deactivated__issue_768(self):
863862
self.assertIn(item_1, items_reloaded)
864863
self.assertIn(item_2, items_reloaded)
865864

865+
def test_prevention_of_attribute_creation__issue_795(self):
866+
data_path = iRODSPath(self.coll_path, helpers.unique_name(datetime.datetime.now())) # noqa: DTZ005
867+
data = self.sess.data_objects.create(data_path)
868+
with self.assertRaises(AttributeError):
869+
# This should cause an error since "admin" is considered as a read-only attribute; whereas
870+
# data.metadata(admin = True) generates a cloned object but for the one change to "admin".
871+
data.metadata.admin = True
872+
873+
866874
if __name__ == "__main__":
867875
# let the tests find the parent irods lib
868876
sys.path.insert(0, os.path.abspath("../.."))

0 commit comments

Comments
 (0)