From 400c11b0c18365cfa7e9c81f439382eb7f7193e6 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Tue, 9 Jul 2024 15:35:27 -0700 Subject: [PATCH 1/3] support for extending window classes --- ahk/_async/engine.py | 17 ++++++++++++++++- ahk/_async/window.py | 9 +++++++++ ahk/_sync/engine.py | 16 +++++++++++++++- ahk/_sync/window.py | 9 +++++++++ ahk/extensions.py | 40 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 87 insertions(+), 4 deletions(-) diff --git a/ahk/_async/engine.py b/ahk/_async/engine.py index 66e8850..64b9b33 100644 --- a/ahk/_async/engine.py +++ b/ahk/_async/engine.py @@ -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: @@ -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: diff --git a/ahk/_async/window.py b/ahk/_async/window.py index 455d67f..57217ce 100644 --- a/ahk/_async/window.py +++ b/ahk/_async/window.py @@ -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 @@ -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') diff --git a/ahk/_sync/engine.py b/ahk/_sync/engine.py index 95bfc3c..e6d2da8 100644 --- a/ahk/_sync/engine.py +++ b/ahk/_sync/engine.py @@ -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: @@ -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: diff --git a/ahk/_sync/window.py b/ahk/_sync/window.py index a7f4790..a08060e 100644 --- a/ahk/_sync/window.py +++ b/ahk/_sync/window.py @@ -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 @@ -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') diff --git a/ahk/extensions.py b/ahk/extensions.py index 3677e30..9b92042 100644 --- a/ahk/extensions.py +++ b/ahk/extensions.py @@ -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): @@ -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] = {} @@ -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 @@ -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))) From f862ea7859a88aa96d4166c2c3ce007b04a780a7 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Tue, 9 Jul 2024 16:18:45 -0700 Subject: [PATCH 2/3] document window extensions feature --- docs/extending.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/extending.rst b/docs/extending.rst index 43423ff..276cf53 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -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) @@ -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 @@ -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 ^^^^^ From 46e51a4ef293393f88e6f811420cf7fcc91aaf87 Mon Sep 17 00:00:00 2001 From: Spencer Phillip Young Date: Tue, 9 Jul 2024 16:21:51 -0700 Subject: [PATCH 3/3] fix links --- docs/extending.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extending.rst b/docs/extending.rst index 276cf53..468a84e 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -330,8 +330,8 @@ 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. +- `ahk-wmutil `_ an extension providing utility support for working with multiple monitors. Includes examples of window extensions. +- `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.