-
-
Notifications
You must be signed in to change notification settings - Fork 419
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 option to check if plugin try to set viewer attr outside main thread #5195
Conversation
Codecov Report
@@ Coverage Diff @@
## main #5195 +/- ##
==========================================
+ Coverage 88.84% 89.03% +0.19%
==========================================
Files 579 579
Lines 49109 49009 -100
==========================================
+ Hits 43629 43634 +5
+ Misses 5480 5375 -105
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
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 looks good, though I'd like to see if we could do this without Qt and there may be an opportunity simplify a few things in the implementation and tests. None of my comments are strong, so feel free to push back and I'll likely approve.
Ideally, this would be two PRs - one for the private __setattr__
warning and one for the non-main thread __setattr__
exception. I guess the non-main thread code is only executed if an environment variable is defined, so I'd be OK bringing this altogether.
napari/utils/_proxies.py
Outdated
typ = type(self.__wrapped__).__name__ | ||
self._private_attr_warning(name, typ) | ||
|
||
# raise AttributeError |
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.
Is this a leftover?
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.
same as in __getattr__
. When we remove private access there should be raised an exception about this,
thread_flag : bool | ||
True if we are in the main thread, False otherwise. | ||
""" | ||
return QCoreApplication.instance().thread() == QThread.currentThread() |
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.
Could we rely on Python's threading.current_thread
and threading.main_thread
to implement this function in a Qt-independent way and define it outside of _qt
?
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 do not think earlier about main_thread.
In general it is not good solution (as main thread in python is thread in which start interpreter, and Qt main threat is thread where Application object is created.
But as it is for debugging purpose, then, with proper explanation it could be done in this way. I need to rethink this.
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 we expect that the interpreter's main thread to sometimes be different from the thread in which the application is created, then maybe this should be called in_qt_main_thread
or in_gui_thread
?
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 do not expect but this is possible. But I'm not sure if we need to support it.
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 second Andy's sentiments here. Mostly, it'd be nice to new Qt dependancies in otherwise non-graphical code.
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.
So you suggest implementing this in pure python? So, where put the documentation that is required to create a QApplication object in the python main thread?
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 know. I don't have strong feelings about it, especially with this sitting in a qt
directory.
napari/utils/_proxies.py
Outdated
if os.environ.get("NAPARI_ENSURE_PLUGIN_MAIN_THREAD", False): | ||
from napari._qt.utils import check_if_in_main_thread | ||
|
||
if not check_if_in_main_thread(): |
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.
Given this class is named PublicOnlyProxy
, I'm not sure this is the right place for this check. But it is convenient and I can't think of a better approach right now. I also can't think of a good name that would describe both checks, so I think it's fine to leave it as is for now - especially since it's guarded with the environment variable.
napari/utils/_proxies.py
Outdated
@@ -73,6 +92,33 @@ def __getattr__(self, name: str): | |||
# ) | |||
return self.create(super().__getattr__(name)) | |||
|
|||
def __setattr__(self, name: str, value: Any): | |||
if os.environ.get("NAPARI_ENSURE_PLUGIN_MAIN_THREAD", False): |
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.
Is this something that could go into settings instead?
(e.g. how experimental octtree is configured)
I'm not a fan of having behavior deep into napari changed based on external configurations. (But, I don't have a strong conviction here. :)
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.
Is this something that could go into settings instead?
But this is for debugging purposes for the plugin creators. Not something that a common user may want to use.
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.
Perhaps a dev section in the settings? The target -- devs or users -- doesn't change my feeling one continuous querying of the environment.
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.
Checking documentation https://docs.python.org/3/library/os.html#os.environ, and it is a pure dict query. It will be faster than querying settings.
I have implemented an alternative approach when Qt check will be used only if available, but the code will work even if Qt is unavailable. |
napari/_qt/_tests/test_qt_utils.py
Outdated
a = 1 | ||
|
||
monkeypatch.setenv('NAPARI_ENSURE_PLUGIN_MAIN_THREAD', 'True') | ||
single_threaded_executor = ThreadPoolExecutor(max_workers=1) |
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.
Probably best to make this a fixture that calls shutdown
on teardown (credit to @kcpevey):
@pytest.fixture()
def single_threaded_executor():
executor = ThreadPoolExecutor(max_workers=1)
yield executor
executor.shutdown()
Or use the executor in a context when it's needed:
with ThreadPoolExecutor() as executor:
f = executor.submit(x.__setattr__, 'a', 2)
f.result()
assert x.a == 2
f = executor.submit(x_proxy.__setattr__, 'a', 3)
with pytest.raises(RuntimeError):
f.result()
napari/utils/_proxies.py
Outdated
if self._is_private_attr(name): | ||
if self._is_called_from_napari(): |
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 love how readable this is now!
True if we are in the main thread, False otherwise. | ||
""" | ||
|
||
global in_main_thread |
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.
Why do we need the global? Is this so that we only execute this function once for performance reasons?
This function is only called when an env variable is set, so I don't think we need to worry to much about performance. So if that's the only reason to do this, I'd suggest removing the global and just defining this function as in_main_thread
.
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, this is for performance reasons.
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.
Then I'd definitely suggest simplify this to:
def in_main_thread() -> bool:
try:
from napari._qt.utils import in_qt_main_thread
return in_qt_main_thread()
except ImportError:
return in_main_thread_py()
except AttributeError:
warnings.warn(
"Qt libs are available but no QtApplication instance is created"
)
return in_main_thread_py()
Does that make sense?
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
The warning for |
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.
Looks good - thanks for the updates!
@@ -261,6 +261,38 @@ def actual_factory( | |||
warnings.warn(msg) | |||
|
|||
|
|||
@pytest.fixture | |||
def make_napari_viewer_proxy(make_napari_viewer, monkeypatch): |
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.
Is this used anywhere? If not, did you use it to help debug this PR?
If it's not used, but you think it's useful, you might want to add a comment to clarify that this is useful for debugging and should not be deleted.
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 is a replacement for make_napari_viewer
for plugins creators and is referred in my PR to docs https://github.com/napari/docs/pull/7/files
Currently, plugins during tests do not have warnings about private attribute access, only during actual execution.
merge this requires napari/docs#7 to be merged also |
@Czaki : as this has two PRs to coordinate (this + docs), feel free to merge both when you'r ready. |
@andy-sweet I have added a test. Could you check it? |
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.
New test looks good - except for the name of it! I'll approve after you change that.
I also suggested an alternate more concise implementation of the test. That's optional.
Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com>
for more information, see https://pre-commit.ci
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.
Looks great! Feel free to merge when you want.
…ead (#5195) Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
…ead (#5195) Co-authored-by: Andy Sweet <andrew.d.sweet@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@@ -19,6 +19,9 @@ | |||
|
|||
@pytest.fixture | |||
def mock_pm(npe2pm: 'TestPluginManager'): | |||
from napari.plugins import _initialize_plugins | |||
|
|||
_initialize_plugins.cache_clear() |
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.
@Czaki could you please explain why this is needed (I can't work it out)? Thank you!
Description
When reading #5142 I remind that in the past, we met problems that may lead to application crashes if the viewer attribute is modified outside the main thread.
In this PR I add
NAPARI_ENSURE_PLUGIN_MAIN_THREAD
environment variable that, if set, enables the check of the thread from which the object is modified. This modification works only for objects wrapped withPublicOnlyProxy
, which is how the viewer is passed to plugins in napari. Not all modifications outside the main thread may be caught. It is possible, that use of some napari function may lead to this (as the wrapper is removed when moving to the napari code).This PR introduces dependency of
napari.utils._proxies
fromnapari._qt
but only if the environment variable is set.Add this PR to the main will allow the plugin creator to check if they do such an operation. For example, when they observe a random crash of napari. Example of such crash could be found here: #4982
This PR also warns to set private attributes in
PublicOnlyProxy
wrapped object. Currently, we only warn about getting its value.This PR requires to add documentation in `docs/plugins/debug_plugins.md] from #5142. So it may require an update if #5142 is merged first.
Type of change
References
How has this been tested?
as there are small differences between the two Qt bindings.
Final checklist:
trans.
to make them localizable.For more information see our translations guide.