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

feat: combine app model #9

Merged
merged 4 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions src/app_model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
148 changes: 148 additions & 0 deletions src/app_model/_app.py
Original file line number Diff line number Diff line change
@@ -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,
)
10 changes: 1 addition & 9 deletions src/app_model/registries/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
18 changes: 5 additions & 13 deletions src/app_model/registries/_keybindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)>"
19 changes: 3 additions & 16 deletions src/app_model/registries/_menus.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Loading