-
Notifications
You must be signed in to change notification settings - Fork 81
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
Enhance Python LivenessScope API #4497
Enhance Python LivenessScope API #4497
Conversation
As part of this PR, I'll add a how-to to our docs, providing some better introduction to using liveness scopes. |
return True | ||
if isinstance(referent, JObjectWrapper): | ||
return is_liveness_referent(referent.j_object) | ||
return False | ||
|
||
|
||
def liveness_scope() -> LivenessScope: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jmao-denver would it make sense to take this away (or deprecate it) and just encourage the LivenessScope constructor instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to ask something similar. How do we minimize the syntax complexity and length while still feeling Pythonic? Maybe we want to keep this method and have it take the open
args. Then users could just do something like:
with liveness_scope(True);
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dropping this in favor of just a regular ctor call, updating class docs
py/server/tests/testbase.py
Outdated
@@ -35,9 +35,11 @@ def tearDownClass(cls) -> None: | |||
|
|||
def setUp(self) -> None: | |||
self._liveness_scope = liveness_scope() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._liveness_scope
does not appear to be used. Can it be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I missed that.
@@ -27,44 +50,143 @@ def __init__(self, j_scope: jpy.JType): | |||
self.j_scope = j_scope | |||
|
|||
def __enter__(self): | |||
warn('Instead of passing liveness_scope() to with, call open() on it first', | |||
DeprecationWarning, stacklevel=2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see where DeprecationWarning
is being imported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Welcome to python I guess? its already imported by default for some reason: https://docs.python.org/3/library/exceptions.html?highlight=deprecationwarning#DeprecationWarning
$ python
Python 3.10.11 (main, May 22 2023, 14:37:10) [GCC 13.1.1 20230429] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(DeprecationWarning)
<class 'DeprecationWarning'>
warn('This function is deprecated, prefer release(). Use cases that rely on this are likely to now fail.', | ||
DeprecationWarning, stacklevel=2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If these use cases are likely to fail, should we just hard remove these warning cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The advantage (I leave it to you/Jianfeng if this is a good one or not) that the deprecation warning will tell you what to fix, but just removing the method will just break and let you sort it out yourself.
Returns True if the provided object is a LivenessReferent, and so can be managed by a LivenessScope. | ||
Args: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returns True if the provided object is a LivenessReferent, and so can be managed by a LivenessScope. | |
Args: | |
Returns True if the provided object is a LivenessReferent, and so can be managed by a LivenessScope. | |
Args: |
""" | ||
Returns True if the provided object is a LivenessReferent, and so can be managed by a LivenessScope. | ||
Args: | ||
referent: the object that maybe a LivenessReferent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing type info
return True | ||
if isinstance(referent, JObjectWrapper): | ||
return is_liveness_referent(referent.j_object) | ||
return False | ||
|
||
|
||
def liveness_scope() -> LivenessScope: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to ask something similar. How do we minimize the syntax complexity and length while still feeling Pythonic? Maybe we want to keep this method and have it take the open
args. Then users could just do something like:
with liveness_scope(True);
...
self.release() | ||
|
||
@contextlib.contextmanager | ||
def open(self, release_after_block: bool = True) -> "LivenessScope": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading this, I have some concerns. It seems like both
with LivenessScope():
...
and
with LivenessScope().open():
...
work, since open
returns a LivenessScope
, but only the second releases properly. I'm concerned that this design may create a construct that doesn't force the user to always do the right / smart thing.
At the same time, the syntax feels more awkward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both "work" (after s/LivenessScope/livenessScope/
), in that the first one will warn "you're doing it wrong, this will not work correctly", thus "strongly encouraging" the user to call open(). We can outright remove this right away and do away with deprecation warnings, or wait a version or two for anyone who happens to be using this and needs to transition away.
The new version is strictly more powerful, as it doesn't conflate creating/releasing a scope with adding/removing it on the stack. We can't really have it both ways... The other option (discussed with Ryan) is to make open() an optional step so that you can call it either way, but this makes the Java and Python usage different/confusing going back and forth.
One other option is that we deprecate liveness_scope() itself, and make the LivenessScope() constructor usable, so we can leave liveness_scope() intact.
py/server/tests/testbase.py
Outdated
@@ -35,9 +35,11 @@ def tearDownClass(cls) -> None: | |||
|
|||
def setUp(self) -> None: | |||
self._liveness_scope = liveness_scope() | |||
self.opened_scope = liveness_scope().open() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This syntax feels worse than what we had before. Can it be improved?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What we had before was too simple, and prevented an important use case: Keeping a LivenessScope around in order to fully control the life cycle of the objects created under it, and/or re-using the same LivenessScope on multiple calls. That is, the old API was simply not fit for purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This particular case is bad, because apparently the unittest "fixtures" can't be wrapped in contextmanagers or a with
expr, etc - you can only explicitly open something, then close it later. That isn't a pattern we want to encourage here, since you might screw up and not close it. Apparently pytest does this better, closer to how junit solves this.
The other changes made in this patch's tests are more representative of how we expect this to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.
Co-authored-by: Chip Kent <5250374+chipkent@users.noreply.github.com>
Co-authored-by: Chip Kent <5250374+chipkent@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of call with @chipkent and @jmao-denver:
Two use cases to support, named "A2" and "B2" on the call:
Simplest example, where a scope is created+opened for a with
block, and closed+released at the end:
with liveness_scope(): # optional "as scope"
# do things here that will be managed by the scope
...
This is the existing factory method, and the use case that it was intended for. The return value of that method is a LivenessScope, and can only be used to manage(...)
/unmanage(...)
or preserve(...)
during that block.
Richer case, where the lifetime of the scope is longer than the time in which it was at the top of the liveness stack":
scope = LivenessScope()
with scope.open():
# do things here that will be managed by the scope
...
# later at some point release the scope (can be re-opened in the meantime)
scope.release()
Instead of the factory method, this uses a constructor and an open()
instance method. Scopes can be opened more than once, and must then be released when the user is finished with them.
The liveness_scope()
method will return a specific type that only supports manage/unmanage/preserve, whereas the LivenessScope()
constructor will also support the open()
and release()
methods.
self.assertTrue(must_keep.j_table.tryRetainReference()) | ||
# drop extra reference | ||
must_keep.j_table.dropReference() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The liveness_scope
pydoc indicates that @liveness_scope
can be used to control scope, but I don't see a test for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added (and otherwise cleaned up tests a bit).
typehints were wrong for both contextmanager usages, i fixed them as well.
Co-authored-by: Chip Kent <5250374+chipkent@users.noreply.github.com>
Co-authored-by: Chip Kent <5250374+chipkent@users.noreply.github.com>
Co-authored-by: Chip Kent <5250374+chipkent@users.noreply.github.com>
b7c21e1
to
5ddb965
Compare
Labels indicate documentation is required. Issues for documentation have been opened: How-to: https://github.com/deephaven/deephaven.io/issues/3277 |
The existing Python APIs for Liveness were difficult to use correctly in cases more complex than a single script - this brings several parts of the Java LivenessScope type to Python to enable more advanced use cases. Rather than introduce a LivenessScopeStack, this adds an open() method to LivenessScope itself, and through deprecation we're encouraging its use over the liveness_scope() factory method being directly passed to a with block. Likewise, close() is deprecated in favor of release(), which has different semantics, and matches the name used in Java.
Also makes TableListenerHandle a JObjectWrapper so it can be more easily unwrapped, can't accidentally be managed as a LivePyObjectWrapper in Java.
Fixes #4471