From 2e5f6488aaa5e2da7413c4cf147b28a893d6d4cb Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 8 Sep 2026 20:58:01 -0400 Subject: [PATCH 1/5] [_833] test --- irods/test/meta_test.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/irods/test/meta_test.py b/irods/test/meta_test.py index 880bf1fe5..1b7bd5284 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -798,6 +798,35 @@ def test_prevention_of_attribute_creation__issue_795(self): # data.metadata(admin = True) generates a cloned object but for the one change to "admin". data.metadata.admin = True + def test_destickifying_of_admin_option__issue_833(self): + # Create a rodsuser, and a session for that roduser. + adm = self.sess + user = d = None + try: + user = adm.users.create("bobby", "rodsuser") + user.modify("password", "bpass") + sessions = [] + for _ in range(2): + with iRODSSession( + port=adm.port, + zone=adm.zone, + host=adm.host, + user=user.name, + password="bpass", + ) as ses: + # Create a data object owned by the rodsuser. Set AVUs in various ways and guarantee each attempt + # has the desired effect. + d = ses.data_objects.create("/{adm.zone}/home/{user.name}/testfile".format(**locals())) + if sessions: + d.metadata.set('a','b') + else: + d.metadata(admin=True) + sessions.append(ses) + finally: + if d: + d.unlink(force=True) + if user: + user.remove() if __name__ == "__main__": # let the tests find the parent irods lib From c571627b29f8d0589d1fef7157931d3e8d788306 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Tue, 8 Sep 2026 20:58:15 -0400 Subject: [PATCH 2/5] [_833] fix sticky admin keyword --- irods/manager/metadata_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 8cf239669..47fb13eb3 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -36,12 +36,13 @@ class MetadataManager(Manager): def __init__(self, *_): self._opts = _MetadataManager_opts_initializer.copy() super().__init__(*_) + self.__kw = self.__default_kw.copy() @property def use_timestamps(self): return self._opts['timestamps'] - __kw: Dict[str, Any] = {} # default (empty) keywords + __default_kw: Dict[str, Any] = {} # default (empty) keywords def _updated_keywords(self, opts): kw_ = self.__kw.copy() From 719820ee6bf45d3a4fa4c2ea1fa71f48dfc74629 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 9 Sep 2026 09:10:33 -0400 Subject: [PATCH 3/5] correct management of cached iRODS ADMIN_KW option --- irods/manager/metadata_manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 47fb13eb3..1dce117db 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -53,18 +53,20 @@ def get_api_keywords(self): return self.__kw.copy() def __call__(self, **flags): - # Make a new shallow copy of the manager object, but update options from parameter list. + # Make a new shallow copy of the manager object, but duplicate options from parameter list as well as iRODS API + # flags (stored in the instance's private __kw member) to be applied in each call. new_self = copy.copy(self) new_self._opts = copy.copy(self._opts) + new_self.__kw = copy.copy(self.__kw) # Update the flags that do bookkeeping in the returned(new) manager object. new_self._opts.update((key, val) for key, val in flags.items() if val is not None) - # Update the ADMIN_KW flag in the returned(new) object. + # For the new object, make ADMIN_KW flag or absence thereof reflect the admin option in _opts. if new_self._opts.get('admin'): - self.__kw[kw.ADMIN_KW] = "" + new_self.__kw[kw.ADMIN_KW] = "" else: - self.__kw.pop(kw.ADMIN_KW, None) + new_self.__kw.pop(kw.ADMIN_KW, None) return new_self From e48240c30e404201d2a12a098d805ef07c9e042f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 9 Sep 2026 10:06:07 -0400 Subject: [PATCH 4/5] test ADMIN_KW not sticky, ie option deep copies are working --- irods/test/meta_test.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/irods/test/meta_test.py b/irods/test/meta_test.py index 1b7bd5284..2c88aa517 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -806,6 +806,9 @@ def test_destickifying_of_admin_option__issue_833(self): user = adm.users.create("bobby", "rodsuser") user.modify("password", "bpass") sessions = [] + + import pdb;pdb.set_trace() + for _ in range(2): with iRODSSession( port=adm.port, @@ -814,14 +817,31 @@ def test_destickifying_of_admin_option__issue_833(self): user=user.name, password="bpass", ) as ses: - # Create a data object owned by the rodsuser. Set AVUs in various ways and guarantee each attempt - # has the desired effect. + # Get a reference to a data object owned by the rodsuser, creating it if not already there. d = ses.data_objects.create("/{adm.zone}/home/{user.name}/testfile".format(**locals())) - if sessions: - d.metadata.set('a','b') - else: + + # d.metadata is a different MetadataManager instance (and d, a different session instance) on the + # second iteration of the loop as compared to the first. Thus, admin flags should not carry over. + if not sessions: d.metadata(admin=True) + else: + # Should not be applying ADMIN_KW because INSUFFICIENT_PRIVILEGE_LEVEL exception would result + d.metadata.set('a','b') sessions.append(ses) + + # Admin option should be false after the second loop iteration. + self.assertFalse(d.metadata.admin) + + get_call_keywords = lambda metacoll: metacoll._manager._updated_keywords((),) + + # Applying admin=True should result in API flags containing ADMIN_KW among the lookup keys. + self.assertIn(kw.ADMIN_KW, get_call_keywords(md_modified:=d.metadata(admin=True))) + + # Admin option should be on in the object options bookkeeping. + self.assertTrue(md_modified.admin) + + # However, the unmodified source object should not reflect use of an ADMIN_KW. + self.assertNotIn(kw.ADMIN_KW, get_call_keywords(d.metadata)) # keyword updates not reflected in copied obj. finally: if d: d.unlink(force=True) From 5a84d0143778af01da32381f9111ed64e42cef16 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 10 Sep 2026 07:33:41 -0400 Subject: [PATCH 5/5] pdb statement cleanup, review suggestion --- irods/manager/metadata_manager.py | 3 ++- irods/test/meta_test.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/irods/manager/metadata_manager.py b/irods/manager/metadata_manager.py index 1dce117db..2007a77de 100644 --- a/irods/manager/metadata_manager.py +++ b/irods/manager/metadata_manager.py @@ -36,7 +36,8 @@ class MetadataManager(Manager): def __init__(self, *_): self._opts = _MetadataManager_opts_initializer.copy() super().__init__(*_) - self.__kw = self.__default_kw.copy() + # For the iRODS-api keywords only (currently ADMIN_KW is the sole one used): + self.__kw = {} @property def use_timestamps(self): diff --git a/irods/test/meta_test.py b/irods/test/meta_test.py index 2c88aa517..f2f0933f0 100644 --- a/irods/test/meta_test.py +++ b/irods/test/meta_test.py @@ -806,9 +806,6 @@ def test_destickifying_of_admin_option__issue_833(self): user = adm.users.create("bobby", "rodsuser") user.modify("password", "bpass") sessions = [] - - import pdb;pdb.set_trace() - for _ in range(2): with iRODSSession( port=adm.port,