From 2d88df732aad41847faa1e2101c2001a0efc7ebb Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 8 Feb 2026 02:20:41 -0500 Subject: [PATCH 1/2] [_795] allow options to be accessed as attrs of metadata obj --- README.md | 18 +++++++++++++++++- irods/manager/metadata_manager.py | 13 ++++++++----- irods/meta.py | 6 ++++++ irods/test/meta_test.py | 16 ++++++++-------- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 702541dc..7a4ff659 100644 --- a/README.md +++ b/README.md @@ -936,7 +936,7 @@ Disabling AVU reloads from the iRODS server With the default setting of `reload = True`, an `iRODSMetaCollection` will proactively read all current AVUs back from the iRODS server after any -metadata write done by the client. This helps methods such as `items()` +metadata write done by the client. This helps methods such as `keys()` and `items()` to return an up-to-date result. Setting `reload = False` can, however, greatly increase code efficiency if for example a lot of AVUs must be added or deleted at once without reading any back again. @@ -952,6 +952,22 @@ current_metadata = obj.metadata().items() print(f"{current_metadata = }") ``` +By way of explanation, please note that calls of the form +`obj.metadata([opt1=value1[,opt2=value2...]])` will always +produce new `iRODSMetaCollection` objects - which nevertheless share the same +session object as the original, as the copy is shallow in most respects. +This avoids always mutating the current instance and thus prevents any need to +implement context manager semantics when temporarily altering options such +as `reload` and `admin`. + +Additionally note that the call `obj.metadata()` without option parameters +always syncs the AVU list within the resulting `iRODSMetaCollection` object to +what is currently in the catalog, because the original object is unmutated with +respect to all options (meaning `obj.metadata.reload` is always `True`) -- that +is, absent any low-level meddling within reserved fields by the application. +Thus, `obj.metadata().items()` will always agree with the in-catalog AVU list +whereas `obj.metadata.items()` might not. + Subclassing `iRODSMeta` --------------------- The keyword option `iRODSMeta_type` can be used to set up any `iRODSMeta` diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index c09a6ab6..8723b465 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -27,14 +27,17 @@ class InvalidAtomicAVURequest(Exception): pass +_default_MetadataManager_opts = { + 'admin':False, + 'timestamps':False, + 'iRODSMeta_type':iRODSMeta, + 'reload':True +} + class MetadataManager(Manager): def __init__(self, *_): - self._opts = { - 'admin':False, - 'timestamps':False, - 'iRODSMeta_type':iRODSMeta - } + self._opts = _default_MetadataManager_opts.copy() super().__init__(*_) @property diff --git a/irods/meta.py b/irods/meta.py index 8ca94ae1..e2ac79af 100644 --- a/irods/meta.py +++ b/irods/meta.py @@ -131,6 +131,12 @@ def __init__(self, operation, avu, **kw): class iRODSMetaCollection: + def __getattr__(self,n): + from irods.manager.metadata_manager import _default_MetadataManager_opts + if n in _default_MetadataManager_opts: + return self._manager._opts[n] + raise AttributeError + def __call__(self, **opts): """ Optional parameters in **opts are: diff --git a/irods/test/meta_test.py b/irods/test/meta_test.py index cd1c3abd..1ce9eb97 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -820,24 +820,24 @@ def test_binary_avu_fields__issue_707(self): def test_cascading_changes_of_metadata_manager_options__issue_709(self): d = None - def get_option(metacoll, key): - return metacoll._manager._opts[key] +# def get_option(metacoll, key): +# return metacoll._manager._opts[key] try: d = self.sess.data_objects.create(f'{self.coll.path}/issue_709_test_1') m = d.metadata - self.assertEqual(get_option(m, 'admin'), False) + self.assertEqual(m.admin, False) m2 = m(admin=True) - self.assertEqual(get_option(m2, 'timestamps'), False) - self.assertEqual(get_option(m2, 'admin'), True) + self.assertEqual(m2.timestamps, False) + self.assertEqual(m2.admin, True) m3 = m2(timestamps=True) - self.assertEqual(get_option(m3, 'timestamps'), True) - self.assertEqual(get_option(m3, 'admin'), True) + self.assertEqual(m3.timestamps, True) + self.assertEqual(m3.admin, True) self.assertEqual(m3._manager.get_api_keywords().get(kw.ADMIN_KW), "") m4 = m3(admin=False) - self.assertEqual(get_option(m4, 'admin'), False) + self.assertEqual(m4.admin, False) self.assertEqual(m4._manager.get_api_keywords().get(kw.ADMIN_KW), None) finally: if d: From 2a6957d0d7b86b5cb5a9b655ce70b446eacec843 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 11 Feb 2026 00:23:27 -0500 Subject: [PATCH 2/2] explain getattr problems and solution --- irods/manager/metadata_manager.py | 4 ++-- irods/meta.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 8723b465..c8ddbcd6 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -27,7 +27,7 @@ class InvalidAtomicAVURequest(Exception): pass -_default_MetadataManager_opts = { +_MetadataManager_opts_initializer = { 'admin':False, 'timestamps':False, 'iRODSMeta_type':iRODSMeta, @@ -37,7 +37,7 @@ class InvalidAtomicAVURequest(Exception): class MetadataManager(Manager): def __init__(self, *_): - self._opts = _default_MetadataManager_opts.copy() + self._opts = _MetadataManager_opts_initializer.copy() super().__init__(*_) @property diff --git a/irods/meta.py b/irods/meta.py index e2ac79af..8f423497 100644 --- a/irods/meta.py +++ b/irods/meta.py @@ -132,8 +132,13 @@ def __init__(self, operation, avu, **kw): class iRODSMetaCollection: def __getattr__(self,n): - from irods.manager.metadata_manager import _default_MetadataManager_opts - if n in _default_MetadataManager_opts: + from irods.manager.metadata_manager import _MetadataManager_opts_initializer + # The remove of _MetadataManager_opts_initializer is to prevent the possibility of arbitrary access + # by copy.copy() to parts of our object's state before they have been initialized, as it is known + # to do by calling hasattr on the "__setstate__" attribute. + # The result of such unfettered access is infinite recursion. See: + # https://nedbatchelder.com/blog/201010/surprising_getattr_recursion + if n in _MetadataManager_opts_initializer: return self._manager._opts[n] raise AttributeError