Git Product home page Git Product logo

app-model's People

Contributors

aganders3 avatar alisterburt avatar czaki avatar dalthviz avatar davidbrochart avatar dependabot[bot] avatar kne42 avatar lucyleeow avatar pre-commit-ci[bot] avatar psobolewskiphd avatar tlambert03 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

app-model's Issues

'destroy' method for `QModelMenu`

Description

Does QModelMenu have a method similar to _destroy in NapariMenu:

    def _destroy(self):
        """Clean up action data to avoid widget leaks."""
        for ax in self.actions():
            ax.setData(None)

            with contextlib.suppress(AttributeError):
                ax._destroy()
        if self in self._INSTANCES:
            self._INSTANCES.remove(self)

If not, would it be suitable to add it? I think (?) ideally it would perform the setData(None) and the QAction._destroy() methods. (I think QModelMenu save a list of instances in _INSTANCES so I don't think that part would be needed).

My specific use case is for use during Window._teardown() in tests.

cc @DragaDoncila

Bug in _disconnect functions on QObjects

some more details ... ultimately, I think this bug is due to a failure in the disconnection strategy i mentioned above in #147 (comment).

If you add self.sample_menu.destroyed.connect(lambda: print("DESTROY")) somewhere, you'll see that the destroyed
event IS indeed emitted, but the intended _disconnect callback is never getting called (and so self._app.menus.menus_changed.disconnect(self._on_registry_changed) is never getting called either). I think this probably means that the strategy of having a QObject connect its own method to its destroyed signal isn't going to work (presumably that callback method itself is cleaned up and no longer called).

If you try to outsmart it by using a locally defined function closing on the QObject:

        @self.destroyed.connect
        def _disconnect() -> None:
            self._app.menus.menus_changed.disconnect(self._on_registry_changed)

then the _disconnect function does get called, but then psygnal has an issue actually executing the disconnection cause it can no longer find the callback to disconnect it:

Traceback (most recent call last):
  File "/Users/talley/dev/self/app-model/src/app_model/backends/qt/_qmenu.py", line 58, in _disconnect
    self._app.menus.menus_changed.disconnect(self._on_registry_changed)
  File "src/psygnal/_signal.py", line 827, in disconnect
  File "src/psygnal/_signal.py", line 799, in _slot_index
  File "src/psygnal/_weak_callback.py", line 130, in weak_callback
  File "src/psygnal/_weak_callback.py", line 363, in __init__
  File "src/psygnal/_weak_callback.py", line 190, in __init__
  File "src/psygnal/_weak_callback.py", line 258, in object_key
RuntimeError: wrapped C/C++ object of type QModelSubmenu has been deleted

It's possible we could catch and special case that in psygnal itself (to handle the case where essentially all attribute access is no longer permitted on the previously connected callback)... but I need to look into that.

Originally posted by @tlambert03 in #147 (comment)

DOC Add context example

Add example of using ContextNamespace, ContextKey and create_context to create a class of related context keys.

FEATURE: add `partial` (args/kwargs) field to Action?

Should we add the ability store args/kwargs (like a partial) in an Action?

    action1 = Action(id="cmd_id", title="title1", callback=cb)
    action2 = Action(id="cmd_id2", title="title2", callback=cb, kwargs={"a": 1})

or should the callback just be the partial. Need to think about pros cons.
first thought: the "pro" of just using callback=partial(cb, a=1) is that it's familiar regular python. The "con" is that it (currently) would require us to create a new command id for every partial variant. Maybe that's not a big deal

Keep consistent order of modifiers ?

  • app-model version: 08d0fbd (Jul 5)
  • Python version: 3.10
  • Operating System: Mac OS

Description

Order of modifiers can be inconsistant, I think it would be good to always have shortcuts in an order that is consistant, an likely the one the current platform uses.

>>> str(KeyBinding.from_str('Meta+Alt'))
'Meta+Alt'

>>> str(KeyBinding.from_str('Meta+Alt+Shift'))
'Alt+Meta+Shift'

I'm workin on napari UI shortcut editor, and while editing a shortcut if I have Alt-Meta pressed, and pressed shift, it is weird to see Alt and Meta swap places.

In general there seem to be some reordering:

In [2]: str(KeyBinding.from_str('Meta+Alt'))
Out[2]: 'Meta+Alt'  # order unchanged from input

In [3]: str(KeyBinding.from_str('Alt+Meta'))
Out[3]: 'Alt+Meta'  # order unchanged from input

In [4]: str(KeyBinding.from_str('Alt+Meta+Shift'))
Out[4]: 'Alt+Meta+Shift' # order unchanged from input

In [5]: str(KeyBinding.from_str('Meta+Alt+Shift'))
Out[5]: 'Alt+Meta+Shift' # order changed ! from input

So another possibility is to keep the order that was passed in, but I think that would be more complicated.

It's unclear to me why this is happening as the __str__s are pretty straitforward and should keep the order:

    def __str__(self) -> str:
        return " ".join(str(part) for part in self.parts)

    def __str__(self) -> str:
        out = ""
        if self.ctrl:
            out += "Ctrl+"
        if self.shift:
            out += "Shift+"
        if self.alt:
            out += "Alt+"
        if self.meta:
            out += "Meta+"
        if self.key:
            out += str(self.key)
        return out

So Im pretty sure there is a right in my face that I don't see, and it's likely related to having modifiers only shortcuts while starting to type a chord that I'm not seeing.

DOC Add `Expr` background and supported expressions

There is some great info in the src/app_model/expressions/_expressions.py module docstring, which is unfortunately not included in the documentation. Would you consider including this / is there a way to easily do this in the current documentation system?

Also would a list of supported comparisons / ops be useful?

Idea: declarative as config, instead of code

I really like where we are going in terms of making GUI declarative, along the way there are some quite notable things you did leading us here. (magic GUI, manifest, etc.).

I would want to throw this idea out to move this one step forward. Why not declare the GUI as config files instead of python code? Coming from the java world, I liked the spring framework of managing objects (which we are also doing the injection part lightweight via in-n-out, thanks!) And I would say there are additional benefits if we switch to file declaration from code declaration. (and I am in no way trying to say we should follow spring to use XML as the format)

  • Developers can then run schema checks without additional tooling
  • versioning can be more explicit by marking them out, potentially having better multi-version support
  • simplify some verbosity with coding

Take the example app as an example. actions.py can be rewritten to actions.yml. We can probably drop the total amount of text by 50%+

and by having the declaration via a config file, the downstream application can declare a set of things supported instead of being injected directly. For example, the app can also declare in the configuration that "Ctrl is reserved for system use and no one can override," etc.

Error instantiating QModelMenu with PySide6 6.5.0

  • app-model version: main (0.1.2)
  • Python version: 3.10.10
  • Operating System: macOS 13.3

Description

There is a problem instantiating the QModelMenu with PySide 6.5.0. It looks like multiple inheritance gets a bit trickier here as now QWidget is effectively calling super().__init__? This feels like a pretty big change so perhaps it should be reported on the Qt bug tracker.

Try this demo script to see what I mean:

# qwidget_demo.py
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QApplication, QWidget

app = QApplication([])


class A(QObject):
    def __init__(self, *args, **kwargs):
        print("A.__init__", args, kwargs)


class B(QWidget, A):
    def __init__(self, *args, **kwargs):
        QWidget.__init__(self, *args, **kwargs)
        print("B.__init__", args, kwargs)


print("instantiating A")
A()

print("instantiating B")
B()

output with PySide 6.4.2

❯ python qwidget_demo.py 
instantiating A
A.__init__ () {}
instantiating B
B.__init__ () {}

output with PySide 6.5.0

❯ python qwidget_demo.py 
instantiating A
A.__init__ () {}
instantiating B
A.__init__ () {}
B.__init__ () {}

What I Did

Create a virtual environment with the newly released PySide6 v6.5.0:

% python -m venv venv
% source venv/bin/activate
% pip install pyside6==6.5.0
% pip install -e '.[qt,test,test-qt]'
% pytest

Tests are failing for QMenu with this rather confusing output:

self = <[RuntimeError("'__init__' method of object's base class (QModelMenu) not called.") raised in repr()] QModelMenu object at 0x12e46ae00>, menu_id = 'help'
app = Application('complete_test_app'), title = None, parent = None

    def __init__(
        self,
        menu_id: str,
        app: Union[str, Application],
        title: Optional[str] = None,
        parent: Optional[QWidget] = None,
    ):
>       QMenu.__init__(self, parent)
E       TypeError: _MenuMixin.__init__() missing 1 required positional argument: 'app'

src/app_model/backends/qt/_qmenu.py:144: TypeError

Possible solution?

I'm not sure what the best solution is here - I don't have a ton of experience with multiple inheritance or mixins. One thing that seems to work is to give _MenuMixin a dummy __init__ like this, but it doesn't necessarily feel like the right way to do it:

--- a/src/app_model/backends/qt/_qmenu.py
+++ b/src/app_model/backends/qt/_qmenu.py
@@ -44,7 +44,10 @@ class _MenuMixin(QObject):
     _app: Application
     _menu_id: str
 
-    def __init__(
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def _init_mixin(
         self,
         menu_id: str,
         app: Union[str, Application],
@@ -141,8 +144,9 @@ class QModelMenu(QMenu, _MenuMixin):
         title: Optional[str] = None,
         parent: Optional[QWidget] = None,
     ):
+        print(QModelMenu.__mro__)
         QMenu.__init__(self, parent)
-        _MenuMixin.__init__(self, menu_id, app)
+        self._init_mixin(menu_id, app)
         if title is not None:
             self.setTitle(title)
         self.aboutToShow.connect(self._on_about_to_show)
@@ -217,7 +221,7 @@ class QModelToolBar(QToolBar, _MenuMixin):
     ) -> None:
         self._exclude = exclude
         QToolBar.__init__(self, parent)
-        _MenuMixin.__init__(self, menu_id, app)
+        self._init_mixin(menu_id, app)
         if title is not None:
             self.setWindowTitle(title)

Support of in-n-out `inject` parameters

I think app-model does not currently support in-n-out inject parameters such as:

  • inject : on_unresolved_required_args, on_unannotated_required_args, guess_self
  • inject_processors: first_processor_only, raise_exception

I have no current use of this. Just wanted to confirm and know what the future plans were, for some napari app-model documentation (that I will one day write 😬 ).

make KeyCode.__or__ like KeyMod

clever implementation

however, nitpick, reversing this does not result in a KeyCombo:

In [1]: from app_model.types import KeyCode, KeyMod

In [2]: KeyMod.CtrlCmd | KeyCode.KeyM
Out[2]: <KeyCombo.CtrlCmd|KeyM: 2078>

In [3]: KeyCode.KeyM | KeyMod.CtrlCmd
Out[3]: 2078

Originally posted by @kne42 in napari/napari#4784 (comment)

app-model 0.2.4 breaks napari tests

  • app-model version: 0.2.4
  • Python version: 3.8, 3.11
  • Operating System: Linux

Description

Autogenerate PR for bump test constraints fails on the test because of the bump to app-model 0.2.4

napari/napari#6478

https://github.com/napari/napari/actions/runs/7320326962/job/19939301830?pr=6478

Test are passing on 0.2.3

A short investigation shows that 0.2.4 introduces the breaking change in #146. Could I ask to yank this release from Pypi and revert this change (or release it as 0.3.0)

Keybinding comparing itself to `str` is really weird.

  • app-model version: 0.1.4
  • Python version: 3.10
  • Operating System: Mac

Description

I see what you are doing with Keybinding, and trying to make it magically work with strings, but I think it's a footgun in the long run.

What I Did

In [1]: from app_model.types import KeyBinding

In [2]: X = [KeyBinding.from_str('A')]

In [3]: [isinstance(s, str) for s in X]
Out[3]: [False]

In [4]: 'A' in X
Out[4]: True

For me Out[3] logically implies that In [4] Cannot be True. And equal but hash is different is a footgun as well:

In [7]: d = {}

In [8]: d[X[0]] = 1

In [9]: d['A'] # must be 1 as X[0] == 'A' ?

No KeyError.

I think this is make worse by the fact that __str__ return the keybinding in the string form as now code in napari liberally use the object directly in places where it's implicitely str()'d.

Should Kebindings.from_string fail on invalid/unkown keys ?

  • app-model version: latest

Description

I was surprised by the following:

In [1]: str(KeyBinding.from_str('Meta+Shift+Return'))
Out[1]: 'Shift+Meta+'

A realized that Return is not a handled key, should we have a warning/error on invalid values ?

idea: add user-facing rendering to keybindings

in napari, we use a system to render shortcuts with special characters (e.g. the Mac Command symbol for Cmd). I’m on my phone right now so it’s hard for me to show what I mean but I hope you understand the gist

I was thinking that this sort of almost “text rendering” should belong in app-model since I imagine it has a pretty common use case

what do you think @tlambert03? I’d like to work on this but wanted to check first if people like the idea

Editable keybinding

Currently, Action is frozen and does not allow editing keybinding.
So either, next to default keybinding there should be an option to provide overwrite of keybinding, or builders (like QModelMenu) should accept dict with overwrite of keybindings (action id to keybind like in napari settings).

The third option is to store key bind overwrite in Application instance, as it is already passed and used.

No `findAction` in `QModelSubmenu`

There is no findAction method for QModelSubmenu. Should we be using QModelMenu to build submenus? I don't think you findAction on a parent QModelMenu will work for finding submenu actions.

Thanks

Does app model support unregistering of a keybinding?

  • app-model version: 0.1.1

  • Python version: 3.9

  • Operating System: macOS

Description

I am trying to unbind the keybinding from previously binding action, but seems no avail

What I Did

unbinds = []
for keybinding in self._app.keybindings._keybindings:
  if keybinding.command_id == current_action:
  unbinds.append(keybinding)

  print(f"Before {len(self._app.keybindings._keybindings)}")
  print(f'len {len(unbinds)}')
  for unbind in unbinds:
    self._app.keybindings._keybindings.remove(unbind)
  print(f"After {len(self._app.keybindings._keybindings)}")

the print statement correctly reported 4 before, 1 to be dropped, and 3 after. however the old keybinding still seem to be in effect.

looking at the dispose method it seems also doing similar things. is this expected?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.