Bundled and bridged AutoHotkey for full native code execution from Python.
Project description
ahkUnwrapped
I wanted to automate Windows with the coverage and simplicity of the complete AutoHotkey API, yet code in Python, so I created ahkUnwrapped
.
AutoHotkey already abstracts the Windows API, so another layer to introduce complexity and slowdowns is undesirable.
Instead, we bundle and bridge AutoHotkey.exe, sending your initial script via stdin with minimal boilerplate to listen for window messages from Python and respond via stdout.
Features
- All of AutoHotkey!
- Execute arbitrary AHK code or load scripts.
- Hypothesis powered testing of convoluted unicode, et al.
- Warnings for loss of precision (maximum 6 decimal places).
- Errors for unsupported values (
NaN
Inf
\0
). - Unhandled AHK exceptions carry over to Python.
- Won't explode when used from multiple threads.
- Separate auto-execute sections to ease scripting.
- Supports PyInstaller for onefile/onedir installations.
- Special care for:
- Descriptive errors with accurate line numbers.
- Persistent Windows notification area settings.
- Unexpected exit handling.
- Minimal latency.
Get started
> pip install ahkunwrapped
call(proc, ...)
f(func, ...)
get(var)
set(var, val)
from ahkunwrapped import Script
ahk = Script()
# built-in functions are directly callable
isNotepadActive = ahk.f('WinActive', 'ahk_class Notepad')
# built-in variables (and user globals) can be set directly
ahk.set('Clipboard', "Copied text!")
print(isNotepadActive)
from ahkunwrapped import Script
ahk = Script('''
LuckyMinimize(winTitle) {
global myVar
myVar := 7
WinMinimize, % winTitle
Clipboard := "You minimized: " winTitle
}
''')
ahk.call('LuckyMinimize', 'ahk_class Notepad')
print("Lucky number", ahk.get('myVar'))
from pathlib import Path
from ahkunwrapped import Script
ahk = Script.from_file(Path('my_msg.ahk'))
ahk.call('MyMsg', "Wooo!")
my_msg.ahk:
; auto-execute section when ran standalone
#SingleInstance force
#Warn
AutoExec() ; we can call this if we want
MyMsg("test our function")
return
; auto-execute section when ran from Python
AutoExec() {
SetBatchLines, 100ms ; slow our code to reduce CPU
}
MyMsg(text) {
MsgBox, % text
}
Settings from AutoExec() will still apply even though we execute directly from the message listening thread for speed.
(AutoHotkey's #Warn is special and will apply to both standalone and from-Python execution, unless you add/remove it dynamically.)
Usage
call(proc, ...)
is for performance, to avoid receiving a large unneeded result.
get(var)
set(var, val)
are shorthand for accessing global variables and built-ins like A_TimeIdle
.
f(func, ...)
get(var)
will infer float
and int
(base-16 beginning with 0x
) like AutoHotkey.
f_raw(func, ...)
get_raw(var)
will return the raw string as-stored.
call_main(proc, ...)
f_main(func, ...)
f_raw_main(func, ...)
will execute on AutoHotkey's main thread instead of the OnMessage() listener.
This avoids AhkCantCallOutInInputSyncCallError
, e.g. from some uses of ComObjCreate().
This is slower (except with very large data), but still fast and unlikely to bottleneck.
Event loop with hotkeys
import sys
import time
from datetime import datetime
from enum import Enum
from pathlib import Path
from ahkunwrapped import Script, AhkExitException
choice = None
HOTKEY_SEND_CHOICE = 'F2'
class Event(Enum):
QUIT, SEND_CHOICE, CLEAR_CHOICE, CHOOSE_MONTH, CHOOSE_DAY = range(5)
# format_dict= so we can use {{VARIABLE}} within example.ahk
ahk = Script.from_file(Path('example.ahk'), format_dict=globals())
def main() -> None:
print("Scroll your mousewheel in Notepad.")
ts = 0
while True:
try:
# ahk.poll() # detect exit, but all ahk functions include this
s_elapsed = time.time() - ts
if s_elapsed >= 60:
ts = time.time()
print_minute()
event = ahk.get('event') # contains ahk.poll()
if event:
ahk.set('event', '')
on_event(event)
except AhkExitException as ex:
sys.exit(ex.args[0])
time.sleep(0.01)
def print_minute() -> None:
print(f"It is now {datetime.now().time()}")
def on_event(event: str) -> None:
global choice
def get_choice() -> str:
return choice or datetime.now().strftime('%#I:%M %p')
if event == str(Event.QUIT):
ahk.exit()
if event == str(Event.CLEAR_CHOICE):
choice = None
if event == str(Event.SEND_CHOICE):
ahk.call('Send', f'{get_choice()} ')
if event == str(Event.CHOOSE_MONTH):
choice = datetime.now().strftime('%b')
ahk.call('ToolTip', f"Month is {get_choice()}, {HOTKEY_SEND_CHOICE} to insert.")
if event == str(Event.CHOOSE_DAY):
choice = datetime.now().strftime('%#d')
ahk.call('ToolTip', f"Day is {get_choice()}, {HOTKEY_SEND_CHOICE} to insert.")
if __name__ == '__main__':
main()
example.ahk:
#SingleInstance, force
#Warn
ToolTip("Standalone script test!")
return
AutoExec() {
global event
event := ""
SendMode, input
}
Send(text) {
Send, % text
}
ToolTip(text, s := 2) {
ToolTip, % text
; negative for non-repeating
SetTimer, RemoveToolTip, % s * -1000
}
RemoveToolTip:
ToolTip,
event = {{Event.CLEAR_CHOICE}}
return
MouseIsOver(winTitle) {
MouseGetPos,,, winId
result := WinExist(winTitle " ahk_id " winId)
return result
}
#If WinActive("ahk_class Notepad")
{{HOTKEY_SEND_CHOICE}}::event = {{Event.SEND_CHOICE}}
^Q::event = {{Event.QUIT}}
#If MouseIsOver("ahk_class Notepad")
WheelUp::event = {{Event.CHOOSE_MONTH}}
WheelDown::event = {{Event.CHOOSE_DAY}}
PyInstaller (single .exe or folder)
example.spec:
# -*- mode: python -*-
from pathlib import Path
import ahkunwrapped
a = Analysis(
['example.py'],
datas=[
(Path(ahkunwrapped.__file__).parent / 'lib', 'lib'), # required
('example.ahk', '.'),
]
)
pyz = PYZ(a.pure)
# for onefile
exe = EXE(pyz, a.scripts, a.binaries, a.datas, name='my-example', upx=True, console=False)
# for onedir
# exe = EXE(pyz, a.scripts, exclude_binaries=True, name='my-example', upx=True, console=False)
# dir = COLLECT(exe, a.binaries, a.datas, name='my-example-folder')
Folder considerations
example.py:
from pathlib import Path
from ahkunwrapped import Script
# tray icon visibility settings rely on consistent exe paths
LOCALAPP_DIR = Path(os.getenv('LOCALAPPDATA') / 'pyinstaller-example')
# working directory is different between onefile and onedir modes
# https://pyinstaller.readthedocs.io/en/stable/runtime-information.html
CUR_DIR = Path(getattr(sys, '_MEIPASS', Path(__file__).parent))
ahk = Script.from_file(CUR_DIR / 'example.ahk', format_dict=globals(), execute_from=LOCALAPP_DIR)
# ...
example.ahk:
AutoExec() {
Menu, Tray, Icon, {{CUR_DIR}}\black.ico
Menu, Tray, Icon ; unhide
}
; ...
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file ahkunwrapped-2.2.1.tar.gz
.
File metadata
- Download URL: ahkunwrapped-2.2.1.tar.gz
- Upload date:
- Size: 663.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.3 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.12 tqdm/4.64.1 importlib-metadata/4.8.3 keyring/23.4.1 rfc3986/1.5.0 colorama/0.4.5 CPython/3.6.8
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8e5e63543f62f459864c01d752b135c2ec94a25dead1a5901f84e3cb278b094b |
|
MD5 | 70809e967dd5675498bb57b4476e1c83 |
|
BLAKE2b-256 | 145153daee32bd33c103ef77b8ace4022fdcdc644758722eafa92cc3a51c6e65 |
File details
Details for the file ahkunwrapped-2.2.1-py3-none-any.whl
.
File metadata
- Download URL: ahkunwrapped-2.2.1-py3-none-any.whl
- Upload date:
- Size: 661.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.3 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.12 tqdm/4.64.1 importlib-metadata/4.8.3 keyring/23.4.1 rfc3986/1.5.0 colorama/0.4.5 CPython/3.6.8
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9d4eef97092b133fca6e1ec6d72475aacfeb538707b8512ef25d1627fb74a2b4 |
|
MD5 | 1bcf4cdfd1f3e51083813f9a3197ea98 |
|
BLAKE2b-256 | ad34540a5041b27d545bf3edf88783ab1a11d04bb22b42d40cfda762410caf05 |