Skip to content

Commit db7407a

Browse files
Czakipre-commit-ci[bot]tlambert03
authored
fix: Do not use lambda in QMenuItemAction.destroyed callback (#183)
* do not use lambda in destroyed callback * style: [pre-commit.ci] auto fixes [...] * change caching pattern * remove old signal * use WeakValueDictionary * use parent * improve cleaning * fix type error * Apply suggestions from code review * style: [pre-commit.ci] auto fixes [...] --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Talley Lambert <[email protected]>
1 parent f8b7406 commit db7407a

File tree

3 files changed

+55
-38
lines changed

3 files changed

+55
-38
lines changed

src/app_model/backends/qt/_qaction.py

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import contextlib
44
from typing import TYPE_CHECKING, ClassVar, Mapping
5+
from weakref import WeakValueDictionary
56

67
from app_model import Application
78
from app_model.expressions import Expr
@@ -13,6 +14,7 @@
1314
if TYPE_CHECKING:
1415
from PyQt6.QtGui import QAction
1516
from qtpy.QtCore import QObject
17+
from typing_extensions import Self
1618

1719
from app_model.types import CommandRule, MenuItem
1820
else:
@@ -118,52 +120,64 @@ class QMenuItemAction(QCommandRuleAction):
118120
119121
Mostly the same as a `CommandRuleAction`, but aware of the `menu_item.when` clause
120122
to toggle visibility.
121-
"""
122-
123-
_cache: ClassVar[dict[tuple[int, int], QMenuItemAction]] = {}
124123
125-
def __new__(
126-
cls: type[QMenuItemAction],
127-
menu_item: MenuItem,
128-
app: Application | str,
129-
parent: QObject | None = None,
130-
*,
131-
cache: bool = True,
132-
) -> QMenuItemAction:
133-
"""Create and cache a QMenuItemAction for the given menu item."""
134-
app = Application.get_or_create(app) if isinstance(app, str) else app
135-
key = (id(app), hash(menu_item))
136-
if cache and key in cls._cache:
137-
return cls._cache[key]
124+
Parameters
125+
----------
126+
menu_item : MenuItem
127+
`MenuItem` instance to create an action for.
128+
app : Application | str
129+
Application instance or name of application instance.
130+
parent : QObject | None
131+
Optional parent widget, by default None
132+
"""
138133

139-
self = super().__new__(cls)
140-
if cache:
141-
cls._cache[key] = self
142-
return self # type: ignore [no-any-return]
134+
_cache: ClassVar[WeakValueDictionary[tuple[int, int], QMenuItemAction]] = (
135+
WeakValueDictionary()
136+
)
143137

144138
def __init__(
145139
self,
146140
menu_item: MenuItem,
147141
app: Application | str,
148142
parent: QObject | None = None,
149-
*,
150-
cache: bool = True, # used in __new__
151143
):
152-
initialized = False
153-
with contextlib.suppress(RuntimeError):
154-
initialized = getattr(self, "_initialized", False)
155-
156-
if not initialized:
157-
super().__init__(menu_item.command, app, parent)
158-
self._menu_item = menu_item
159-
key = (id(self._app), hash(menu_item))
160-
self.destroyed.connect(lambda: QMenuItemAction._cache.pop(key, None))
161-
self._app.destroyed.connect(lambda: QMenuItemAction._cache.pop(key, None))
162-
self._initialized = True
163-
144+
super().__init__(menu_item.command, app, parent)
145+
self._menu_item = menu_item
164146
with contextlib.suppress(NameError):
165147
self.update_from_context(self._app.context)
166148

149+
@staticmethod
150+
def _cache_key(app: Application, menu_item: MenuItem) -> tuple[int, int]:
151+
return (id(app), hash(menu_item))
152+
153+
@classmethod
154+
def create(
155+
cls,
156+
menu_item: MenuItem,
157+
app: Application | str,
158+
parent: QObject | None = None,
159+
) -> Self:
160+
"""Create a new QMenuItemAction for the given menu item.
161+
162+
Prefer this method over `__init__` to ensure that the cache is used,
163+
so that:
164+
165+
```python
166+
a1 = QMenuItemAction.create(action, full_app)
167+
a2 = QMenuItemAction.create(action, full_app)
168+
a1 is a2 # True
169+
```
170+
"""
171+
app = Application.get_or_create(app) if isinstance(app, str) else app
172+
cache_key = QMenuItemAction._cache_key(app, menu_item)
173+
if cache_key in cls._cache:
174+
res = cls._cache[cache_key]
175+
res.setParent(parent)
176+
return res
177+
178+
cls._cache[cache_key] = obj = cls(menu_item, app, parent)
179+
return obj
180+
167181
def update_from_context(self, ctx: Mapping[str, object]) -> None:
168182
"""Update the enabled/visible state of this menu item from `ctx`."""
169183
super().update_from_context(ctx)

src/app_model/backends/qt/_qmenu.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,10 @@ def _rebuild(
305305
exclude: Collection[str] | None = None,
306306
) -> None:
307307
"""Rebuild menu by looking up `menu` in `Application`'s menu_registry."""
308-
menu.clear()
308+
actions = menu.actions()
309+
for action in actions:
310+
menu.removeAction(action)
311+
309312
_exclude = exclude or set()
310313

311314
groups = list(app.menus.iter_menu_groups(menu_id))
@@ -317,7 +320,7 @@ def _rebuild(
317320
submenu = QModelSubmenu(item, app, parent=menu)
318321
cast("QMenu", menu).addMenu(submenu)
319322
elif item.command.id not in _exclude:
320-
action = QMenuItemAction(item, app=app, parent=menu)
323+
action = QMenuItemAction.create(item, app=app, parent=menu)
321324
menu.addAction(action)
322325
if n < n_groups - 1:
323326
menu.addSeparator()

tests/test_qt/test_qactions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def test_cache_qaction(qapp, full_app: "FullApp") -> None:
1313
action = next(
1414
i for k, items in full_app.menus for i in items if isinstance(i, MenuItem)
1515
)
16-
a1 = QMenuItemAction(action, full_app)
17-
a2 = QMenuItemAction(action, full_app)
16+
a1 = QMenuItemAction.create(action, full_app)
17+
a2 = QMenuItemAction.create(action, full_app)
1818
assert a1 is a2
1919
assert repr(a1).startswith("QMenuItemAction")
2020

0 commit comments

Comments
 (0)