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 option to check if plugin try to set viewer attr outside main thread #5195

Merged
merged 27 commits into from
Nov 2, 2022

Conversation

Czaki
Copy link
Collaborator

@Czaki Czaki commented Oct 9, 2022

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 with PublicOnlyProxy, 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 from napari._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

  • Bug-fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

References

How has this been tested?

  • example: the test suite for my feature covers cases x, y, and z
  • example: all tests pass with my change
  • example: I check if my changes works with both PySide and PyQt backends
    as there are small differences between the two Qt bindings.

Final checklist:

  • My PR is the minimum possible work for the desired functionality
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • If I included new strings, I have used trans. to make them localizable.
    For more information see our translations guide.

@github-actions github-actions bot added qt Relates to qt tests Something related to our tests labels Oct 9, 2022
@codecov
Copy link

codecov bot commented Oct 9, 2022

Codecov Report

Merging #5195 (9492984) into main (6ba16c1) will increase coverage by 0.19%.
The diff coverage is 97.22%.

@@            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     
Impacted Files Coverage Δ
napari/utils/_testsupport.py 63.63% <83.33%> (+2.02%) ⬆️
napari/_qt/_tests/test_proxy_fixture.py 100.00% <100.00%> (ø)
napari/_qt/_tests/test_qt_utils.py 100.00% <100.00%> (ø)
napari/_qt/utils.py 80.19% <100.00%> (+0.19%) ⬆️
napari/plugins/_tests/test_npe2.py 100.00% <100.00%> (ø)
napari/utils/_proxies.py 94.93% <100.00%> (+1.28%) ⬆️
napari/utils/_tests/test_proxies.py 100.00% <100.00%> (ø)
napari/components/experimental/chunk/_pool.py 85.71% <0.00%> (-7.94%) ⬇️
napari/utils/io.py 72.00% <0.00%> (-6.95%) ⬇️
napari/layers/tracks/_track_utils.py 87.50% <0.00%> (-3.41%) ⬇️
... and 14 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@Czaki Czaki changed the title Add option to check if pluugin try to set viewer attr outside main thread Add option to check if plugin try to set viewer attr outside main thread Oct 9, 2022
Copy link
Member

@andy-sweet andy-sweet left a 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/_qt/utils.py Outdated Show resolved Hide resolved
typ = type(self.__wrapped__).__name__
self._private_attr_warning(name, typ)

# raise AttributeError
Copy link
Member

Choose a reason for hiding this comment

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

Is this a leftover?

Copy link
Collaborator Author

@Czaki Czaki Oct 10, 2022

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,

napari/utils/_proxies.py Outdated Show resolved Hide resolved
napari/utils/_tests/test_proxies.py Outdated Show resolved Hide resolved
napari/utils/_tests/test_proxies.py Outdated Show resolved Hide resolved
thread_flag : bool
True if we are in the main thread, False otherwise.
"""
return QCoreApplication.instance().thread() == QThread.currentThread()
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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?

Copy link
Contributor

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.

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():
Copy link
Member

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/_tests/test_proxies.py Outdated Show resolved Hide resolved
@Czaki Czaki requested a review from andy-sweet October 10, 2022 19:27
@@ -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):
Copy link
Contributor

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. :)

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

@Czaki
Copy link
Collaborator Author

Czaki commented Oct 11, 2022

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.

a = 1

monkeypatch.setenv('NAPARI_ENSURE_PLUGIN_MAIN_THREAD', 'True')
single_threaded_executor = ThreadPoolExecutor(max_workers=1)
Copy link
Member

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()

Comment on lines 76 to 77
if self._is_private_attr(name):
if self._is_called_from_napari():
Copy link
Member

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
Copy link
Member

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.

Copy link
Collaborator Author

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.

Copy link
Member

@andy-sweet andy-sweet Oct 12, 2022

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes

@andy-sweet andy-sweet added this to the 0.4.17 milestone Oct 12, 2022
@andy-sweet
Copy link
Member

The warning for setattr looks great and should definitely come into v0.4.17. How strongly do you feel about bringing in the thread checks? I still feel like splitting this PR in two could be the best way to go here.

@Czaki Czaki reopened this Oct 14, 2022
@github-actions github-actions bot added qt Relates to qt tests Something related to our tests labels Oct 14, 2022
Copy link
Member

@andy-sweet andy-sweet left a 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):
Copy link
Member

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.

Copy link
Collaborator Author

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.

@Czaki
Copy link
Collaborator Author

Czaki commented Oct 27, 2022

merge this requires napari/docs#7 to be merged also

@andy-sweet
Copy link
Member

@Czaki : as this has two PRs to coordinate (this + docs), feel free to merge both when you'r ready.

@Czaki
Copy link
Collaborator Author

Czaki commented Nov 1, 2022

@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?

@Czaki Czaki added feature New feature or request and removed documentation labels Nov 1, 2022
Copy link
Member

@andy-sweet andy-sweet left a 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.

napari/utils/_tests/test_proxies.py Outdated Show resolved Hide resolved
@Czaki Czaki requested a review from andy-sweet November 2, 2022 00:05
Copy link
Member

@andy-sweet andy-sweet left a 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.

@Czaki Czaki merged commit 505df71 into napari:main Nov 2, 2022
@Czaki Czaki deleted the feature/debug_main_thread branch November 2, 2022 09:38
@Czaki Czaki modified the milestones: 0.5.0, 0.4.18 Jun 23, 2023
Czaki added a commit that referenced this pull request Jun 23, 2023
…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>
@Czaki Czaki mentioned this pull request Jun 24, 2023
Czaki added a commit that referenced this pull request Jun 30, 2023
…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()
Copy link
Contributor

@lucyleeow lucyleeow Jul 5, 2023

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request qt Relates to qt tests Something related to our tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants