diff --git a/src/app_model/_app.py b/src/app_model/_app.py index 65507eb..8df3e7a 100644 --- a/src/app_model/_app.py +++ b/src/app_model/_app.py @@ -4,6 +4,7 @@ import os import sys from collections.abc import Iterable, MutableMapping +from types import MappingProxyType from typing import ( TYPE_CHECKING, ClassVar, @@ -126,6 +127,7 @@ def __init__( self.injection_store.on_unannotated_required_args = "ignore" + self._registered_actions: dict[str, Action] = {} self._disposers: list[tuple[str, DisposeCallable]] = [] @property @@ -291,3 +293,42 @@ def _dispose() -> None: d.pop()() return _dispose + + @property + def registered_actions(self) -> MappingProxyType[str, Action]: + """Return a Mapping of id->Action object for all registered actions. + + Note that this only includes actions that were registered using + `register_action`. Commands registered directly via + `Application.commands.register_action` will not be included in this mapping. + """ + return MappingProxyType(self._registered_actions) + + def _register_action_obj(self, action: Action) -> DisposeCallable: + """Register an Action object. Return a function that unregisters the action. + + Helper for `register_action()`. + """ + # register commands + disposers = [self.commands.register_action(action)] + # register menus + if dm := self.menus.append_action_menus(action): + disposers.append(dm) + # register keybindings + if dk := self.keybindings.register_action_keybindings(action): + disposers.append(dk) + + # remember the action object as a whole. + # note that commands.register_action will have raised an exception + # if the action.id is already registered, so we can assume that + # the keys are unique. + self._registered_actions[action.id] = action + + # create a function that will dispose of all the disposers + def _dispose() -> None: + self._registered_actions.pop(action.id, None) + for d in disposers: + d() + + self._disposers.append((action.id, _dispose)) + return _dispose diff --git a/src/app_model/registries/_register.py b/src/app_model/registries/_register.py index 1712d14..6186495 100644 --- a/src/app_model/registries/_register.py +++ b/src/app_model/registries/_register.py @@ -269,19 +269,4 @@ def _register_action_obj(app: Application | str, action: Action) -> DisposeCalla from app_model._app import Application app = app if isinstance(app, Application) else Application.get_or_create(app) - - # commands - disposers = [app.commands.register_action(action)] - # menus - if dm := app.menus.append_action_menus(action): - disposers.append(dm) - # keybindings - if dk := app.keybindings.register_action_keybindings(action): - disposers.append(dk) - - def _dispose() -> None: - for d in disposers: - d() - - app._disposers.append((action.id, _dispose)) - return _dispose + return app._register_action_obj(action) diff --git a/src/app_model/types/__init__.py b/src/app_model/types/__init__.py index 39a6530..72acf38 100644 --- a/src/app_model/types/__init__.py +++ b/src/app_model/types/__init__.py @@ -19,7 +19,9 @@ from ._menu_rule import MenuItem, MenuItemBase, MenuRule, SubmenuItem if TYPE_CHECKING: - from typing import Callable, TypeAlias + from typing import Callable + + from typing_extensions import TypeAlias from ._icon import IconOrDict as IconOrDict from ._keybinding_rule import KeyBindingRuleDict as KeyBindingRuleDict diff --git a/tests/test_app.py b/tests/test_app.py index 61d7940..8f43cfb 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -8,6 +8,8 @@ from app_model import Application from app_model.expressions import Context +from app_model.types import Action +from app_model.types._menu_rule import MenuRule if TYPE_CHECKING: from conftest import FullApp @@ -115,3 +117,18 @@ def test_app_context() -> None: with pytest.raises(TypeError, match="context must be a Context or MutableMapping"): Application("app4", context=1) # type: ignore[arg-type] + + +def test_register_actions() -> None: + app = Application("app5") + actions = app.registered_actions + assert not actions + dispose = app.register_action( + "my_action", title="My Action", callback=lambda: None, menus=["Window"] + ) + assert "my_action" in actions + assert isinstance(action := actions["my_action"], Action) + assert action.menus == [MenuRule(id="Window")] + dispose() + assert "my_action" not in actions + assert not actions