Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an interface to turn objects read-only #1162

Merged
merged 17 commits into from
Dec 18, 2023
Merged

Add an interface to turn objects read-only #1162

merged 17 commits into from
Dec 18, 2023

Conversation

pmrv
Copy link
Contributor

@pmrv pmrv commented Jul 10, 2023

As we discussed in #1157 it be nice to have some way of blocking users from changing values or calling methods after a certain point in time. This is implemented here with some doc test examples. For now it locks this recursively, but I have a vague fear that this might be not be super performant. I'll have to do some tests. I'll also move over at least DataContainer and Server in this PR.

@liamhuber Do you remember why HasStoredTraits implements its own read_only logic instead of reusing the existing one from DataContainer?

pmrv added 4 commits July 11, 2023 00:41
Sub classes that use sentinel on __setattr__ need to special case when
the attribute to set is `read_only` or `_read_only`.  Failure to do so
would result in being unable to unlock the object.  `sentinel` now
handles this.

Additionally adds a `lock_method`.  Default is `error`, but can be set
to emit a warning and allow modification instead.
This requires a few supers where subclasses were previously not diligent about it
@stale
Copy link

stale bot commented Aug 12, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Aug 12, 2023
@stale stale bot closed this Sep 17, 2023
@pmrv pmrv reopened this Oct 4, 2023
@stale stale bot removed the stale label Oct 4, 2023
@pmrv
Copy link
Contributor Author

pmrv commented Oct 4, 2023

Current failure seems to be related to #1200

@pmrv pmrv added the format_black reformat the code using the black standard label Oct 5, 2023
@pmrv pmrv marked this pull request as ready for review October 5, 2023 13:50
@pmrv pmrv requested review from liamhuber and jan-janssen October 5, 2023 13:50
@pmrv
Copy link
Contributor Author

pmrv commented Oct 5, 2023

Functionality and documentation is done from my side. There's some tests as this is now used by DataContainer and Server, but I can add additional specific tests after we've discussed whether we want this in a pyiron meeting.

@liamhuber
Copy link
Member

@pmrv, this hasn't escaped my notice, I just didn't manage to get this far today!

@pmrv pmrv added format_black reformat the code using the black standard and removed format_black reformat the code using the black standard labels Oct 9, 2023
Copy link
Member

@liamhuber liamhuber left a comment

Choose a reason for hiding this comment

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

Really nice, Marvin. For specific application here, I only have the one nit to refactor:rename unlocked() to unlock(). Otherwise lgtm, tests can be written, and I think this is useful in base.

Zooming out a bit though, I think this is an elegant solution to the very generic problem of transforming python attributes to being read-only. I think this would be an excellent candidate for the standard-library-only toolbox discussed in #1208. Here it would just be a matter of purging reference to HasGroups, which could then be re-introduced here in pyiron_base by defining _iterate_lockable_subs and a new class inheriting from the generic tool that overrides _on_lock and _on_unlock. Unlike Singleton, which is a breeze to re-implement as needed, what you have here is a bit more complex and might really be useful to the community as a standalone tool (or at least stand-with-other-lightweight-tools-in-a-nice-toolbox)

object.__setattr__(self, "_lock_method", method)
self.read_only = True

def unlocked(self) -> _UnlockContext:
Copy link
Member

Choose a reason for hiding this comment

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

nit: I think this should be unlock to match the verb tense of lock

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I named it asymmetrical on purpose, because lock and unlocked are not symmetric either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I wanted to avoid is that some code unlocks an object and then crashes (or forgets it) before it can relock it. So obj.unlocked() is a context manager that does the relocking.

Copy link
Member

Choose a reason for hiding this comment

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

Clever. I approve. But I was too dense to pick up on it on the first read, so maybe a comment or docstring to clarify intent is good.

Comment on lines +314 to +315
for it in _iterate_lockable_subs(self):
it.lock()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for it in _iterate_lockable_subs(self):
it.lock()
pass

maybe (see main review comment)

Comment on lines +318 to +319
for it in _iterate_lockable_subs(self):
it.read_only = False
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for it in _iterate_lockable_subs(self):
it.read_only = False
pass

maybe (see main review comment)

Comment on lines +104 to +122
def _iterate_lockable_subs(lockable_groups):
"""
Yield sub nodes and groups that are lockable. Recurse into groups.

If the given object is not a :class:`HasGroups` yield nothing.
"""
if not isinstance(lockable_groups, HasGroups):
return

subs = lockable_groups.list_all()
for n in subs["nodes"]:
node = lockable_groups[n]
if isinstance(node, Lockable):
yield node

for g in subs["groups"]:
group = lockable_groups[g]
yield group
yield from _iterate_lockable_subs(g)
Copy link
Member

Choose a reason for hiding this comment

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

pyiron_base-specific

@pmrv pmrv added format_black reformat the code using the black standard integration Start the integration tests with pyiron_atomistics/contrib for this PR and removed format_black reformat the code using the black standard labels Dec 15, 2023
@pmrv
Copy link
Contributor Author

pmrv commented Dec 18, 2023

Integration tests are currently not passing, because base wants pint==0.23, but atomistics still requires <=0.22. I assume this will solve itself when atomistics is updated, but will go ahead merge this here anyway.

@pmrv pmrv merged commit c46434e into main Dec 18, 2023
20 of 25 checks passed
@delete-merged-branch delete-merged-branch bot deleted the lock branch December 18, 2023 10:03
@jan-janssen
Copy link
Member

Integration tests are currently not passing, because base wants pint==0.23, but atomistics still requires <=0.22. I assume this will solve itself when atomistics is updated, but will go ahead merge this here anyway.

It seems like there was more wrong than just the pint dependency as now the tests fail on pyiron_atomistics - pyiron/pyiron_atomistics#1265 - another indication that we should not merge broken code with main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request format_black reformat the code using the black standard integration Start the integration tests with pyiron_atomistics/contrib for this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants