Skip to content

Commit

Permalink
Merge pull request #330 from spyoungtech/window-extensions
Browse files Browse the repository at this point in the history
support for extending window classes
  • Loading branch information
spyoungtech authored Jul 9, 2024
2 parents 4b00c59 + 46e51a4 commit ce87e75
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 5 deletions.
17 changes: 16 additions & 1 deletion ahk/_async/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ def __init__(
raise ValueError(
f'Incompatible extension detected. Extension requires AutoHotkey {ext._requires} but current version is {version}'
)
self._method_registry = _ExtensionMethodRegistry(sync_methods={}, async_methods={})
self._method_registry = _ExtensionMethodRegistry(
sync_methods={}, async_methods={}, async_window_methods={}, sync_window_methods={}
)
for ext in self._extensions:
self._method_registry.merge(ext._extension_method_registry)
if TransportClass is None:
Expand Down Expand Up @@ -176,6 +178,19 @@ def __getattr__(self, name: str) -> Callable[..., Any]:

raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {name!r}')

def _get_window_extension_method(self, name: str) -> Callable[..., Any] | None:
is_async = False
is_async = True # unasync: remove
if is_async:
if name in self._method_registry.async_window_methods:
method = self._method_registry.async_window_methods[name]
return method
else:
if name in self._method_registry.sync_window_methods:
method = self._method_registry.sync_window_methods[name]
return method
return None

def add_hotkey(
self, keyname: str, callback: Callable[[], Any], ex_handler: Optional[Callable[[str, Exception], Any]] = None
) -> None:
Expand Down
9 changes: 9 additions & 0 deletions ahk/_async/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import sys
import warnings
from functools import partial
from typing import Any
from typing import Callable
from typing import Coroutine
from typing import Literal
from typing import Optional
Expand Down Expand Up @@ -70,6 +72,13 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash(self._ahk_id)

def __getattr__(self, name: str) -> Callable[..., Any]:
method = self._engine._get_window_extension_method(name)
if method is None:
raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {name!r}')
else:
return partial(method, self)

async def close(self) -> None:
await self._engine.win_close(
title=f'ahk_id {self._ahk_id}', detect_hidden_windows=True, title_match_mode=(1, 'Fast')
Expand Down
16 changes: 15 additions & 1 deletion ahk/_sync/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ def __init__(
raise ValueError(
f'Incompatible extension detected. Extension requires AutoHotkey {ext._requires} but current version is {version}'
)
self._method_registry = _ExtensionMethodRegistry(sync_methods={}, async_methods={})
self._method_registry = _ExtensionMethodRegistry(
sync_methods={}, async_methods={}, async_window_methods={}, sync_window_methods={}
)
for ext in self._extensions:
self._method_registry.merge(ext._extension_method_registry)
if TransportClass is None:
Expand All @@ -171,6 +173,18 @@ def __getattr__(self, name: str) -> Callable[..., Any]:

raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {name!r}')

def _get_window_extension_method(self, name: str) -> Callable[..., Any] | None:
is_async = False
if is_async:
if name in self._method_registry.async_window_methods:
method = self._method_registry.async_window_methods[name]
return method
else:
if name in self._method_registry.sync_window_methods:
method = self._method_registry.sync_window_methods[name]
return method
return None

def add_hotkey(
self, keyname: str, callback: Callable[[], Any], ex_handler: Optional[Callable[[str, Exception], Any]] = None
) -> None:
Expand Down
9 changes: 9 additions & 0 deletions ahk/_sync/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import sys
import warnings
from functools import partial
from typing import Any
from typing import Callable
from typing import Coroutine
from typing import Literal
from typing import Optional
Expand Down Expand Up @@ -66,6 +68,13 @@ def __eq__(self, other: object) -> bool:
def __hash__(self) -> int:
return hash(self._ahk_id)

def __getattr__(self, name: str) -> Callable[..., Any]:
method = self._engine._get_window_extension_method(name)
if method is None:
raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {name!r}')
else:
return partial(method, self)

def close(self) -> None:
self._engine.win_close(
title=f'ahk_id {self._ahk_id}', detect_hidden_windows=True, title_match_mode=(1, 'Fast')
Expand Down
40 changes: 38 additions & 2 deletions ahk/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ class _ExtensionEntry:


if typing.TYPE_CHECKING:
from ahk import AHK, AsyncAHK
from ahk import AHK, AsyncAHK, Window, AsyncWindow

TAHK = TypeVar('TAHK', bound=typing.Union[AHK[Any], AsyncAHK[Any]])
TWindow = TypeVar('TWindow', bound=typing.Union[Window, AsyncWindow])


@dataclass
class _ExtensionMethodRegistry:
sync_methods: dict[str, Callable[..., Any]]
async_methods: dict[str, Callable[..., Any]]
sync_window_methods: dict[str, Callable[..., Any]]
async_window_methods: dict[str, Callable[..., Any]]

def register(self, f: Callable[Concatenate[TAHK, P], T]) -> Callable[Concatenate[TAHK, P], T]:
if asyncio.iscoroutinefunction(f):
Expand All @@ -63,14 +66,41 @@ def register(self, f: Callable[Concatenate[TAHK, P], T]) -> Callable[Concatenate
self.sync_methods[f.__name__] = f
return f

def register_window_method(self, f: Callable[Concatenate[TWindow, P], T]) -> Callable[Concatenate[TWindow, P], T]:
if asyncio.iscoroutinefunction(f):
if f.__name__ in self.async_window_methods:
warnings.warn(
f'Method of name {f.__name__!r} has already been registered. '
f'Previously registered method {self.async_window_methods[f.__name__]!r} '
f'will be overridden by {f!r}',
stacklevel=2,
)
self.async_window_methods[f.__name__] = f
else:
if f.__name__ in self.sync_window_methods:
warnings.warn(
f'Method of name {f.__name__!r} has already been registered. '
f'Previously registered method {self.sync_window_methods[f.__name__]!r} '
f'will be overridden by {f!r}',
stacklevel=2,
)
self.sync_window_methods[f.__name__] = f
return f

def merge(self, other: _ExtensionMethodRegistry) -> None:
for name, method in other.methods:
self.register(method)
for name, method in other.window_methods:
self.register_window_method(method)

@property
def methods(self) -> list[tuple[str, Callable[..., Any]]]:
return list(itertools.chain(self.async_methods.items(), self.sync_methods.items()))

@property
def window_methods(self) -> list[tuple[str, Callable[..., Any]]]:
return list(itertools.chain(self.async_window_methods.items(), self.sync_window_methods.items()))


_extension_registry: dict[Extension, _ExtensionMethodRegistry] = {}

Expand All @@ -88,7 +118,7 @@ def __init__(
self._includes: list[str] = includes or []
self.dependencies: list[Extension] = dependencies or []
self._extension_method_registry: _ExtensionMethodRegistry = _ExtensionMethodRegistry(
sync_methods={}, async_methods={}
sync_methods={}, async_methods={}, sync_window_methods={}, async_window_methods={}
)
_extension_registry[self] = self._extension_method_registry

Expand All @@ -108,6 +138,12 @@ def register(self, f: Callable[Concatenate[TAHK, P], T]) -> Callable[Concatenate
self._extension_method_registry.register(f)
return f

register_method = register

def register_window_method(self, f: Callable[Concatenate[TWindow, P], T]) -> Callable[Concatenate[TWindow, P], T]:
self._extension_method_registry.register_window_method(f)
return f

def __hash__(self) -> int:
return hash((self._text, tuple(self.includes), tuple(self.dependencies)))

Expand Down
21 changes: 20 additions & 1 deletion docs/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ containing the AutoHotkey code we just wrote above.
'''
simple_math_extension = Extension(script_text=script_text)
@simple_meth_extension.register # register the method for the extension
@simple_math_extension.register # register the method for the extension
def simple_math(ahk: AHK, lhs: int, rhs: int, operator: Literal['+', '*']) -> int:
assert isinstance(lhs, int)
assert isinstance(rhs, int)
Expand Down Expand Up @@ -141,6 +141,13 @@ If you use this example code, it should output something like this: ::
An exception was raised. Exception message was: Invalid operator: %


Extending ``Window`` methods
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Just as you can add methods that are accessible from ``AHK`` (and ``AsyncAHK``) instances, you can also add methods
that are accessible from the ``Window`` and ``AsyncWindow`` classes as well. This is identical to the process
described above, except you use the ``register_window_method`` decorator instead of the ``register`` decorator. The
first argument of such decorated functions should accept a ``Window`` object (or ``AsyncWindow`` object for async functions).


Includes
Expand Down Expand Up @@ -316,6 +323,18 @@ For example, suppose you want your method to return a datetime object, you might
In AHK code, you can reference custom response messages by the their fully qualified name, including the namespace.
(if you're not sure what this means, you can see this value by calling the ``fqn()`` method, e.g. ``DateTimeResponseMessage.fqn()``)


Featured extension packages
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Since this feature is in early development, not many extensions exist yet. However, I've authored two small extensions
which can be used as references or examples of how to create and distribute an extension:

- `ahk-wmutil <https://github.com/spyoungtech/ahk-wmutil>`_ an extension providing utility support for working with multiple monitors. Includes examples of window extensions.
- `ahk-json <https://github.com/spyoungtech/ahk-json>`_ an extension providing custom a JSON message type that can be used by other extensions.

If you have created an extension you'd like to share, consider opening an issue, PR, or discussion and it may be added to this list.

Notes
^^^^^

Expand Down

0 comments on commit ce87e75

Please sign in to comment.