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))) diff --git a/docs/extending.rst b/docs/extending.rst index 43423ff..468a84e 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 `_ 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. + Notes ^^^^^