From 7a811847ec02bdbc3bc5e8adc7913cb8e03067cd Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 20 Mar 2026 19:06:28 -0400 Subject: [PATCH 01/40] add SYS_INTERNAL_ERR --- irods/exception.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irods/exception.py b/irods/exception.py index b9551fdc1..4dbbd66de 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -650,6 +650,10 @@ class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 +class SYS_INTERNAL_ERR(SystemException): + code = -154000 + + class SYS_BAD_INPUT(iRODSException): code = -158000 From 228bf860d906d6bde877b13fb6faf747de54dd12 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 19 Mar 2026 02:40:53 -0400 Subject: [PATCH 02/40] [_505,sq] atomic ACLs endpoint --- irods/access.py | 5 ++++- irods/manager/access_manager.py | 30 +++++++++++++++++++++++++- irods/test/access_test.py | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 465585ddc..3c0d206d6 100644 --- a/irods/access.py +++ b/irods/access.py @@ -102,7 +102,7 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) - def copy(self, decanonicalize=False): + def copy(self, decanonicalize=False, ref_zone=''): other = copy.deepcopy(self) if decanonicalize: replacement_string = { @@ -112,6 +112,9 @@ def copy(self, decanonicalize=False): "modify_object": "write", }.get(self.access_name) other.access_name = replacement_string if replacement_string is not None else self.access_name + if '' != ref_zone == other.user_zone: + other.user_zone = '' + return other def __repr__(self): diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index bf32dc283..58cec8100 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -2,7 +2,7 @@ from irods.manager import Manager from irods.api_number import api_number -from irods.message import ModAclRequest, iRODSMessage +from irods.message import ModAclRequest, iRODSMessage, JSON_Message from irods.data_object import iRODSDataObject, irods_dirname, irods_basename from irods.collection import iRODSCollection from irods.models import ( @@ -14,6 +14,7 @@ CollectionAccess, ) from irods.access import iRODSAccess +import irods.exception as ex from irods.column import In from irods.user import iRODSUser @@ -36,6 +37,33 @@ def users_by_ids(session, ids=()): class AccessManager(Manager): + + def _ACL_operation(self, op_input: iRODSAccess): + return { + "acl": op_input.access_name, + "entity_name": op_input.user_name, + **( + {} if not (z := op_input.user_zone) + else {"zone": z} + ) + } + + def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): + request_text = {"logical_path": logical_path} + request_text["admin_mode"] = admin + request_text["operations"] = [self._ACL_operation(op) for op in operations] + + with self.sess.pool.get_connection() as conn: + request_msg = iRODSMessage( + "RODS_API_REQ", + JSON_Message(request_text, conn.server_version), + int_info=20005, + ) + conn.send(request_msg) + response = conn.recv() + response_msg = response.get_json_encoded_struct() + logger.debug("in atomic ACL api, server responded with: %r", response_msg) + def get(self, target, report_raw_acls=True, **kw): if report_raw_acls: diff --git a/irods/test/access_test.py b/irods/test/access_test.py index fadc6a7dc..5bf397d87 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -497,6 +497,43 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel self.sess, ) + def test_atomic_acls_505(self): + #import pdb;pdb.set_trace() + ses = self.sess + zone = user1 = user2 = group = None + try: + zone = ses.zones.create("twilight","remote") + user1 = ses.users.create("test_user_505", "rodsuser") + user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") + group = ses.groups.create("test_group_505") + ses.acls._call_atomic_acl_api( + self.coll_path, + a1:=iRODSAccess("write", "", user1.name, user1.zone), + a2:=iRODSAccess("read", "", user2.name, user2.zone), + a3:=iRODSAccess("read", "", group.name), + ) + + accesses = ses.acls.get(self.coll) + + # For purposes of equality tests, assign the path name of interest into each ACL. + for p in (a1, a2, a3): + p.path = self.coll_path + + # Assert that the ACLs we added are among those listed for the object in the catalog. + normalize = lambda access: access.copy(decanonicalize=True, ref_zone=ses.zone) + self.assertLess( + set(normalize(_) for _ in (a1,a2,a3)), + set(normalize(_) for _ in accesses) + ) + finally: + if user1: + user1.remove() + if user2: + user2.remove() + if group: + group.remove() + if zone: + zone.remove() if __name__ == "__main__": # let the tests find the parent irods lib From fb6e37366a48fea4914449a972ab2ef8c157af59 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sat, 21 Mar 2026 00:45:03 -0400 Subject: [PATCH 03/40] misc corrections / ruff lint and format --- README.md | 28 +++++++++ irods/access.py | 102 ++++++++++++++++++++++++++------ irods/api_number.py | 1 + irods/manager/access_manager.py | 22 ++++--- irods/test/access_test.py | 36 +++++------ 5 files changed, 141 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 5bd193205..dc3aed6a4 100644 --- a/README.md +++ b/README.md @@ -2118,6 +2118,34 @@ membership, this can be achieved with another query. `.permissions` was therefore removed in v2.0.0 in favor of `.acls`. +Atomic ACLs +----------- + +A list of permissions may be added to an object atomically using +the AccessManager's apply_atomic_operations method: +``` +from irods.access import ACLOperation +from irods.helpers import home_collection +session = irods.helpers.make_session() +myCollection = session.collections.create(f"{home_collection(session).path}/newCollection") + +session.acls.apply_atomic_operations(myCollection.path, + *[ACLOperation("read", "public"), + ACLOperation("write", "bob", "otherZone") + ]) +``` +ACLOperation objects form a linear order with iRODSAccess objects, and +indeed are subclassed from them as well, allowing equivalency testing: + +Thus, for example: +``` +ACLOperation('read','public') in sess.acls.get(object) +``` +is a valid operation. Consequently, any client application that habitually +caches object permissions could use similar code to check new ACLOperations against the cache +and conceivably be able to optimize size of an atomic ACLs request by eliminating +any ACLOperations that might have been redundant. + Quotas (v2.0.0) --------------- diff --git a/irods/access.py b/irods/access.py index 3c0d206d6..e2d9f5e24 100644 --- a/irods/access.py +++ b/irods/access.py @@ -5,6 +5,22 @@ from irods.path import iRODSPath +_ichmod_listed_permissions = ( + "own", + "delete_object", + "write", + "modify_object", + "create_object", + "delete_metadata", + "modify_metadata", + "create_metadata", + "read", + "read_object", + "read_metadata", + "null", +) + + class _Access_LookupMeta(type): def __getitem__(self, key): return self.codes[key] @@ -28,6 +44,7 @@ def to_int(cls, key): def to_string(cls, key): return cls.strings[key] + # noqa: RUF012 - Cannot change in minor release codes = collections.OrderedDict( (key_, value_) for key_, value_ in sorted( @@ -55,24 +72,10 @@ def to_string(cls, key): ).items(), key=lambda _: _[1], ) - if key_ - in ( - # These are copied from ichmod help text. - "own", - "delete_object", - "write", - "modify_object", - "create_object", - "delete_metadata", - "modify_metadata", - "create_metadata", - "read", - "read_object", - "read_metadata", - "null", - ) + if key_ in _ichmod_listed_permissions ) + # noqa: RUF012 - Cannot change in minor release strings = collections.OrderedDict((number, string) for string, number in codes.items()) def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): @@ -91,6 +94,14 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None self.user_zone = user_zone self.user_type = user_type + def __lt__(self, other): + return (self.access_name, self.user_name, self.user_zone, iRODSPath(self.path)) < ( + other.access_name, + other.user_name, + other.user_zone, + iRODSPath(other.path), + ) + def __eq__(self, other): return ( self.access_name == other.access_name @@ -102,8 +113,9 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) - def copy(self, decanonicalize=False, ref_zone=''): + def copy(self, decanonicalize=False, implied_zone=''): other = copy.deepcopy(self) + if decanonicalize: replacement_string = { "read object": "read", @@ -112,8 +124,10 @@ def copy(self, decanonicalize=False, ref_zone=''): "modify_object": "write", }.get(self.access_name) other.access_name = replacement_string if replacement_string is not None else self.access_name - if '' != ref_zone == other.user_zone: - other.user_zone = '' + + # Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for equality testing: + if '' != implied_zone == other.user_zone: + other.user_zone = '' return other @@ -124,6 +138,56 @@ def __repr__(self): return f"" +class ACLOperation(iRODSAccess): + def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): + super().__init__( + access_name=access_name, + path="", + user_name=user_name, + user_zone=user_zone, + ) + + def __eq__(self, other): + return ( + self.access_name, + self.user_name, + self.user_zone, + ) == ( + other.access_name, + other.user_name, + other.user_zone, + ) + + def __lt__(self, other): + return ( + self.access_name, + self.user_name, + self.user_zone, + ) < ( + other.access_name, + other.user_name, + other.user_zone, + ) + + def __repr__(self): + return f"" + + +( + _ichmod_synonym_mapping := { + # syn : canonical + "write": "modify_object", + "read": "read_object", + } +).update((key.replace("_", " "), key) for key in iRODSAccess.codes.keys()) + + +all_permissions = { + **iRODSAccess.codes, + **{key: iRODSAccess.codes[_ichmod_synonym_mapping[key]] for key in _ichmod_synonym_mapping}, +} + + class _iRODSAccess_pre_4_3_0(iRODSAccess): codes = collections.OrderedDict( (key.replace("_", " "), value) diff --git a/irods/api_number.py b/irods/api_number.py index fe614ffce..03ac3de86 100644 --- a/irods/api_number.py +++ b/irods/api_number.py @@ -177,6 +177,7 @@ "ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002, "GET_FILE_DESCRIPTOR_INFO_APN": 20000, "REPLICA_CLOSE_APN": 20004, + "ATOMIC_APPLY_ACL_OPERATIONS_APN": 20005, "TOUCH_APN": 20007, "AUTH_PLUG_REQ_AN": 1201, "AUTHENTICATION_APN": 110000, diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 58cec8100..d63e5e2d8 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -14,7 +14,6 @@ CollectionAccess, ) from irods.access import iRODSAccess -import irods.exception as ex from irods.column import In from irods.user import iRODSUser @@ -37,27 +36,26 @@ def users_by_ids(session, ids=()): class AccessManager(Manager): - - def _ACL_operation(self, op_input: iRODSAccess): + @staticmethod + def _to_acl_operation_json(op_input: iRODSAccess): return { "acl": op_input.access_name, "entity_name": op_input.user_name, - **( - {} if not (z := op_input.user_zone) - else {"zone": z} - ) + **({} if not (z := op_input.user_zone) else {"zone": z}), } - def _call_atomic_acl_api(self, logical_path : str, *operations, admin=False): - request_text = {"logical_path": logical_path} - request_text["admin_mode"] = admin - request_text["operations"] = [self._ACL_operation(op) for op in operations] + def apply_atomic_operations(self, logical_path: str, *operations, admin=False): + request_text = { + "logical_path": logical_path, + "admin_mode": admin, + "operations": [self._to_acl_operation_json(op) for op in operations], + } with self.sess.pool.get_connection() as conn: request_msg = iRODSMessage( "RODS_API_REQ", JSON_Message(request_text, conn.server_version), - int_info=20005, + int_info=api_number["ATOMIC_APPLY_ACL_OPERATIONS_APN"], ) conn.send(request_msg) response = conn.recv() diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 5bf397d87..3c65566e6 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -4,7 +4,7 @@ import sys import unittest -from irods.access import iRODSAccess +from irods.access import iRODSAccess, ACLOperation from irods.collection import iRODSCollection from irods.column import In, Like from irods.exception import UserDoesNotExist @@ -498,43 +498,45 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel ) def test_atomic_acls_505(self): - #import pdb;pdb.set_trace() ses = self.sess - zone = user1 = user2 = group = None + zone = user1 = user2 = user3 = group = None try: - zone = ses.zones.create("twilight","remote") + zone = ses.zones.create("twilight", "remote") user1 = ses.users.create("test_user_505", "rodsuser") user2 = ses.users.create("rod_serling_505#twilight", "rodsuser") + user3 = ses.users.create("local_test_user_505", "rodsuser") group = ses.groups.create("test_group_505") - ses.acls._call_atomic_acl_api( + ses.acls.apply_atomic_operations( self.coll_path, - a1:=iRODSAccess("write", "", user1.name, user1.zone), - a2:=iRODSAccess("read", "", user2.name, user2.zone), - a3:=iRODSAccess("read", "", group.name), + a1:=ACLOperation("write", user1.name, user1.zone), + a2:=ACLOperation("read", user2.name, user2.zone), + a3:=ACLOperation("read", user3.name), + a4:=ACLOperation("read", group.name), ) - accesses = ses.acls.get(self.coll) + normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) - # For purposes of equality tests, assign the path name of interest into each ACL. - for p in (a1, a2, a3): - p.path = self.coll_path + accesses = [normalize(acl) for acl in ses.acls.get(self.coll)] # Assert that the ACLs we added are among those listed for the object in the catalog. - normalize = lambda access: access.copy(decanonicalize=True, ref_zone=ses.zone) - self.assertLess( - set(normalize(_) for _ in (a1,a2,a3)), - set(normalize(_) for _ in accesses) - ) + self.assertIn(normalize(a1), accesses) + self.assertIn(normalize(a2), accesses) + self.assertIn(normalize(a3), accesses) + self.assertIn(normalize(a4), accesses) + finally: if user1: user1.remove() if user2: user2.remove() + if user3: + user3.remove() if group: group.remove() if zone: zone.remove() + if __name__ == "__main__": # let the tests find the parent irods lib sys.path.insert(0, os.path.abspath("../..")) From 1b2e7e1eb479250b3423a22a3157342593d2942b Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 26 Mar 2026 22:33:17 -0400 Subject: [PATCH 04/40] convert "codes" and "strings" into properties of the class to prevent casual corruption. RUF012 points out that instances of the class can casually modify an unmutable class variable, eg.: class A: value = [] def f(self,*y): self.value += [*y] --- irods/access.py | 60 ++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/irods/access.py b/irods/access.py index e2d9f5e24..332c7734d 100644 --- a/irods/access.py +++ b/irods/access.py @@ -22,30 +22,10 @@ class _Access_LookupMeta(type): - def __getitem__(self, key): - return self.codes[key] - - def keys(self): - return list(self.codes.keys()) - - def values(self): - return list(self.codes[k] for k in self.codes.keys()) - - def items(self): - return list(zip(self.keys(), self.values())) - -class iRODSAccess(metaclass=_Access_LookupMeta): - @classmethod - def to_int(cls, key): - return cls.codes[key] - - @classmethod - def to_string(cls, key): - return cls.strings[key] - - # noqa: RUF012 - Cannot change in minor release - codes = collections.OrderedDict( + @staticmethod + def _codes(): + return collections.OrderedDict( (key_, value_) for key_, value_ in sorted( dict( @@ -75,10 +55,40 @@ def to_string(cls, key): if key_ in _ichmod_listed_permissions ) - # noqa: RUF012 - Cannot change in minor release - strings = collections.OrderedDict((number, string) for string, number in codes.items()) + @property + def codes(metaclass_target): return metaclass_target._codes() + + @property + def strings(metaclass_target): + return collections.OrderedDict((number, string) for string, number in + metaclass_target._codes().items()) + + def __getitem__(self, key): + return self.codes[key] + + def keys(self): + return list(self.codes.keys()) + + def values(self): + return list(self.codes[k] for k in self.codes.keys()) + + def items(self): + return list(zip(self.keys(), self.values())) + + +class iRODSAccess(metaclass=_Access_LookupMeta): + @classmethod + def to_int(cls, key): + return cls.codes[key] + + @classmethod + def to_string(cls, key): + return cls.strings[key] + def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): + self.codes = self.__class__.codes.copy() + self.strings = self.__class__.strings.copy() self.access_name = access_name if isinstance(path, (iRODSCollection, iRODSDataObject)): self.path = path.path From 32f6a24e7712ebaa0b8c1f30e7ca8074c8399013 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 27 Mar 2026 05:20:13 -0400 Subject: [PATCH 05/40] ruff related changes --- README.md | 7 ++-- irods/access.py | 76 ++++++++++++++++++++------------------- irods/test/access_test.py | 11 +++--- 3 files changed, 49 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index dc3aed6a4..79c1d3fa8 100644 --- a/README.md +++ b/README.md @@ -2141,10 +2141,9 @@ Thus, for example: ``` ACLOperation('read','public') in sess.acls.get(object) ``` -is a valid operation. Consequently, any client application that habitually -caches object permissions could use similar code to check new ACLOperations against the cache -and conceivably be able to optimize size of an atomic ACLs request by eliminating -any ACLOperations that might have been redundant. +is a valid operation, so that an application that tends to cache object +permissions client-side might use such checks in optimizing atomic ACL +requests against the inclusion of any redundant ACLOperations. Quotas (v2.0.0) --------------- diff --git a/irods/access.py b/irods/access.py index 332c7734d..3b2a58225 100644 --- a/irods/access.py +++ b/irods/access.py @@ -26,42 +26,42 @@ class _Access_LookupMeta(type): @staticmethod def _codes(): return collections.OrderedDict( - (key_, value_) - for key_, value_ in sorted( - dict( - # copied from iRODS source code in - # ./server/core/include/irods/catalog_utilities.hpp: - null=1000, - execute=1010, - read_annotation=1020, - read_system_metadata=1030, - read_metadata=1040, - read_object=1050, - write_annotation=1060, - create_metadata=1070, - modify_metadata=1080, - delete_metadata=1090, - administer_object=1100, - create_object=1110, - modify_object=1120, - delete_object=1130, - create_token=1140, - delete_token=1150, - curate=1160, - own=1200, - ).items(), - key=lambda _: _[1], + (key_, value_) + for key_, value_ in sorted( + dict( + # copied from iRODS source code in + # ./server/core/include/irods/catalog_utilities.hpp: + null=1000, + execute=1010, + read_annotation=1020, + read_system_metadata=1030, + read_metadata=1040, + read_object=1050, + write_annotation=1060, + create_metadata=1070, + modify_metadata=1080, + delete_metadata=1090, + administer_object=1100, + create_object=1110, + modify_object=1120, + delete_object=1130, + create_token=1140, + delete_token=1150, + curate=1160, + own=1200, + ).items(), + key=lambda _: _[1], + ) + if key_ in _ichmod_listed_permissions ) - if key_ in _ichmod_listed_permissions - ) @property - def codes(metaclass_target): return metaclass_target._codes() + def codes(metaclass_target): + return metaclass_target._codes() @property def strings(metaclass_target): - return collections.OrderedDict((number, string) for string, number in - metaclass_target._codes().items()) + return collections.OrderedDict((number, string) for string, number in metaclass_target._codes().items()) def __getitem__(self, key): return self.codes[key] @@ -76,7 +76,7 @@ def items(self): return list(zip(self.keys(), self.values())) -class iRODSAccess(metaclass=_Access_LookupMeta): +class _iRODSAccess_base: @classmethod def to_int(cls, key): return cls.codes[key] @@ -85,10 +85,7 @@ def to_int(cls, key): def to_string(cls, key): return cls.strings[key] - - def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): - self.codes = self.__class__.codes.copy() - self.strings = self.__class__.strings.copy() + def __init__(self, access_name, path, user_name, user_zone, user_type): self.access_name = access_name if isinstance(path, (iRODSCollection, iRODSDataObject)): self.path = path.path @@ -148,6 +145,13 @@ def __repr__(self): return f"" +class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta): + def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): + self.codes = self.__class__.codes + self.strings = self.__class__.strings + super().__init__(access_name, path, user_name, user_zone, user_type) + + class ACLOperation(iRODSAccess): def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): super().__init__( @@ -198,7 +202,7 @@ def __repr__(self): } -class _iRODSAccess_pre_4_3_0(iRODSAccess): +class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): codes = collections.OrderedDict( (key.replace("_", " "), value) for key, value in iRODSAccess.codes.items() diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 3c65566e6..46ecf5a49 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -508,13 +508,14 @@ def test_atomic_acls_505(self): group = ses.groups.create("test_group_505") ses.acls.apply_atomic_operations( self.coll_path, - a1:=ACLOperation("write", user1.name, user1.zone), - a2:=ACLOperation("read", user2.name, user2.zone), - a3:=ACLOperation("read", user3.name), - a4:=ACLOperation("read", group.name), + a1 := ACLOperation("write", user1.name, user1.zone), + a2 := ACLOperation("read", user2.name, user2.zone), + a3 := ACLOperation("read", user3.name), + a4 := ACLOperation("read", group.name), ) - normalize = lambda access: access.copy(decanonicalize=True, implied_zone=ses.zone) + def normalize(access): + return access.copy(decanonicalize=True, implied_zone=ses.zone) accesses = [normalize(acl) for acl in ses.acls.get(self.coll)] From e373423b5f2cfa9408c8362e1dd60bc17963edfb Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 27 Mar 2026 14:30:09 -0400 Subject: [PATCH 06/40] [_809] deprecate class for storing permission codes pre-iRODS-4.3 --- irods/access.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/irods/access.py b/irods/access.py index 3b2a58225..e830bc454 100644 --- a/irods/access.py +++ b/irods/access.py @@ -1,5 +1,7 @@ import collections import copy +import warnings + from irods.collection import iRODSCollection from irods.data_object import iRODSDataObject from irods.path import iRODSPath @@ -202,10 +204,26 @@ def __repr__(self): } -class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): - codes = collections.OrderedDict( - (key.replace("_", " "), value) - for key, value in iRODSAccess.codes.items() - if key in ("own", "write", "modify_object", "read", "read_object", "null") - ) - strings = collections.OrderedDict((number, string) for string, number in codes.items()) +class _deprecated: + class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): + codes = collections.OrderedDict( + (key.replace("_", " "), value) + for key, value in iRODSAccess.codes.items() + if key in ("own", "write", "modify_object", "read", "read_object", "null") + ) + strings = collections.OrderedDict((number, string) for string, number in codes.items()) + def __init__(self, *args, **kwargs): + warnings.warn( + "_iRODSAccess_pre_4_3_0 is deprecated and will be removed in a future version. Use iRODSAccess instead.", + DeprecationWarning, + stacklevel=2 + ) + super().__init__(*args,**kwargs) + +_deprecated_names = {'_iRODSAccess_pre_4_3_0':_deprecated._iRODSAccess_pre_4_3_0} + +def __getattr__(name): + if name in _deprecated_names: + warnings.warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2) + return _deprecated_names[name] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 642182956f8cec0b23a0aa195817978db702ac03 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Fri, 27 Mar 2026 14:46:43 -0400 Subject: [PATCH 07/40] README updates --- README.md | 46 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 79c1d3fa8..4fc299024 100644 --- a/README.md +++ b/README.md @@ -2122,28 +2122,54 @@ Atomic ACLs ----------- A list of permissions may be added to an object atomically using -the AccessManager's apply_atomic_operations method: -``` +the AccessManager's `apply_atomic_operations` method: +```py from irods.access import ACLOperation from irods.helpers import home_collection session = irods.helpers.make_session() myCollection = session.collections.create(f"{home_collection(session).path}/newCollection") -session.acls.apply_atomic_operations(myCollection.path, - *[ACLOperation("read", "public"), - ACLOperation("write", "bob", "otherZone") - ]) +session.acls.apply_atomic_operations( + myCollection.path, + *[ + ACLOperation("read", "public"), + ACLOperation("write", "bob", "otherZone") + ] +) ``` -ACLOperation objects form a linear order with iRODSAccess objects, and -indeed are subclassed from them as well, allowing equivalency testing: +`ACLOperation` objects form a linear order with `iRODSAccess` objects, and +indeed are subclassed from them as well, allowing equivalency testing. Thus, for example: -``` +```py ACLOperation('read','public') in sess.acls.get(object) ``` is a valid operation, so that an application that tends to cache object permissions client-side might use such checks in optimizing atomic ACL -requests against the inclusion of any redundant ACLOperations. +requests against the inclusion of any redundant ACL operations. + +For purposes of sorting, a `__lt__` operator is also defined that would +allow sorting of lists of `iRODSAccess`, `ACLOperations`, or the two intermixed: +```py +perms_list=[ + ACLOperation('read', 'bob'), + iRODSAccess('read', '/tempZone/home/alice', 'alice') +] +print(sorted(perms_list)) +``` + +and, as always, a sort key may be used for custom sorting; for example, +the following sorts the objects simply by ascending numerical value of the access: +```py +perms = sorted( + [ + ACLOperation('read', 'bob'), + ACLOperation('write', 'rods'), + ACLOperation('read_object', 'alice') + ], + key=lambda acl: iRODSAccess.codes[acl.access_name] +) +``` Quotas (v2.0.0) --------------- From 8397b343424e9160dd22cf62a62ff1432a518cfd Mon Sep 17 00:00:00 2001 From: Daniel Moore Date: Fri, 27 Mar 2026 21:17:17 -0400 Subject: [PATCH 08/40] update test name to include _issue_ --- irods/test/access_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 46ecf5a49..899b0ec8a 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -497,7 +497,7 @@ def test_iRODSAccess_cannot_be_constructed_using_unsupported_type__issue_558(sel self.sess, ) - def test_atomic_acls_505(self): + def test_atomic_acls__issue_505(self): ses = self.sess zone = user1 = user2 = user3 = group = None try: From 5b1c514fd234d0a609747b9bbc4dec7781151c30 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 29 Mar 2026 10:31:32 -0400 Subject: [PATCH 09/40] README example for normalization before comparison of acl-like objects --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fc299024..24c880bd5 100644 --- a/README.md +++ b/README.md @@ -2138,7 +2138,21 @@ session.acls.apply_atomic_operations( ) ``` `ACLOperation` objects form a linear order with `iRODSAccess` objects, and -indeed are subclassed from them as well, allowing equivalency testing. +indeed are subclassed from them as well, allowing intermixed sequences to be +sorted; however, care should be taken to normalize the objects before comparisons +or for related uses of the 'in' operator: +```py +normalize = lambda acl: acl.copy(decanonicalize=True, implied_zone='tempZone') +acls = sorted( + [ + iRODSAccess('read object', '/tempZone/home/alice', 'bob', 'tempZone'), + ACLOperation('write', 'rods'), + ACLOperation('read', 'bob'), + ], + key=normalize +) +print(normalize(acls[0]) == normalize(acls[1])) +``` Thus, for example: ```py From e565b16545e9db8a002e81b2724e1af580bcce1f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 29 Mar 2026 19:58:29 -0400 Subject: [PATCH 10/40] add true ACLOperation/iRODSAccess canonicalization for api calls and comparison --- README.md | 25 ++------------------- irods/access.py | 39 +++++++++++++++++++++++++++++---- irods/manager/access_manager.py | 2 +- irods/test/access_test.py | 2 +- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 24c880bd5..9b6ad9d1f 100644 --- a/README.md +++ b/README.md @@ -2142,39 +2142,18 @@ indeed are subclassed from them as well, allowing intermixed sequences to be sorted; however, care should be taken to normalize the objects before comparisons or for related uses of the 'in' operator: ```py -normalize = lambda acl: acl.copy(decanonicalize=True, implied_zone='tempZone') +normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') acls = sorted( [ iRODSAccess('read object', '/tempZone/home/alice', 'bob', 'tempZone'), ACLOperation('write', 'rods'), ACLOperation('read', 'bob'), - ], + ], key=normalize ) print(normalize(acls[0]) == normalize(acls[1])) ``` -Thus, for example: -```py -ACLOperation('read','public') in sess.acls.get(object) -``` -is a valid operation, so that an application that tends to cache object -permissions client-side might use such checks in optimizing atomic ACL -requests against the inclusion of any redundant ACL operations. - -For purposes of sorting, a `__lt__` operator is also defined that would -allow sorting of lists of `iRODSAccess`, `ACLOperations`, or the two intermixed: -```py -perms_list=[ - ACLOperation('read', 'bob'), - iRODSAccess('read', '/tempZone/home/alice', 'alice') -] -print(sorted(perms_list)) -``` - -and, as always, a sort key may be used for custom sorting; for example, -the following sorts the objects simply by ascending numerical value of the access: -```py perms = sorted( [ ACLOperation('read', 'bob'), diff --git a/irods/access.py b/irods/access.py index e830bc454..07373fc58 100644 --- a/irods/access.py +++ b/irods/access.py @@ -123,16 +123,44 @@ def __hash__(self): return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) def copy(self, decanonicalize=False, implied_zone=''): + """ + Create a copy of the object, possibly in a normalized form. + + Args: + decanonicalize: Whether to modify to access_name field to a more human (1/True) or more standard (-1) form + If the former, then one-word style is favored, ie "read" and "write". If the latter, the new access_name + will be more machine-friendly for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). + implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object and force the zone_name to + zero-length in the new object. + + Returns: + The new copy + """ other = copy.deepcopy(self) - if decanonicalize: - replacement_string = { + access_name = self.access_name + + if decanonicalize == 1: + if (new_access_name := { "read object": "read", "read_object": "read", "modify object": "write", "modify_object": "write", - }.get(self.access_name) - other.access_name = replacement_string if replacement_string is not None else self.access_name + }.get(access_name)) != None: access_name = new_access_name + elif decanonicalize == -1: + # Canonicalize, ie. change out old access_name for an unambiguous "standard" value. + access_name = access_name.replace(" ","_") + if (new_access_name := { + "read": "read_object", + "write": "modify_object", + }.get(access_name)) != None: access_name = new_access_name + elif decanonicalize == 0: + pass + else: + msg = "Improper value for 'decanonicalize' parameter" + raise RuntimerError(msg) + + other.access_name = access_name # Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for equality testing: if '' != implied_zone == other.user_zone: @@ -203,6 +231,9 @@ def __repr__(self): **{key: iRODSAccess.codes[_ichmod_synonym_mapping[key]] for key in _ichmod_synonym_mapping}, } +canonical_permissions = dict( + (k,v) for k,v in all_permissions.items() if ' ' not in k and k not in ('read','write') +) class _deprecated: class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index d63e5e2d8..527a8640a 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -174,7 +174,7 @@ def set(self, acl, recursive=False, admin=False, **kw): zone_ = acl.user_zone if acl.access_name.endswith("inherit"): zone_ = userName_ = "" - acl = acl.copy(decanonicalize=True) + acl = acl.copy(decanonicalize=-1) message_body = ModAclRequest( recursiveFlag=int(recursive), accessLevel=f"{prefix}{acl.access_name}", diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 899b0ec8a..402a79f90 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -515,7 +515,7 @@ def test_atomic_acls__issue_505(self): ) def normalize(access): - return access.copy(decanonicalize=True, implied_zone=ses.zone) + return access.copy(decanonicalize=-1, implied_zone=ses.zone) accesses = [normalize(acl) for acl in ses.acls.get(self.coll)] From b9c4088b34f10a147f37054d00f5bb95a543dd42 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 29 Mar 2026 20:32:40 -0400 Subject: [PATCH 11/40] correct README again --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9b6ad9d1f..a9d018871 100644 --- a/README.md +++ b/README.md @@ -2139,29 +2139,35 @@ session.acls.apply_atomic_operations( ``` `ACLOperation` objects form a linear order with `iRODSAccess` objects, and indeed are subclassed from them as well, allowing intermixed sequences to be -sorted; however, care should be taken to normalize the objects before comparisons -or for related uses of the 'in' operator: +sorted. Care should be taken to normalize the objects before such comparisons, +sorting, or with related uses of the 'in' operator: ```py +from irods.access import * normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') acls = sorted( [ - iRODSAccess('read object', '/tempZone/home/alice', 'bob', 'tempZone'), + iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'), ACLOperation('write', 'rods'), ACLOperation('read', 'bob'), ], key=normalize ) print(normalize(acls[0]) == normalize(acls[1])) +print(normalize(iRODSAccess('read', '/tempZone/home/alice', 'bob')) in map(normalize, acls)) ``` -perms = sorted( +If strict order of permissions is desired, use something like the below: +```py +from irods.access import * +from pprint import pp +pp(sorted( [ ACLOperation('read', 'bob'), - ACLOperation('write', 'rods'), + ACLOperation('own', 'rods'), ACLOperation('read_object', 'alice') ], - key=lambda acl: iRODSAccess.codes[acl.access_name] -) + key=lambda acl: (all_permissions[acl.access_name], normalize(acl)) +)) ``` Quotas (v2.0.0) From fe51e786475e731ff13871cc02a7679c8fc78d88 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 29 Mar 2026 20:47:06 -0400 Subject: [PATCH 12/40] README ':'->'.' --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9d018871..7e31f1fd5 100644 --- a/README.md +++ b/README.md @@ -2140,7 +2140,7 @@ session.acls.apply_atomic_operations( `ACLOperation` objects form a linear order with `iRODSAccess` objects, and indeed are subclassed from them as well, allowing intermixed sequences to be sorted. Care should be taken to normalize the objects before such comparisons, -sorting, or with related uses of the 'in' operator: +sorting, or with related uses of the 'in' operator. ```py from irods.access import * normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') @@ -2165,7 +2165,7 @@ pp(sorted( ACLOperation('read', 'bob'), ACLOperation('own', 'rods'), ACLOperation('read_object', 'alice') - ], + ], key=lambda acl: (all_permissions[acl.access_name], normalize(acl)) )) ``` From 375ab0d8824737b10f3b1e6de01fe7b93ea6418e Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 29 Mar 2026 20:52:05 -0400 Subject: [PATCH 13/40] whitespace --- irods/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 07373fc58..2bb71de88 100644 --- a/irods/access.py +++ b/irods/access.py @@ -127,8 +127,8 @@ def copy(self, decanonicalize=False, implied_zone=''): Create a copy of the object, possibly in a normalized form. Args: - decanonicalize: Whether to modify to access_name field to a more human (1/True) or more standard (-1) form - If the former, then one-word style is favored, ie "read" and "write". If the latter, the new access_name + decanonicalize: Whether to modify to access_name field to a more human-readable (1/True) or more standard (-1) form. + If the former, then a more organic style is favored, i.e. "read" and "write". If the latter, the new access_name will be more machine-friendly for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object and force the zone_name to zero-length in the new object. From c57620e89a9d7267aa714ee8ce1feeb421a4bf23 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 30 Mar 2026 08:43:25 -0400 Subject: [PATCH 14/40] improve README phrasing --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7e31f1fd5..1e9300860 100644 --- a/README.md +++ b/README.md @@ -2138,9 +2138,15 @@ session.acls.apply_atomic_operations( ) ``` `ACLOperation` objects form a linear order with `iRODSAccess` objects, and -indeed are subclassed from them as well, allowing intermixed sequences to be -sorted. Care should be taken to normalize the objects before such comparisons, -sorting, or with related uses of the 'in' operator. +indeed are subclassed from them as well, allowing equivalence comparisons and +also permitting intermixed sequences to be sorted (using the `__lt__` method +if no sort `key` parameter is given). + +Care should be taken however to normalize the objects before such comparisons +and sorting, and with connected uses of the `in` operator (which leverages `__eq__`). + +The following code sorts the objects based on their lexical order starting with the +normalized `access_name`, which serves to group identical permissions together: ```py from irods.access import * normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') @@ -2156,7 +2162,7 @@ print(normalize(acls[0]) == normalize(acls[1])) print(normalize(iRODSAccess('read', '/tempZone/home/alice', 'bob')) in map(normalize, acls)) ``` -If strict order of permissions is desired, use something like the below: +If strict order of permissions is desired, we can use code such as the following: ```py from irods.access import * from pprint import pp From a808b38d6d212fad2bd17bc0bda309cd3cdbcacf Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 30 Mar 2026 09:07:05 -0400 Subject: [PATCH 15/40] correct README examples --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1e9300860..b2415d1d1 100644 --- a/README.md +++ b/README.md @@ -2127,7 +2127,7 @@ the AccessManager's `apply_atomic_operations` method: from irods.access import ACLOperation from irods.helpers import home_collection session = irods.helpers.make_session() -myCollection = session.collections.create(f"{home_collection(session).path}/newCollection") +myCollection = session.collections.create(f"{home_collection(session)}/newCollection") session.acls.apply_atomic_operations( myCollection.path, @@ -2150,15 +2150,13 @@ normalized `access_name`, which serves to group identical permissions together: ```py from irods.access import * normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') -acls = sorted( - [ - iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'), - ACLOperation('write', 'rods'), - ACLOperation('read', 'bob'), - ], - key=normalize -) -print(normalize(acls[0]) == normalize(acls[1])) +acls = [ + iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'), + ACLOperation('write', 'rods'), + ACLOperation('read', 'bob'), +] +print(normalize(acls[0]) == normalize(acls[2])) +acls.sort(key=normalize) print(normalize(iRODSAccess('read', '/tempZone/home/alice', 'bob')) in map(normalize, acls)) ``` From b5668945ad9e277325f773bbb6bb84cf865c6b1b Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 30 Mar 2026 09:15:19 -0400 Subject: [PATCH 16/40] drop use of 'ichmod' in identifiers --- irods/access.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/irods/access.py b/irods/access.py index 2bb71de88..6aded77c9 100644 --- a/irods/access.py +++ b/irods/access.py @@ -7,7 +7,7 @@ from irods.path import iRODSPath -_ichmod_listed_permissions = ( +_permissions = ( "own", "delete_object", "write", @@ -54,7 +54,7 @@ def _codes(): ).items(), key=lambda _: _[1], ) - if key_ in _ichmod_listed_permissions + if key_ in _permissions ) @property @@ -218,7 +218,7 @@ def __repr__(self): ( - _ichmod_synonym_mapping := { + _synonym_mapping := { # syn : canonical "write": "modify_object", "read": "read_object", @@ -228,7 +228,7 @@ def __repr__(self): all_permissions = { **iRODSAccess.codes, - **{key: iRODSAccess.codes[_ichmod_synonym_mapping[key]] for key in _ichmod_synonym_mapping}, + **{key: iRODSAccess.codes[_synonym_mapping[key]] for key in _synonym_mapping}, } canonical_permissions = dict( From 4120862acd24ed253337e36d625915d299dc08dc Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 30 Mar 2026 09:38:06 -0400 Subject: [PATCH 17/40] add permissions in README section title --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b2415d1d1..bf45b2ac2 100644 --- a/README.md +++ b/README.md @@ -2118,8 +2118,8 @@ membership, this can be achieved with another query. `.permissions` was therefore removed in v2.0.0 in favor of `.acls`. -Atomic ACLs ------------ +Atomically setting permissions +------------------------------ A list of permissions may be added to an object atomically using the AccessManager's `apply_atomic_operations` method: From 889b5c18a85a07f9f93c11817102b40709c7845d Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 30 Mar 2026 09:45:55 -0400 Subject: [PATCH 18/40] ruff stuff --- irods/access.py | 113 +++++++++++++++++--------------- irods/manager/access_manager.py | 20 +++--- irods/session.py | 11 +++- irods/test/access_test.py | 8 +-- 4 files changed, 81 insertions(+), 71 deletions(-) diff --git a/irods/access.py b/irods/access.py index 6aded77c9..cccce5f7c 100644 --- a/irods/access.py +++ b/irods/access.py @@ -6,7 +6,6 @@ from irods.data_object import iRODSDataObject from irods.path import iRODSPath - _permissions = ( "own", "delete_object", @@ -24,46 +23,45 @@ class _Access_LookupMeta(type): - @staticmethod def _codes(): return collections.OrderedDict( (key_, value_) for key_, value_ in sorted( - dict( - # copied from iRODS source code in + { + # adapted from iRODS source code in # ./server/core/include/irods/catalog_utilities.hpp: - null=1000, - execute=1010, - read_annotation=1020, - read_system_metadata=1030, - read_metadata=1040, - read_object=1050, - write_annotation=1060, - create_metadata=1070, - modify_metadata=1080, - delete_metadata=1090, - administer_object=1100, - create_object=1110, - modify_object=1120, - delete_object=1130, - create_token=1140, - delete_token=1150, - curate=1160, - own=1200, - ).items(), + "null": 1000, + "execute": 1010, + "read_annotation": 1020, + "read_system_metadata": 1030, + "read_metadata": 1040, + "read_object": 1050, + "write_annotation": 1060, + "create_metadata": 1070, + "modify_metadata": 1080, + "delete_metadata": 1090, + "administer_object": 1100, + "create_object": 1110, + "modify_object": 1120, + "delete_object": 1130, + "create_token": 1140, + "delete_token": 1150, + "curate": 1160, + "own": 1200, + }.items(), key=lambda _: _[1], ) if key_ in _permissions ) @property - def codes(metaclass_target): - return metaclass_target._codes() + def codes(cls): + return cls._codes() @property - def strings(metaclass_target): - return collections.OrderedDict((number, string) for string, number in metaclass_target._codes().items()) + def strings(cls): + return collections.OrderedDict((number, string) for string, number in cls._codes().items()) def __getitem__(self, key): return self.codes[key] @@ -127,11 +125,12 @@ def copy(self, decanonicalize=False, implied_zone=''): Create a copy of the object, possibly in a normalized form. Args: - decanonicalize: Whether to modify to access_name field to a more human-readable (1/True) or more standard (-1) form. - If the former, then a more organic style is favored, i.e. "read" and "write". If the latter, the new access_name - will be more machine-friendly for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). - implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object and force the zone_name to - zero-length in the new object. + decanonicalize: Whether to modify to access_name field to a more human-readable (1/True) + or more standard (-1) form. If the former, then a more organic style is favored, i.e. + "read" and "write". If the latter, the new access_name will be more machine-friendly + for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). + implied_zone: If a nonzero-length name, compare this against the zone_name field of the + old object, and if they match, force the zone_name to zero-length in the new object. Returns: The new copy @@ -141,28 +140,35 @@ def copy(self, decanonicalize=False, implied_zone=''): access_name = self.access_name if decanonicalize == 1: - if (new_access_name := { - "read object": "read", - "read_object": "read", - "modify object": "write", - "modify_object": "write", - }.get(access_name)) != None: access_name = new_access_name + if ( + new_access_name := { + "read object": "read", + "read_object": "read", + "modify object": "write", + "modify_object": "write", + }.get(access_name) + ) is not None: + access_name = new_access_name elif decanonicalize == -1: # Canonicalize, ie. change out old access_name for an unambiguous "standard" value. - access_name = access_name.replace(" ","_") - if (new_access_name := { - "read": "read_object", - "write": "modify_object", - }.get(access_name)) != None: access_name = new_access_name + access_name = access_name.replace(" ", "_") + if ( + new_access_name := { + "read": "read_object", + "write": "modify_object", + }.get(access_name) + ) is not None: + access_name = new_access_name elif decanonicalize == 0: pass else: msg = "Improper value for 'decanonicalize' parameter" - raise RuntimerError(msg) + raise RuntimeError(msg) other.access_name = access_name - # Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for equality testing: + # Useful if we wish to force an explicitly specified local zone to an implicit zone spec in the copy, for + # equality testing: if '' != implied_zone == other.user_zone: other.user_zone = '' @@ -219,11 +225,10 @@ def __repr__(self): ( _synonym_mapping := { - # syn : canonical "write": "modify_object", "read": "read_object", } -).update((key.replace("_", " "), key) for key in iRODSAccess.codes.keys()) +).update((key.replace("_", " "), key) for key in iRODSAccess.codes) all_permissions = { @@ -231,9 +236,8 @@ def __repr__(self): **{key: iRODSAccess.codes[_synonym_mapping[key]] for key in _synonym_mapping}, } -canonical_permissions = dict( - (k,v) for k,v in all_permissions.items() if ' ' not in k and k not in ('read','write') -) +canonical_permissions = {k: v for k, v in all_permissions.items() if ' ' not in k and k not in ('read', 'write')} + class _deprecated: class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): @@ -243,15 +247,18 @@ class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): if key in ("own", "write", "modify_object", "read", "read_object", "null") ) strings = collections.OrderedDict((number, string) for string, number in codes.items()) + def __init__(self, *args, **kwargs): warnings.warn( "_iRODSAccess_pre_4_3_0 is deprecated and will be removed in a future version. Use iRODSAccess instead.", DeprecationWarning, - stacklevel=2 + stacklevel=2, ) - super().__init__(*args,**kwargs) + super().__init__(*args, **kwargs) + + +_deprecated_names = {'_iRODSAccess_pre_4_3_0': _deprecated._iRODSAccess_pre_4_3_0} -_deprecated_names = {'_iRODSAccess_pre_4_3_0':_deprecated._iRODSAccess_pre_4_3_0} def __getattr__(name): if name in _deprecated_names: diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 527a8640a..1b0828e9a 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,25 +1,23 @@ +import logging from os.path import basename, dirname -from irods.manager import Manager +from irods.access import iRODSAccess from irods.api_number import api_number -from irods.message import ModAclRequest, iRODSMessage, JSON_Message -from irods.data_object import iRODSDataObject, irods_dirname, irods_basename from irods.collection import iRODSCollection +from irods.column import In +from irods.data_object import irods_basename, irods_dirname, iRODSDataObject +from irods.manager import Manager +from irods.message import JSON_Message, ModAclRequest, iRODSMessage from irods.models import ( - DataObject, Collection, - User, + CollectionAccess, CollectionUser, DataAccess, - CollectionAccess, + DataObject, + User, ) -from irods.access import iRODSAccess -from irods.column import In from irods.user import iRODSUser -import logging -import warnings - logger = logging.getLogger(__name__) diff --git a/irods/session.py b/irods/session.py index 0b32a01f1..1f8d8305e 100644 --- a/irods/session.py +++ b/irods/session.py @@ -106,12 +106,17 @@ def auth_file(self): @property def available_permissions(self): - from irods.access import iRODSAccess, _iRODSAccess_pre_4_3_0 - try: self.__access except AttributeError: - self.__access = _iRODSAccess_pre_4_3_0 if self.server_version < (4, 3) else iRODSAccess + if self.server_version < (4, 3): + from irods.access import _iRODSAccess_pre_4_3_0 + + self.__access = _iRODSAccess_pre_4_3_0 + else: + from irods.access import iRODSAccess + + self.__access = iRODSAccess return self.__access def __init__(self, configure=True, auto_cleanup=True, **kwargs): diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 402a79f90..64fde8a36 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -4,15 +4,15 @@ import sys import unittest -from irods.access import iRODSAccess, ACLOperation +from irods.test import helpers +from irods.access import ACLOperation, iRODSAccess from irods.collection import iRODSCollection from irods.column import In, Like from irods.exception import UserDoesNotExist -from irods.models import User, Collection, DataObject +from irods.models import Collection, DataObject, User from irods.path import iRODSPath -from irods.user import iRODSUser from irods.session import iRODSSession -import irods.test.helpers as helpers +from irods.user import iRODSUser class TestAccess(unittest.TestCase): From 34980b3f048f80c92ed45febfdd34aa3526bfb77 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 5 Apr 2026 12:35:56 -0400 Subject: [PATCH 19/40] docstring stuff --- irods/access.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/irods/access.py b/irods/access.py index cccce5f7c..e8b602304 100644 --- a/irods/access.py +++ b/irods/access.py @@ -133,7 +133,7 @@ def copy(self, decanonicalize=False, implied_zone=''): old object, and if they match, force the zone_name to zero-length in the new object. Returns: - The new copy + A copy of the invoking object, normalized if requested. """ other = copy.deepcopy(self) @@ -182,6 +182,12 @@ def __repr__(self): class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta): + """ + This class represents an ACL in iRODS and functions as a data container + to convey information to the iRODS server (in the `set` call) and back again to the client + again (in the `get` call). + """ + def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): self.codes = self.__class__.codes self.strings = self.__class__.strings @@ -189,6 +195,14 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): + """ + Similar to its base class iRODSAccess, this class represents an ACL to be set on an object. + but this class is the counterpart used for the atomic ACLs api. It differs from its base + class in that it has no field to store a logical object path. (For an atomic API call, i + here is always a single logical path to which all operations apply, meaning that it is + appropriate to conveyed that in a location separate from the operations themselves.) + """ + def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): super().__init__( access_name=access_name, From 83e16e69b27e50ba19e287ff11e830f61c62709d Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Wed, 1 Apr 2026 17:14:21 -0400 Subject: [PATCH 20/40] hash function for ACLOperation --- irods/access.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/irods/access.py b/irods/access.py index e8b602304..9d4bf89a6 100644 --- a/irods/access.py +++ b/irods/access.py @@ -222,6 +222,15 @@ def __eq__(self, other): other.user_zone, ) + def __hash__(self): + # Hash in a way consistent with an iRODSAccess having path "". + return hash(( + self.access_name, + "", # path + self.user_name, + self.user_zone, + )) + def __lt__(self, other): return ( self.access_name, From 7792dfc9fe14da3e6ec72411208dfc26642f3fe8 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 03:46:57 -0400 Subject: [PATCH 21/40] extra normalizer method "normal" and consistent eq/hash semantics for use as dict keys and/or set members. --- README.md | 12 ++++++++---- irods/access.py | 14 +++++++++----- irods/test/access_test.py | 13 +++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bf45b2ac2..486fea6df 100644 --- a/README.md +++ b/README.md @@ -2149,15 +2149,19 @@ The following code sorts the objects based on their lexical order starting with normalized `access_name`, which serves to group identical permissions together: ```py from irods.access import * -normalize = lambda acl: acl.copy(decanonicalize=-1, implied_zone='tempZone') +import irods.helpers acls = [ iRODSAccess('read_object', '/tempZone/home/alice', 'bob', 'tempZone'), ACLOperation('write', 'rods'), ACLOperation('read', 'bob'), ] + +session = irods.helpers.make_session() +normalize = lambda acl: acl.normal(session) + print(normalize(acls[0]) == normalize(acls[2])) acls.sort(key=normalize) -print(normalize(iRODSAccess('read', '/tempZone/home/alice', 'bob')) in map(normalize, acls)) +print(normalize(iRODSAccess('read', '', 'bob')) in map(normalize,acls)) ``` If strict order of permissions is desired, we can use code such as the following: @@ -2166,11 +2170,11 @@ from irods.access import * from pprint import pp pp(sorted( [ - ACLOperation('read', 'bob'), + ACLOperation('read', 'bob' ), ACLOperation('own', 'rods'), ACLOperation('read_object', 'alice') ], - key=lambda acl: (all_permissions[acl.access_name], normalize(acl)) + key=lambda acl: (all_permissions[acl.access_name], acl.normal()) )) ``` diff --git a/irods/access.py b/irods/access.py index 9d4bf89a6..4d1745bcd 100644 --- a/irods/access.py +++ b/irods/access.py @@ -4,7 +4,6 @@ from irods.collection import iRODSCollection from irods.data_object import iRODSDataObject -from irods.path import iRODSPath _permissions = ( "own", @@ -102,23 +101,28 @@ def __init__(self, access_name, path, user_name, user_zone, user_type): self.user_type = user_type def __lt__(self, other): - return (self.access_name, self.user_name, self.user_zone, iRODSPath(self.path)) < ( + return (self.access_name, self.user_name, self.user_zone, str(self.path)) < ( other.access_name, other.user_name, other.user_zone, - iRODSPath(other.path), + str(other.path), ) def __eq__(self, other): return ( self.access_name == other.access_name - and iRODSPath(self.path) == iRODSPath(other.path) + and str(self.path) == str(other.path) and self.user_name == other.user_name and self.user_zone == other.user_zone ) def __hash__(self): - return hash((self.access_name, iRODSPath(self.path), self.user_name, self.user_zone)) + return hash((self.access_name, str(self.path), self.user_name, self.user_zone)) + + def normal(self, session = None): + normal_form = self.copy(decanonicalize=-1, implied_zone = (session.zone if session else '')) + normal_form.path = "" + return normal_form def copy(self, decanonicalize=False, implied_zone=''): """ diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 64fde8a36..08146491a 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -514,16 +514,13 @@ def test_atomic_acls__issue_505(self): a4 := ACLOperation("read", group.name), ) - def normalize(access): - return access.copy(decanonicalize=-1, implied_zone=ses.zone) - - accesses = [normalize(acl) for acl in ses.acls.get(self.coll)] + accesses = {acl.normal(ses) for acl in ses.acls.get(self.coll)} # Assert that the ACLs we added are among those listed for the object in the catalog. - self.assertIn(normalize(a1), accesses) - self.assertIn(normalize(a2), accesses) - self.assertIn(normalize(a3), accesses) - self.assertIn(normalize(a4), accesses) + self.assertIn(a1.normal(ses), accesses) + self.assertIn(a2.normal(ses), accesses) + self.assertIn(a3.normal(ses), accesses) + self.assertIn(a4.normal(ses), accesses) finally: if user1: From cf322408e3ec05dad714b5cf132028b3a917fdb3 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 10:00:14 -0400 Subject: [PATCH 22/40] ruff reformat of previous commit --- irods/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 4d1745bcd..6147a8c93 100644 --- a/irods/access.py +++ b/irods/access.py @@ -119,8 +119,8 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, str(self.path), self.user_name, self.user_zone)) - def normal(self, session = None): - normal_form = self.copy(decanonicalize=-1, implied_zone = (session.zone if session else '')) + def normal(self, session=None): + normal_form = self.copy(decanonicalize=-1, implied_zone=(session.zone if session else '')) normal_form.path = "" return normal_form From 6eac4f779fc9ac6d474206baa877c27297fb7e44 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 11:11:48 -0400 Subject: [PATCH 23/40] doc string fixes --- irods/access.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 6147a8c93..d04174bd4 100644 --- a/irods/access.py +++ b/irods/access.py @@ -129,15 +129,19 @@ def copy(self, decanonicalize=False, implied_zone=''): Create a copy of the object, possibly in a normalized form. Args: - decanonicalize: Whether to modify to access_name field to a more human-readable (1/True) - or more standard (-1) form. If the former, then a more organic style is favored, i.e. + decanonicalize: Whether to modify to access_name field to a more human-readable (when 1 i.e. True) + or more standard form (when -1). If the former, then a more organic style is favored, i.e. "read" and "write". If the latter, the new access_name will be more machine-friendly for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). + If equal to (0 i.e. False), no adjustment is done. implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object, and if they match, force the zone_name to zero-length in the new object. Returns: A copy of the invoking object, normalized if requested. + + Raises: + RuntimeError: if decanonicalize parameter is not one of {-1,0,False,1,True}. """ other = copy.deepcopy(self) From 16e2a28d1b28253ba92c96ec138efcda8cf31d04 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 11:13:56 -0400 Subject: [PATCH 24/40] docstring reformat --- irods/access.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/irods/access.py b/irods/access.py index d04174bd4..b93e13ce4 100644 --- a/irods/access.py +++ b/irods/access.py @@ -129,11 +129,12 @@ def copy(self, decanonicalize=False, implied_zone=''): Create a copy of the object, possibly in a normalized form. Args: - decanonicalize: Whether to modify to access_name field to a more human-readable (when 1 i.e. True) - or more standard form (when -1). If the former, then a more organic style is favored, i.e. - "read" and "write". If the latter, the new access_name will be more machine-friendly - for operators __lt__ (for sorting) and __eq__ (for equivalence or use with 'in'). - If equal to (0 i.e. False), no adjustment is done. + decanonicalize: Whether to modify to access_name field to a more human-readable form + (when 1 or True) or a more standard form (when -1). If the former, then a more + organic style is favored, i.e. "read" and "write". If the latter, the new + access_name will be more machine-friendly for operators __lt__ (for sorting) and + __eq__ (for equivalence or use with 'in'). If equal to 0 (or False), no adjustment + is done. implied_zone: If a nonzero-length name, compare this against the zone_name field of the old object, and if they match, force the zone_name to zero-length in the new object. From 4bf90b1647f02fa955a128f3166b93b5e486708f Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 14:25:51 -0400 Subject: [PATCH 25/40] alter parameter; zone name instead of session --- README.md | 2 +- irods/access.py | 16 ++++++++++++++-- irods/test/access_test.py | 10 +++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 486fea6df..495ed65a1 100644 --- a/README.md +++ b/README.md @@ -2157,7 +2157,7 @@ acls = [ ] session = irods.helpers.make_session() -normalize = lambda acl: acl.normal(session) +normalize = lambda acl: acl.normal(local_zone=session.zone) print(normalize(acls[0]) == normalize(acls[2])) acls.sort(key=normalize) diff --git a/irods/access.py b/irods/access.py index b93e13ce4..26786d584 100644 --- a/irods/access.py +++ b/irods/access.py @@ -119,8 +119,20 @@ def __eq__(self, other): def __hash__(self): return hash((self.access_name, str(self.path), self.user_name, self.user_zone)) - def normal(self, session=None): - normal_form = self.copy(decanonicalize=-1, implied_zone=(session.zone if session else '')) + def normal(self, local_zone=""): + """ + Create a normalized version of the object for comparison in sorting or determining equavalence. + + Args: + local_zone: the name of the home zone, if any, in which client user directly authenticates. + The purpose is zone name normalization; if this parameter is a nonzero-length string which + matches the zone_name in the source object, the copy will contain a null zone_name field. + + Returns: + The normalized copy of the source object. In practice, this will be an ACLOperation or iRODSAccess + object, according to the type of the source object. + """ + normal_form = self.copy(decanonicalize=-1, implied_zone=local_zone) normal_form.path = "" return normal_form diff --git a/irods/test/access_test.py b/irods/test/access_test.py index 08146491a..dec2115a1 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -514,13 +514,13 @@ def test_atomic_acls__issue_505(self): a4 := ACLOperation("read", group.name), ) - accesses = {acl.normal(ses) for acl in ses.acls.get(self.coll)} + accesses = {acl.normal(ses.zone) for acl in ses.acls.get(self.coll)} # Assert that the ACLs we added are among those listed for the object in the catalog. - self.assertIn(a1.normal(ses), accesses) - self.assertIn(a2.normal(ses), accesses) - self.assertIn(a3.normal(ses), accesses) - self.assertIn(a4.normal(ses), accesses) + self.assertIn(a1.normal(ses.zone), accesses) + self.assertIn(a2.normal(ses.zone), accesses) + self.assertIn(a3.normal(ses.zone), accesses) + self.assertIn(a4.normal(ses.zone), accesses) finally: if user1: From b61a820d35bf9b2c5bb9922cdd6373d04d3d01fa Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Thu, 2 Apr 2026 14:38:07 -0400 Subject: [PATCH 26/40] more ruff stuff --- irods/access.py | 34 ++++++++++++++++++++++----------- irods/exception.py | 3 ++- irods/manager/access_manager.py | 2 ++ irods/test/access_test.py | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/irods/access.py b/irods/access.py index 26786d584..39e7decff 100644 --- a/irods/access.py +++ b/irods/access.py @@ -75,7 +75,7 @@ def items(self): return list(zip(self.keys(), self.values())) -class _iRODSAccess_base: +class _iRODSAccess_base: # noqa: N801 @classmethod def to_int(cls, key): return cls.codes[key] @@ -204,12 +204,14 @@ def __repr__(self): class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta): """ - This class represents an ACL in iRODS and functions as a data container - to convey information to the iRODS server (in the `set` call) and back again to the client - again (in the `get` call). + Represents an ACL in iRODS. + + An instance of this class functions as a data container to convey information to the iRODS + server (in the `set` call) and back again to the client again (in the `get` call). """ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): + # noqa: D107 self.codes = self.__class__.codes self.strings = self.__class__.strings super().__init__(access_name, path, user_name, user_zone, user_type) @@ -217,12 +219,15 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): """ - Similar to its base class iRODSAccess, this class represents an ACL to be set on an object. - but this class is the counterpart used for the atomic ACLs api. It differs from its base - class in that it has no field to store a logical object path. (For an atomic API call, i - here is always a single logical path to which all operations apply, meaning that it is - appropriate to conveyed that in a location separate from the operations themselves.) - """ + Represents an operation to be performed in iRODS' atomic ACL api. + + Similar to its base class, iRODSAccess, this class names an ACL to be set on an object. + It differs, however, in that it forgoes option to store a logical object path. (In the atomic + API call, there is always a single logical path to which all such operations apply, thus + it is appropriate that the path parameter is in a location separate from the operations.) + """ # noqa: D400 RUF100 + + # ruff: noqa: D105 on def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): super().__init__( @@ -244,6 +249,7 @@ def __eq__(self, other): ) def __hash__(self): + # Hash in a way consistent with an iRODSAccess having path "". return hash(( self.access_name, @@ -266,6 +272,8 @@ def __lt__(self, other): def __repr__(self): return f"" + # ruff: noqa: D105 off + ( _synonym_mapping := { @@ -283,6 +291,7 @@ def __repr__(self): canonical_permissions = {k: v for k, v in all_permissions.items() if ' ' not in k and k not in ('read', 'write')} +# ruff: noqa: RUF012 N801 on class _deprecated: class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): codes = collections.OrderedDict( @@ -294,13 +303,16 @@ class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): def __init__(self, *args, **kwargs): warnings.warn( - "_iRODSAccess_pre_4_3_0 is deprecated and will be removed in a future version. Use iRODSAccess instead.", + "_iRODSAccess_pre_4_3_0 is deprecated and will be removed in " + "a future version. Use iRODSAccess instead.", DeprecationWarning, stacklevel=2, ) super().__init__(*args, **kwargs) +# ruff: noqa: RUF012 N801 SLF001 off + _deprecated_names = {'_iRODSAccess_pre_4_3_0': _deprecated._iRODSAccess_pre_4_3_0} diff --git a/irods/exception.py b/irods/exception.py index 4dbbd66de..a1463d7f5 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -650,7 +650,8 @@ class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 -class SYS_INTERNAL_ERR(SystemException): +class SYS_INTERNAL_ERR(SystemException): # noqa: N801 + # noqa: D101 code = -154000 diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 1b0828e9a..13e3f845e 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,3 +1,5 @@ +""" The access manager is a collection of methods useful for managing iRODS ACLs. """ + import logging from os.path import basename, dirname diff --git a/irods/test/access_test.py b/irods/test/access_test.py index dec2115a1..d6be21f1a 100644 --- a/irods/test/access_test.py +++ b/irods/test/access_test.py @@ -4,7 +4,6 @@ import sys import unittest -from irods.test import helpers from irods.access import ACLOperation, iRODSAccess from irods.collection import iRODSCollection from irods.column import In, Like @@ -12,6 +11,7 @@ from irods.models import Collection, DataObject, User from irods.path import iRODSPath from irods.session import iRODSSession +from irods.test import helpers from irods.user import iRODSUser From b365440757e4246ea646f0e8c3b13fb85b66415c Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 5 Apr 2026 12:01:43 -0400 Subject: [PATCH 27/40] general noqa directives --- irods/access.py | 4 +++- irods/exception.py | 7 ++++--- irods/manager/access_manager.py | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/irods/access.py b/irods/access.py index 39e7decff..1affac84d 100644 --- a/irods/access.py +++ b/irods/access.py @@ -1,3 +1,5 @@ +# noqa + import collections import copy import warnings @@ -225,7 +227,7 @@ class ACLOperation(iRODSAccess): It differs, however, in that it forgoes option to store a logical object path. (In the atomic API call, there is always a single logical path to which all such operations apply, thus it is appropriate that the path parameter is in a location separate from the operations.) - """ # noqa: D400 RUF100 + """ # noqa: D400 # ruff: noqa: D105 on diff --git a/irods/exception.py b/irods/exception.py index a1463d7f5..ce8b4bea7 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -1,7 +1,8 @@ +# noqa + # if you're copying these from the docs, you might find the following regex helpful: # s/\(\w\+\)\s\+\(-\d\+\)/class \1(SystemException):\r code = \2/g - import errno import numbers import os @@ -650,8 +651,7 @@ class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 -class SYS_INTERNAL_ERR(SystemException): # noqa: N801 - # noqa: D101 +class SYS_INTERNAL_ERR(SystemException): code = -154000 @@ -2125,3 +2125,4 @@ class PAM_AUTH_PASSWORD_FAILED(PAMException): class PAM_AUTH_PASSWORD_INVALID_TTL(PAMException): code = -994000 + diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 13e3f845e..09af230b2 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,3 +1,4 @@ +# noqa """ The access manager is a collection of methods useful for managing iRODS ACLs. """ import logging From 11d8e92cb94d5200f77bfee5843b6a79fb04ddb8 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 5 Apr 2026 14:03:00 -0400 Subject: [PATCH 28/40] delete surrounding ws --- irods/manager/access_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 09af230b2..f45192229 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,5 +1,5 @@ # noqa -""" The access manager is a collection of methods useful for managing iRODS ACLs. """ +"""The access manager is a collection of methods useful for managing iRODS ACLs.""" import logging from os.path import basename, dirname From 957d390d4f144f7b7c6ecf0587410d15ed9d9d7e Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Sun, 5 Apr 2026 19:27:10 -0400 Subject: [PATCH 29/40] correct spelling --- irods/access.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/irods/access.py b/irods/access.py index 1affac84d..ca4625048 100644 --- a/irods/access.py +++ b/irods/access.py @@ -1,4 +1,4 @@ -# noqa + import collections import copy @@ -77,7 +77,7 @@ def items(self): return list(zip(self.keys(), self.values())) -class _iRODSAccess_base: # noqa: N801 +class _iRODSAccess_base: @classmethod def to_int(cls, key): return cls.codes[key] @@ -123,7 +123,7 @@ def __hash__(self): def normal(self, local_zone=""): """ - Create a normalized version of the object for comparison in sorting or determining equavalence. + Create a normalized version of the object for comparison in sorting or determining equivalence. Args: local_zone: the name of the home zone, if any, in which client user directly authenticates. @@ -313,8 +313,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -# ruff: noqa: RUF012 N801 SLF001 off - _deprecated_names = {'_iRODSAccess_pre_4_3_0': _deprecated._iRODSAccess_pre_4_3_0} From 72492f5feaf92508c4db940b69aec301d314d079 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 10:06:35 -0400 Subject: [PATCH 30/40] test ruff: noqa --- irods/access.py | 2 +- irods/exception.py | 2 -- irods/manager/access_manager.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/irods/access.py b/irods/access.py index ca4625048..8026c807b 100644 --- a/irods/access.py +++ b/irods/access.py @@ -1,4 +1,4 @@ - +# ruff: noqa import collections import copy diff --git a/irods/exception.py b/irods/exception.py index ce8b4bea7..3dda30e0f 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -1,5 +1,3 @@ -# noqa - # if you're copying these from the docs, you might find the following regex helpful: # s/\(\w\+\)\s\+\(-\d\+\)/class \1(SystemException):\r code = \2/g diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index f45192229..8ea4a1c6c 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,4 +1,4 @@ -# noqa +# ruff: noqa """The access manager is a collection of methods useful for managing iRODS ACLs.""" import logging From 4618440b99ebbd45b2ac59facce72d6338ecc4d1 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 10:32:55 -0400 Subject: [PATCH 31/40] irods/exceptions - ruff --- irods/exception.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/irods/exception.py b/irods/exception.py index 3dda30e0f..785014d10 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -648,10 +648,12 @@ class SYS_SOCK_READ_TIMEDOUT(SystemException): class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 +# ruff: noqa: N801 D101 off class SYS_INTERNAL_ERR(SystemException): code = -154000 +# ruff: noqa: N801 D101 on class SYS_BAD_INPUT(iRODSException): code = -158000 From f0542d821e7be14c1c59512764db44265befe010 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 11:29:53 -0400 Subject: [PATCH 32/40] disable trivial ruff reports for exception classes --- irods/exception.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/irods/exception.py b/irods/exception.py index 785014d10..a7c966ba1 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -8,6 +8,9 @@ from typing import Dict +# ruff: noqa: N801 D101 off + + class PycommandsException(Exception): pass @@ -648,12 +651,10 @@ class SYS_SOCK_READ_TIMEDOUT(SystemException): class SYS_INVALID_INPUT_PARAM(SystemException): code = -130000 -# ruff: noqa: N801 D101 off class SYS_INTERNAL_ERR(SystemException): code = -154000 -# ruff: noqa: N801 D101 on class SYS_BAD_INPUT(iRODSException): code = -158000 @@ -2126,3 +2127,4 @@ class PAM_AUTH_PASSWORD_FAILED(PAMException): class PAM_AUTH_PASSWORD_INVALID_TTL(PAMException): code = -994000 +# ruff: noqa: N801 D101 on From af08c63b40b24f89be63a2ad16d1f27e068985b8 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 11:35:15 -0400 Subject: [PATCH 33/40] ruff format --- irods/exception.py | 1 + 1 file changed, 1 insertion(+) diff --git a/irods/exception.py b/irods/exception.py index a7c966ba1..8ee119771 100644 --- a/irods/exception.py +++ b/irods/exception.py @@ -2127,4 +2127,5 @@ class PAM_AUTH_PASSWORD_FAILED(PAMException): class PAM_AUTH_PASSWORD_INVALID_TTL(PAMException): code = -994000 + # ruff: noqa: N801 D101 on From bbb1ff3fac980b312d4a2fad28107ddb88f8bde0 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 12:38:21 -0400 Subject: [PATCH 34/40] delete blanket noqa --- irods/access.py | 2 -- irods/manager/access_manager.py | 1 - 2 files changed, 3 deletions(-) diff --git a/irods/access.py b/irods/access.py index 8026c807b..f71341f63 100644 --- a/irods/access.py +++ b/irods/access.py @@ -1,5 +1,3 @@ -# ruff: noqa - import collections import copy import warnings diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index 8ea4a1c6c..b8c9d952a 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -1,4 +1,3 @@ -# ruff: noqa """The access manager is a collection of methods useful for managing iRODS ACLs.""" import logging From f08bfbb25e2e6ca62c7a9e6e3e36d3c9d22a1dc6 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 13:42:09 -0400 Subject: [PATCH 35/40] noqa comment in wrong place? experimenting.... --- irods/access.py | 3 +-- irods/manager/access_manager.py | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index f71341f63..25b4dde13 100644 --- a/irods/access.py +++ b/irods/access.py @@ -210,8 +210,7 @@ class iRODSAccess(_iRODSAccess_base, metaclass=_Access_LookupMeta): server (in the `set` call) and back again to the client again (in the `get` call). """ - def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): - # noqa: D107 + def __init__(self, access_name, path, user_name="", user_zone="", user_type=None): # noqa: D107 self.codes = self.__class__.codes self.strings = self.__class__.strings super().__init__(access_name, path, user_name, user_zone, user_type) diff --git a/irods/manager/access_manager.py b/irods/manager/access_manager.py index b8c9d952a..5873c1e15 100644 --- a/irods/manager/access_manager.py +++ b/irods/manager/access_manager.py @@ -45,6 +45,14 @@ def _to_acl_operation_json(op_input: iRODSAccess): } def apply_atomic_operations(self, logical_path: str, *operations, admin=False): + """ + Apply the requested operations atomically to the object at logical_path. + + Args: + logical_path: the fully qualified logical path of the target data object or collection. + operations: a sequence of ACLOperation instances. + admin: True if the admin flag should be applied for the Atomic ACLs api call. + """ request_text = { "logical_path": logical_path, "admin_mode": admin, From e0df45f3ba3c7b2c888bda8e28b661ae7366c497 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 14:37:50 -0400 Subject: [PATCH 36/40] block noqa suppressions --- irods/access.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 25b4dde13..f933d9256 100644 --- a/irods/access.py +++ b/irods/access.py @@ -228,7 +228,7 @@ class ACLOperation(iRODSAccess): # ruff: noqa: D105 on - def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): + def __init__(self, access_name: str, user_name: str = "", user_zone: str = ""): # noqa: D107 super().__init__( access_name=access_name, path="", @@ -290,7 +290,9 @@ def __repr__(self): canonical_permissions = {k: v for k, v in all_permissions.items() if ' ' not in k and k not in ('read', 'write')} -# ruff: noqa: RUF012 N801 on +# ruff: noqa: RUF012 N801 SLF001 on + + class _deprecated: class _iRODSAccess_pre_4_3_0(_iRODSAccess_base): codes = collections.OrderedDict( @@ -318,3 +320,6 @@ def __getattr__(name): warnings.warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2) return _deprecated_names[name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +# ruff: noqa: RUF012 N801 SLF001 off From 0d64c93c4c6598bb9c954eb88ae5392ada072dd0 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 14:43:50 -0400 Subject: [PATCH 37/40] D400 corrrection --- irods/access.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/irods/access.py b/irods/access.py index f933d9256..c53e3e79c 100644 --- a/irods/access.py +++ b/irods/access.py @@ -217,6 +217,7 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): + # ruff: enable[D400] """ Represents an operation to be performed in iRODS' atomic ACL api. @@ -224,7 +225,8 @@ class ACLOperation(iRODSAccess): It differs, however, in that it forgoes option to store a logical object path. (In the atomic API call, there is always a single logical path to which all such operations apply, thus it is appropriate that the path parameter is in a location separate from the operations.) - """ # noqa: D400 + """ # the 'noqa: D400' annotation placed here was ignored by ruff + # ruff: disable[D400] # ruff: noqa: D105 on From 789b1f50f9d0794d2c3cb23b43ee0f59efc0d9d6 Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 14:47:59 -0400 Subject: [PATCH 38/40] D400 corrrection correction --- irods/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index c53e3e79c..9a25a7c6c 100644 --- a/irods/access.py +++ b/irods/access.py @@ -217,7 +217,7 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): - # ruff: enable[D400] + # ruff: disable[D400] """ Represents an operation to be performed in iRODS' atomic ACL api. @@ -226,7 +226,7 @@ class ACLOperation(iRODSAccess): API call, there is always a single logical path to which all such operations apply, thus it is appropriate that the path parameter is in a location separate from the operations.) """ # the 'noqa: D400' annotation placed here was ignored by ruff - # ruff: disable[D400] + # ruff: enable[D400] # ruff: noqa: D105 on From a0de44f5ca71eb47811f0b06b083468f9951aa0d Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 15:32:47 -0400 Subject: [PATCH 39/40] Revert "D400 corrrection correction" This reverts commit 2c40bb96a434db9eb0c7215250974a137deb999a. --- irods/access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irods/access.py b/irods/access.py index 9a25a7c6c..c53e3e79c 100644 --- a/irods/access.py +++ b/irods/access.py @@ -217,7 +217,7 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): - # ruff: disable[D400] + # ruff: enable[D400] """ Represents an operation to be performed in iRODS' atomic ACL api. @@ -226,7 +226,7 @@ class ACLOperation(iRODSAccess): API call, there is always a single logical path to which all such operations apply, thus it is appropriate that the path parameter is in a location separate from the operations.) """ # the 'noqa: D400' annotation placed here was ignored by ruff - # ruff: enable[D400] + # ruff: disable[D400] # ruff: noqa: D105 on From 789f4cb794c49871296d138c12547fd872ea7f2c Mon Sep 17 00:00:00 2001 From: d-w-moore Date: Mon, 6 Apr 2026 15:32:50 -0400 Subject: [PATCH 40/40] Revert "D400 corrrection" This reverts commit 67d9a8462026ef1fc6bb5f069db5c932973238db. --- irods/access.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/irods/access.py b/irods/access.py index c53e3e79c..f933d9256 100644 --- a/irods/access.py +++ b/irods/access.py @@ -217,7 +217,6 @@ def __init__(self, access_name, path, user_name="", user_zone="", user_type=None class ACLOperation(iRODSAccess): - # ruff: enable[D400] """ Represents an operation to be performed in iRODS' atomic ACL api. @@ -225,8 +224,7 @@ class ACLOperation(iRODSAccess): It differs, however, in that it forgoes option to store a logical object path. (In the atomic API call, there is always a single logical path to which all such operations apply, thus it is appropriate that the path parameter is in a location separate from the operations.) - """ # the 'noqa: D400' annotation placed here was ignored by ruff - # ruff: disable[D400] + """ # noqa: D400 # ruff: noqa: D105 on