diff --git a/pyproject.toml b/pyproject.toml index 36a2b94..5b72088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,7 @@ D100 # missing docstring in public module D105 # missing docstring in magic method D401 # imperative mood W503 # line break before binary operator +B010 """ per-file-ignores = [ "tests/*: D", diff --git a/src/app_model/__init__.py b/src/app_model/__init__.py index b16f6d6..9329376 100644 --- a/src/app_model/__init__.py +++ b/src/app_model/__init__.py @@ -6,5 +6,7 @@ __version__ = version("app-model") except PackageNotFoundError: # pragma: no cover __version__ = "uninstalled" -__author__ = "Talley Lambert" -__email__ = "talley.lambert@gmail.com" + +from ._app import Application + +__all__ = ["__version__", "Application"] diff --git a/src/app_model/_app.py b/src/app_model/_app.py new file mode 100644 index 0000000..4843e71 --- /dev/null +++ b/src/app_model/_app.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + ClassVar, + Dict, + List, + Literal, + Optional, + Tuple, + Union, + overload, +) + +from .registries import ( + CommandsRegistry, + KeybindingsRegistry, + MenusRegistry, + register_action, +) + +if TYPE_CHECKING: + from . import expressions + from .registries._commands import CommandCallable + from .registries._register import CommandDecorator + from .types import ( + Action, + CommandIdStr, + IconOrDict, + KeybindingRuleOrDict, + MenuRuleOrDict, + ) + from .types._misc import DisposeCallable + + +class Application: + """Full application model.""" + + _instances: ClassVar[Dict[str, Application]] = {} + + def __init__(self, name: str) -> None: + self._name = name + if name in Application._instances: + raise ValueError( + f"Application {name!r} already exists. Retrieve it with " + f"`Application.get_or_create({name!r})`." + ) + Application._instances[name] = self + + self.keybindings = KeybindingsRegistry() + self.menus = MenusRegistry() + self.commands = CommandsRegistry() + self._disposers: List[Tuple[CommandIdStr, DisposeCallable]] = [] + + @classmethod + def get_or_create(cls, name: str) -> Application: + """Get app named `name` or create and return a new one if it doesn't exist.""" + return cls._instances[name] if name in cls._instances else cls(name) + + @classmethod + def destroy(cls, name: str) -> None: + """Destroy the app named `name`.""" + cls._instances.pop(name, None) + + def __del__(self) -> None: + """Remove the app from the registry when it is garbage collected.""" + Application.destroy(self.name) + + @property + def name(self) -> str: + """Return the name of the app.""" + return self._name + + def __repr__(self) -> str: + return f"Application({self.name!r})" + + def dispose(self) -> None: + """Dispose of the app.""" + for _, dispose in self._disposers: + dispose() + self._disposers.clear() + + @overload + def register_action(self, id_or_action: Action) -> DisposeCallable: + ... + + @overload + def register_action( + self, + id_or_action: CommandIdStr, + title: str, + *, + run: Literal[None] = None, + category: Optional[str] = None, + tooltip: Optional[str] = None, + icon: Optional[IconOrDict] = None, + enablement: Optional[expressions.Expr] = None, + menus: Optional[List[MenuRuleOrDict]] = None, + keybindings: Optional[List[KeybindingRuleOrDict]] = None, + add_to_command_palette: bool = True, + ) -> CommandDecorator: + ... + + @overload + def register_action( + self, + id_or_action: CommandIdStr, + title: str, + *, + run: CommandCallable, + category: Optional[str] = None, + tooltip: Optional[str] = None, + icon: Optional[IconOrDict] = None, + enablement: Optional[expressions.Expr] = None, + menus: Optional[List[MenuRuleOrDict]] = None, + keybindings: Optional[List[KeybindingRuleOrDict]] = None, + add_to_command_palette: bool = True, + ) -> DisposeCallable: + ... + + def register_action( + self, + id_or_action: Union[CommandIdStr, Action], + title: Optional[str] = None, + *, + run: Optional[CommandCallable] = None, + category: Optional[str] = None, + tooltip: Optional[str] = None, + icon: Optional[IconOrDict] = None, + enablement: Optional[expressions.Expr] = None, + menus: Optional[List[MenuRuleOrDict]] = None, + keybindings: Optional[List[KeybindingRuleOrDict]] = None, + add_to_command_palette: bool = True, + ) -> Union[CommandDecorator, DisposeCallable]: + """Register an action and return a dispose function.""" + return register_action( + self, + id_or_action, # type: ignore + title=title, # type: ignore + run=run, # type: ignore + category=category, + tooltip=tooltip, + icon=icon, + enablement=enablement, + menus=menus, + keybindings=keybindings, + add_to_command_palette=add_to_command_palette, + ) diff --git a/src/app_model/registries/_commands.py b/src/app_model/registries/_commands.py index c26b758..6f06f2c 100644 --- a/src/app_model/registries/_commands.py +++ b/src/app_model/registries/_commands.py @@ -2,7 +2,7 @@ from concurrent.futures import Future, ThreadPoolExecutor from functools import cached_property -from typing import TYPE_CHECKING, Any, Callable, Optional +from typing import TYPE_CHECKING, Any, Callable from psygnal import Signal @@ -40,18 +40,10 @@ class CommandsRegistry: """Registry for commands (callable objects).""" registered = Signal(str) - __instance: Optional[CommandsRegistry] = None def __init__(self) -> None: self._commands: Dict[CommandIdStr, List[_RegisteredCommand]] = {} - @classmethod - def instance(cls) -> CommandsRegistry: - """Return global instance of the CommandsRegistry.""" - if cls.__instance is None: - cls.__instance = cls() - return cls.__instance - def register_command( self, id: CommandIdStr, diff --git a/src/app_model/registries/_keybindings.py b/src/app_model/registries/_keybindings.py index c2d7587..2ef114d 100644 --- a/src/app_model/registries/_keybindings.py +++ b/src/app_model/registries/_keybindings.py @@ -28,17 +28,9 @@ class KeybindingsRegistry: """Registery for keybindings.""" registered = Signal() - __instance: Optional[KeybindingsRegistry] = None def __init__(self) -> None: - self._coreKeybindings: List[_RegisteredKeyBinding] = [] - - @classmethod - def instance(cls) -> KeybindingsRegistry: - """Return global instance of the KeybindingsRegistry.""" - if cls.__instance is None: - cls.__instance = cls() - return cls.__instance + self._keybindings: List[_RegisteredKeyBinding] = [] def register_keybinding_rule( self, id: CommandIdStr, rule: KeybindingRule @@ -64,18 +56,18 @@ def register_keybinding_rule( weight=rule.weight, when=rule.when, ) - self._coreKeybindings.append(entry) + self._keybindings.append(entry) self.registered.emit() def _dispose() -> None: - self._coreKeybindings.remove(entry) + self._keybindings.remove(entry) return _dispose return None # pragma: no cover def __iter__(self) -> Iterator[_RegisteredKeyBinding]: - yield from self._coreKeybindings + yield from self._keybindings def __repr__(self) -> str: name = self.__class__.__name__ - return f"<{name} at {hex(id(self))} ({len(self._coreKeybindings)} bindings)>" + return f"<{name} at {hex(id(self))} ({len(self._keybindings)} bindings)>" diff --git a/src/app_model/registries/_menus.py b/src/app_model/registries/_menus.py index e60ac10..ad9b9eb 100644 --- a/src/app_model/registries/_menus.py +++ b/src/app_model/registries/_menus.py @@ -1,34 +1,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Optional +from typing import Callable, Dict, Iterator, List, Optional, Sequence, Set, Tuple from psygnal import Signal -from ..types import MenuItem, SubmenuItem - -if TYPE_CHECKING: - from typing import Dict, Iterator, List, Sequence, Set, Tuple, Union - - DisposeCallable = Callable[[], None] - MenuOrSubmenu = Union[MenuItem, SubmenuItem] +from ..types import MenuItem, MenuOrSubmenu +from ..types._misc import DisposeCallable class MenusRegistry: """Registry for menu and submenu items.""" menus_changed = Signal(set) - __instance: Optional[MenusRegistry] = None def __init__(self) -> None: self._menu_items: Dict[str, List[MenuOrSubmenu]] = {} - @classmethod - def instance(cls) -> MenusRegistry: - """Return global instance of the MenusRegistry.""" - if cls.__instance is None: - cls.__instance = cls() - return cls.__instance - def append_menu_items( self, items: Sequence[Tuple[str, MenuOrSubmenu]] ) -> DisposeCallable: diff --git a/src/app_model/registries/_register.py b/src/app_model/registries/_register.py index 638b712..85eeee1 100644 --- a/src/app_model/registries/_register.py +++ b/src/app_model/registries/_register.py @@ -1,46 +1,38 @@ from __future__ import annotations -from typing import TYPE_CHECKING, TypeVar, overload +from typing import TYPE_CHECKING, overload from ..types import Action, MenuItem -from ._commands import CommandsRegistry -from ._keybindings import KeybindingsRegistry -from ._menus import MenusRegistry if TYPE_CHECKING: from typing import Any, Callable, List, Literal, Optional, Union from .. import expressions - from ..types import ( - CommandIdStr, - Icon, - KeybindingRule, - KeybindingRuleDict, - MenuRule, - MenuRuleDict, - ) - - KeybindingRuleOrDict = Union[KeybindingRule, KeybindingRuleDict] - MenuRuleOrDict = Union[MenuRule, MenuRuleDict] - DisposeCallable = Callable[[], None] + from .._app import Application + from ..types import CommandIdStr, IconOrDict, KeybindingRuleOrDict, MenuRuleOrDict + from ..types._misc import DisposeCallable + from ._commands import CommandCallable + CommandDecorator = Callable[[Callable], Callable] - CommandCallable = TypeVar("CommandCallable", bound=Callable) @overload -def register_action(id_or_action: Action) -> DisposeCallable: +def register_action( + app: Union[Application, str], id_or_action: Action +) -> DisposeCallable: ... @overload def register_action( + app: Union[Application, str], id_or_action: CommandIdStr, title: str, *, run: Literal[None] = None, category: Optional[str] = None, tooltip: Optional[str] = None, - icon: Optional[Icon] = None, + icon: Optional[IconOrDict] = None, enablement: Optional[expressions.Expr] = None, menus: Optional[List[MenuRuleOrDict]] = None, keybindings: Optional[List[KeybindingRuleOrDict]] = None, @@ -51,13 +43,14 @@ def register_action( @overload def register_action( + app: Union[Application, str], id_or_action: CommandIdStr, title: str, *, run: CommandCallable, category: Optional[str] = None, tooltip: Optional[str] = None, - icon: Optional[Icon] = None, + icon: Optional[IconOrDict] = None, enablement: Optional[expressions.Expr] = None, menus: Optional[List[MenuRuleOrDict]] = None, keybindings: Optional[List[KeybindingRuleOrDict]] = None, @@ -67,13 +60,14 @@ def register_action( def register_action( + app: Union[Application, str], id_or_action: Union[CommandIdStr, Action], title: Optional[str] = None, *, run: Optional[CommandCallable] = None, category: Optional[str] = None, tooltip: Optional[str] = None, - icon: Optional[Icon] = None, + icon: Optional[IconOrDict] = None, enablement: Optional[expressions.Expr] = None, menus: Optional[List[MenuRuleOrDict]] = None, keybindings: Optional[List[KeybindingRuleOrDict]] = None, @@ -105,6 +99,9 @@ def register_action( Parameters ---------- + app: Union[Application, str] + The app in which to register the action. If a string, the app is retrieved + or created as necessary using `Application.get_or_create(app)`. id_or_action : Union[CommandId, Action] Either a complete Action object or a string id of the command being registered. If an `Action` object is provided, then all other arguments are ignored. @@ -149,11 +146,12 @@ def register_action( If `id_or_action` is not a string or an `Action` object. """ if isinstance(id_or_action, Action): - return _register_action_obj(id_or_action) + return _register_action_obj(app, id_or_action) if isinstance(id_or_action, str): if not title: raise ValueError("'title' is required when 'id' is a string") return _register_action_str( + app=app, id=id_or_action, title=title, category=category, @@ -169,6 +167,7 @@ def register_action( def _register_action_str( + app: Union[Application, str], **kwargs: Any, ) -> Union[CommandDecorator, DisposeCallable]: """Create and register an Action with a string id and title. @@ -181,10 +180,10 @@ def _register_action_str( to decorate the callable that executes the action. """ if callable(kwargs.get("run")): - return _register_action_obj(Action(**kwargs)) + return _register_action_obj(app, Action(**kwargs)) def decorator(command: CommandCallable, **k: Any) -> CommandCallable: - _register_action_obj(Action(**{**kwargs, **k, "run": command})) + _register_action_obj(app, Action(**{**kwargs, **k, "run": command})) return command decorator.__doc__ = f"Decorate function as callback for command {kwargs['id']!r}" @@ -192,23 +191,19 @@ def decorator(command: CommandCallable, **k: Any) -> CommandCallable: def _register_action_obj( + app: Union[Application, str], action: Action, - commands_registry: Optional[CommandsRegistry] = None, - menus_registry: Optional[MenusRegistry] = None, - keybindings_registry: Optional[KeybindingsRegistry] = None, ) -> DisposeCallable: """Register an Action object. Return a function that unregisters the action. Helper for `register_action()`. """ - commands_registry = commands_registry or CommandsRegistry.instance() - menus_registry = menus_registry or MenusRegistry.instance() - keybindings_registry = keybindings_registry or KeybindingsRegistry.instance() + from .._app import Application + + app = app if isinstance(app, Application) else Application.get_or_create(app) # command - disposers = [ - commands_registry.register_command(action.id, action.run, action.title) - ] + disposers = [app.commands.register_command(action.id, action.run, action.title)] # menu @@ -219,15 +214,16 @@ def _register_action_obj( ) items.append((rule.id, menu_item)) - disposers.append(menus_registry.append_menu_items(items)) + disposers.append(app.menus.append_menu_items(items)) # keybinding for keyb in action.keybindings or (): - if _d := keybindings_registry.register_keybinding_rule(action.id, keyb): + if _d := app.keybindings.register_keybinding_rule(action.id, keyb): disposers.append(_d) def _dispose() -> None: for d in disposers: d() + app._disposers.append((action.id, _dispose)) return _dispose diff --git a/src/app_model/types/__init__.py b/src/app_model/types/__init__.py index f719669..afacb10 100644 --- a/src/app_model/types/__init__.py +++ b/src/app_model/types/__init__.py @@ -1,15 +1,22 @@ """App-model types.""" -from typing import TYPE_CHECKING - from ._action import Action from ._command import CommandIdStr, CommandRule -from ._icon import Icon, IconCodeStr -from ._keybinding import KeybindingRule, KeyCodeStr -from ._menu import MenuIdStr, MenuItem, MenuRule, SubmenuItem - -if TYPE_CHECKING: - from ._keybinding import KeybindingRuleDict - from ._menu import MenuRuleDict +from ._icon import Icon, IconCodeStr, IconOrDict +from ._keybinding import ( + KeybindingRule, + KeybindingRuleDict, + KeybindingRuleOrDict, + KeyCodeStr, +) +from ._menu import ( + MenuIdStr, + MenuItem, + MenuOrSubmenu, + MenuRule, + MenuRuleDict, + MenuRuleOrDict, + SubmenuItem, +) __all__ = [ "Action", @@ -17,12 +24,16 @@ "CommandRule", "Icon", "IconCodeStr", + "IconOrDict", "KeybindingRule", "KeybindingRuleDict", + "KeybindingRuleOrDict", "KeyCodeStr", "MenuIdStr", "MenuItem", + "MenuOrSubmenu", "MenuRule", "MenuRuleDict", + "MenuRuleOrDict", "SubmenuItem", ] diff --git a/src/app_model/types/_icon.py b/src/app_model/types/_icon.py index f4ce369..accdf27 100644 --- a/src/app_model/types/_icon.py +++ b/src/app_model/types/_icon.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Generator, NewType, Optional +from typing import Any, Callable, Generator, NewType, Optional, TypedDict, Union from pydantic import Field @@ -35,3 +35,13 @@ def validate(cls, v: Any) -> "Icon": if isinstance(v, str): v = {"dark": v, "light": v} return cls(**v) + + +class IconDict(TypedDict): + """Icon dictionary.""" + + dark: Optional[IconCodeStr] + light: Optional[IconCodeStr] + + +IconOrDict = Union[Icon, IconDict] diff --git a/src/app_model/types/_keybinding.py b/src/app_model/types/_keybinding.py index f458ea4..9207cc6 100644 --- a/src/app_model/types/_keybinding.py +++ b/src/app_model/types/_keybinding.py @@ -1,6 +1,6 @@ import os import sys -from typing import TYPE_CHECKING, NewType, Optional +from typing import NewType, Optional, TypedDict, Union from pydantic import Field @@ -10,20 +10,8 @@ WINDOWS = os.name == "nt" MACOS = sys.platform == "darwin" LINUX = sys.platform.startswith("linux") -KeyCodeStr = NewType("KeyCodeStr", str) - -if TYPE_CHECKING: - from typing import TypedDict - - class KeybindingRuleDict(TypedDict, total=False): - """Typed dict for KeybindingRule kwargs.""" - primary: Optional[KeyCodeStr] - win: Optional[KeyCodeStr] - linux: Optional[KeyCodeStr] - mac: Optional[KeyCodeStr] - weight: int - when: Optional[expressions.Expr] +KeyCodeStr = NewType("KeyCodeStr", str) class KeybindingRule(_StrictModel): @@ -63,3 +51,17 @@ def _bind_to_current_platform(self) -> Optional[KeyCodeStr]: if LINUX and self.linux: return self.linux return self.primary + + +class KeybindingRuleDict(TypedDict, total=False): + """Typed dict for KeybindingRule kwargs.""" + + primary: Optional[KeyCodeStr] + win: Optional[KeyCodeStr] + linux: Optional[KeyCodeStr] + mac: Optional[KeyCodeStr] + weight: int + when: Optional[expressions.Expr] + + +KeybindingRuleOrDict = Union[KeybindingRule, KeybindingRuleDict] diff --git a/src/app_model/types/_menu.py b/src/app_model/types/_menu.py index 19b0855..ac39fd1 100644 --- a/src/app_model/types/_menu.py +++ b/src/app_model/types/_menu.py @@ -1,12 +1,13 @@ from typing import ( - TYPE_CHECKING, Any, Callable, Generator, NewType, Optional, Type, + TypedDict, TypeVar, + Union, ) from pydantic import Field @@ -18,20 +19,6 @@ MenuIdStr = NewType("MenuIdStr", str) T = TypeVar("T") -if TYPE_CHECKING: - from typing import TypedDict - - # Typed dicts mimic the API of their pydantic counterparts. - # Since pydantic allows you to pass in either an object or a dict, - # This lets us use either anywhere, without losing typing support. - # e.g. Union[MenuRuleDict, MenuRule] - class MenuRuleDict(TypedDict, total=False): - """Typed dict for MenuRule kwargs.""" - - when: Optional[expressions.Expr] - group: str - order: Optional[float] - id: MenuIdStr class _MenuItemBase(_StrictModel): @@ -111,3 +98,19 @@ class SubmenuItem(_MenuItemBase): description="(Optional) Icon used to represent this submenu. " "These may be superqt fonticon keys, such as `fa5s.arrow_down`", ) + + +class MenuRuleDict(TypedDict, total=False): + """Typed dict for MenuRule kwargs. + + This mimics the pydantic `MenuRule` interface, but allows you to pass in a dict + """ + + when: Optional[expressions.Expr] + group: str + order: Optional[float] + id: MenuIdStr + + +MenuRuleOrDict = Union[MenuRule, MenuRuleDict] +MenuOrSubmenu = Union[MenuItem, SubmenuItem] diff --git a/src/app_model/types/_misc.py b/src/app_model/types/_misc.py new file mode 100644 index 0000000..218d320 --- /dev/null +++ b/src/app_model/types/_misc.py @@ -0,0 +1,3 @@ +from typing import Callable + +DisposeCallable = Callable[[], None] diff --git a/tests/test_actions.py b/tests/test_actions.py index 798252f..0667d97 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -1,14 +1,8 @@ -from typing import Callable, Optional -from unittest.mock import Mock, patch +from unittest.mock import Mock import pytest -from app_model.registries import ( - CommandsRegistry, - KeybindingsRegistry, - MenusRegistry, - register_action, -) +from app_model import Application from app_model.types import Action, CommandIdStr PRIMARY_KEY = "ctrl+a" @@ -30,57 +24,34 @@ @pytest.fixture -def cmd_reg(): - reg = CommandsRegistry() - reg.registered_emit = Mock() - reg.registered.connect(reg.registered_emit) - with patch.object(CommandsRegistry, "instance", return_value=reg): - yield reg - reg._commands.clear() - - -@pytest.fixture -def key_reg(): - reg = KeybindingsRegistry() - reg.registered_emit = Mock() - reg.registered.connect(reg.registered_emit) - with patch.object(KeybindingsRegistry, "instance", return_value=reg): - yield reg - reg._coreKeybindings.clear() - - -@pytest.fixture -def menu_reg(): - reg = MenusRegistry() - reg.menus_changed_emit = Mock() - reg.menus_changed.connect(reg.menus_changed_emit) - with patch.object(MenusRegistry, "instance", return_value=reg): - yield reg - reg._menu_items.clear() +def app(): + app = Application("test") + app.commands_changed = Mock() + app.commands.registered.connect(app.commands_changed) + app.keybindings_changed = Mock() + app.keybindings.registered.connect(app.keybindings_changed) + app.menus_changed = Mock() + app.menus.menus_changed.connect(app.menus_changed) + yield app + Application.destroy("test") + assert "test" not in Application._instances @pytest.mark.parametrize("kwargs", KWARGS) @pytest.mark.parametrize("mode", ["str", "decorator", "action"]) -def test_register_action_decorator( - kwargs, - cmd_reg: CommandsRegistry, - key_reg: KeybindingsRegistry, - menu_reg: MenusRegistry, - mode, -): +def test_register_action_decorator(kwargs, app: Application, mode): # make sure mocks are working - assert not list(cmd_reg) - assert not list(key_reg) - assert not list(menu_reg) + assert not list(app.commands) + assert not list(app.keybindings) + assert not list(app.menus) - dispose: Optional[Callable] = None cmd_id = CommandIdStr("cmd.id") kwargs["title"] = "Test title" # register the action if mode == "decorator": - @register_action(cmd_id, **kwargs) + @app.register_action(cmd_id, **kwargs) def f1(): return "hi" @@ -92,59 +63,47 @@ def f2(): return "hi" if mode == "str": - dispose = register_action(cmd_id, run=f2, **kwargs) + app.register_action(cmd_id, run=f2, **kwargs) elif mode == "action": action = Action(id=cmd_id, run=f2, **kwargs) - dispose = register_action(action) + app.register_action(action) # make sure the command is registered - assert cmd_id in cmd_reg - assert list(cmd_reg) + assert cmd_id in app.commands + assert list(app.commands) # make sure an event was emitted signaling the command was registered - cmd_reg.registered_emit.assert_called_once_with(cmd_id) # type: ignore + app.commands_changed.assert_called_once_with(cmd_id) # type: ignore # make sure we can call the command, and that we can inject dependencies. - assert cmd_reg.execute_command(cmd_id).result() == "hi" + assert app.commands.execute_command(cmd_id).result() == "hi" # make sure menus are registered if specified if menus := kwargs.get("menus"): for entry in menus: - assert entry["id"] in menu_reg - menu_reg.menus_changed_emit.assert_called_with({entry["id"]}) + assert entry["id"] in app.menus + app.menus_changed.assert_called_with({entry["id"]}) else: - assert not list(menu_reg) + assert not list(app.menus) # make sure keybindings are registered if specified if keybindings := kwargs.get("keybindings"): for entry in keybindings: key = PRIMARY_KEY if len(entry) == 1 else OS_KEY # see KWARGS[5] - assert any(i.keybinding == key for i in key_reg) - key_reg.registered_emit.assert_called() # type: ignore + assert any(i.keybinding == key for i in app.keybindings) + app.keybindings_changed.assert_called() # type: ignore else: - assert not list(key_reg) + assert not list(app.keybindings) - # if we're not using the decorator, check that calling the dispose - # function removes everything. (the decorator returns the function, so can't - # return the dispose function) - if dispose: - dispose() - assert not list(cmd_reg) - assert not list(key_reg) - assert not list(menu_reg) + # check that calling the dispose function removes everything. + app.dispose() + assert not list(app.commands) + assert not list(app.keybindings) + assert not list(app.menus) -def test_errors(): +def test_errors(app: Application): with pytest.raises(ValueError, match="'title' is required"): - register_action("cmd_id") # type: ignore + app.register_action("cmd_id") # type: ignore with pytest.raises(TypeError, match="must be a string or an Action"): - register_action(None) # type: ignore - - -def test_instances(): - assert isinstance(MenusRegistry().instance(), MenusRegistry) - assert isinstance( - KeybindingsRegistry().instance(), - KeybindingsRegistry, - ) - assert isinstance(CommandsRegistry().instance(), CommandsRegistry) + app.register_action(None) # type: ignore diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..4981bd5 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,16 @@ +import pytest + +from app_model import Application + + +def test_app_create(): + app = Application("my_app") + + # NOTE: for some strange reason, this test fails if I move this line + # below the error assertion below... I don't know why. + assert Application.get_or_create("my_app") is app + + with pytest.raises(ValueError, match="Application 'my_app' already exists"): + Application("my_app") + + assert repr(app) == "Application('my_app')"