Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ Disabling AVU reloads from the iRODS server

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

By way of explanation, please note that calls of the form
`obj.metadata([opt1=value1[,opt2=value2...]])` will always
produce new `iRODSMetaCollection` objects - which nevertheless share the same
session object as the original, as the copy is shallow in most respects.
This avoids always mutating the current instance and thus prevents any need to
implement context manager semantics when temporarily altering options such
as `reload` and `admin`.

Additionally note that the call `obj.metadata()` without option parameters
always syncs the AVU list within the resulting `iRODSMetaCollection` object to
what is currently in the catalog, because the original object is unmutated with
respect to all options (meaning `obj.metadata.reload` is always `True`) -- that
is, absent any low-level meddling within reserved fields by the application.
Thus, `obj.metadata().items()` will always agree with the in-catalog AVU list
whereas `obj.metadata.items()` might not.

Subclassing `iRODSMeta`
---------------------
The keyword option `iRODSMeta_type` can be used to set up any `iRODSMeta`
Expand Down
15 changes: 10 additions & 5 deletions irods/manager/metadata_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,19 @@
pass


# This was necessarily made separate from the MetadataManager definition
# in order to avoid infinite recursion in iRODSMetaCollection.__getattr__
_MetadataManager_opts_initializer = {
'admin':False,
'timestamps':False,
'iRODSMeta_type':iRODSMeta,
'reload':True
}

Check failure on line 37 in irods/manager/metadata_manager.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is for now. Source code will later be formatted according to ruff's criteria.


class MetadataManager(Manager):

def __init__(self, *_):
self._opts = {
'admin':False,
'timestamps':False,
'iRODSMeta_type':iRODSMeta
}
self._opts = _MetadataManager_opts_initializer.copy()
super().__init__(*_)

@property
Expand Down
42 changes: 42 additions & 0 deletions irods/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,48 @@


class iRODSMetaCollection:
def __setattr__(self, name, value):
"""Prevent the flag attributes such as 'admin', 'timestamps', etc., from
being changed in a way not sanctioned by the library.
"""

Check failure on line 137 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff DOC501

DOC501: Raised exception `AttributeError` missing from docstring [pydoclint:docstring-missing-exception]

Check failure on line 137 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D213

D213: Multi-line docstring summary should start at the second line [pydocstyle:multi-line-summary-second-line]

Check failure on line 137 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D205

D205: 1 blank line required between summary line and description [pydocstyle:missing-blank-line-after-summary]
from irods.manager.metadata_manager import _MetadataManager_opts_initializer

if name in _MetadataManager_opts_initializer:
msg = f"""The "{name}" attribute is a special one, settable only via a
call on the object. For example: admin_view = data_obj.metadata({name}=<value>)"""
raise AttributeError(msg)

super().__setattr__(name, value)

def __getattr__(self, name):
"""Here we intervene for the purpose of casting such settable flags such
as 'admin', 'timestamps', etc. as readable attributes of the object.

If the attribute name exists as a key in _MetadataManager_opts_initializer

Check failure on line 151 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff W291

W291: Trailing whitespace [pycodestyle:trailing-whitespace]

Check failure on line 151 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-format

Ruff format

Improper formatting
(for the "_opts" lookup in the manager object), then the current value from
_opts is returned. Otherwise we throw an exception to revert to the default
__getattr__ behavior.

For more specific coverage of such attributes and how they are used, please
consult the test module irods/test/meta_test.py and read the test for
issue #709, named:

test_cascading_changes_of_metadata_manager_options__issue_709
"""

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff DOC501

DOC501: Raised exception `AttributeError` missing from docstring [pydoclint:docstring-missing-exception]

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff DOC201

DOC201: `return` is not documented in docstring [pydoclint:docstring-missing-returns]

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D401

D401: First line of docstring should be in imperative mood: "Here we intervene for the purpose of casting such settable flags such" [pydocstyle:non-imperative-mood]

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D213

D213: Multi-line docstring summary should start at the second line [pydocstyle:multi-line-summary-second-line]

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D205

D205: 1 blank line required between summary line and description [pydocstyle:missing-blank-line-after-summary]

Check failure on line 161 in irods/meta.py

View workflow job for this annotation

GitHub Actions / ruff-lint / ruff-check

Ruff D202

D202: No blank lines allowed after function docstring (found 1) [pydocstyle:blank-line-after-function]

from irods.manager.metadata_manager import _MetadataManager_opts_initializer

# Separating _MetadataManager_opts_initializer from the MetadataManager class
# prevents the possibility of arbitrary access by copy.copy() to parts of
# our object's state before they have been initialized, as it is known to do
# by calling hasattr on the "__setstate__" attribute. The result of such
# unfettered access is infinite recursion. See:
# https://nedbatchelder.com/blog/201010/surprising_getattr_recursion

if name in _MetadataManager_opts_initializer:
return self._manager._opts[name] # noqa: SLF001
raise AttributeError

def __call__(self, **opts):
"""
Optional parameters in **opts are:
Expand Down
14 changes: 6 additions & 8 deletions irods/test/meta_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,24 +820,22 @@ def test_binary_avu_fields__issue_707(self):
def test_cascading_changes_of_metadata_manager_options__issue_709(self):
d = None

def get_option(metacoll, key):
return metacoll._manager._opts[key]
try:
d = self.sess.data_objects.create(f'{self.coll.path}/issue_709_test_1')
m = d.metadata
self.assertEqual(get_option(m, 'admin'), False)
self.assertEqual(m.admin, False)

m2 = m(admin=True)
self.assertEqual(get_option(m2, 'timestamps'), False)
self.assertEqual(get_option(m2, 'admin'), True)
self.assertEqual(m2.timestamps, False)
self.assertEqual(m2.admin, True)

m3 = m2(timestamps=True)
self.assertEqual(get_option(m3, 'timestamps'), True)
self.assertEqual(get_option(m3, 'admin'), True)
self.assertEqual(m3.timestamps, True)
self.assertEqual(m3.admin, True)
self.assertEqual(m3._manager.get_api_keywords().get(kw.ADMIN_KW), "")

m4 = m3(admin=False)
self.assertEqual(get_option(m4, 'admin'), False)
self.assertEqual(m4.admin, False)
self.assertEqual(m4._manager.get_api_keywords().get(kw.ADMIN_KW), None)
finally:
if d:
Expand Down
Loading