anki/pylib/tools/hookslib.py

170 lines
4.8 KiB
Python
Raw Normal View History

2020-01-13 05:38:05 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Code for generating hooks.
2020-01-13 05:38:05 +01:00
"""
import os
import subprocess
import sys
2020-01-13 05:38:05 +01:00
from dataclasses import dataclass
from operator import attrgetter
from typing import List, Optional
import stringcase
2020-01-13 05:38:05 +01:00
@dataclass
class Hook:
# the name of the hook. _filter or _hook is appending automatically.
name: str
# string of the typed arguments passed to the callback, eg
# ["kind: str", "val: int"]
args: List[str] = None
2020-01-13 05:38:05 +01:00
# string of the return type. if set, hook is a filter.
return_type: Optional[str] = None
# if add-ons may be relying on the legacy hook name, add it here
legacy_hook: Optional[str] = None
# if legacy hook takes no arguments but the new hook does, set this
legacy_no_args: bool = False
2020-01-15 03:14:32 +01:00
# docstring to add to hook class
doc: Optional[str] = None
2020-01-13 05:38:05 +01:00
def callable(self) -> str:
"Convert args into a Callable."
types = []
for arg in self.args or []:
2020-01-13 05:38:05 +01:00
(name, type) = arg.split(":")
2021-10-02 15:51:42 +02:00
type = f'"{type.strip()}"'
types.append(type)
2020-01-13 05:38:05 +01:00
types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
def arg_names(self) -> List[str]:
names = []
for arg in self.args or []:
2020-01-13 05:38:05 +01:00
if not arg:
continue
(name, type) = arg.split(":")
names.append(name.strip())
return names
def full_name(self) -> str:
return f"{self.name}_{self.kind()}"
def kind(self) -> str:
if self.return_type is not None:
return "filter"
else:
return "hook"
def classname(self) -> str:
2021-10-02 15:51:42 +02:00
return f"_{stringcase.pascalcase(self.full_name())}"
2020-01-13 05:38:05 +01:00
def list_code(self) -> str:
return f"""\
2020-01-15 03:14:32 +01:00
_hooks: List[{self.callable()}] = []
2020-01-13 05:38:05 +01:00
"""
def code(self) -> str:
2020-01-15 03:14:32 +01:00
appenddoc = f"({', '.join(self.args or [])})"
if self.doc:
classdoc = f" '''{self.doc}'''\n"
else:
classdoc = ""
code = f"""\
class {self.classname()}:
2020-01-15 03:14:32 +01:00
{classdoc}{self.list_code()}
2021-06-27 05:49:58 +02:00
def append(self, callback: {self.callable()}) -> None:
2020-01-15 03:14:32 +01:00
'''{appenddoc}'''
2021-06-27 05:49:58 +02:00
self._hooks.append(callback)
2021-06-27 05:49:58 +02:00
def remove(self, callback: {self.callable()}) -> None:
if callback in self._hooks:
self._hooks.remove(callback)
def count(self) -> int:
return len(self._hooks)
{self.fire_code()}
2020-01-15 07:53:24 +01:00
{self.name} = {self.classname()}()
"""
return code
2020-01-13 05:38:05 +01:00
def fire_code(self) -> str:
if self.return_type is not None:
# filter
return self.filter_fire_code()
else:
# hook
return self.hook_fire_code()
def legacy_args(self) -> str:
if self.legacy_no_args:
# hook name only
return f'"{self.legacy_hook}"'
else:
return ", ".join([f'"{self.legacy_hook}"'] + self.arg_names())
2020-01-13 05:38:05 +01:00
def hook_fire_code(self) -> str:
arg_names = self.arg_names()
args_including_self = ["self"] + (self.args or [])
2020-01-13 05:38:05 +01:00
out = f"""\
def __call__({", ".join(args_including_self)}) -> None:
for hook in self._hooks:
try:
hook({", ".join(arg_names)})
except:
# if the hook fails, remove it
self._hooks.remove(hook)
raise
2020-01-13 05:38:05 +01:00
"""
if self.legacy_hook:
out += f"""\
# legacy support
anki.hooks.runHook({self.legacy_args()})
2020-01-13 05:38:05 +01:00
"""
2021-10-02 15:51:42 +02:00
return f"{out}\n\n"
2020-01-13 05:38:05 +01:00
def filter_fire_code(self) -> str:
arg_names = self.arg_names()
args_including_self = ["self"] + (self.args or [])
2020-01-13 05:38:05 +01:00
out = f"""\
def __call__({", ".join(args_including_self)}) -> {self.return_type}:
for filter in self._hooks:
try:
{arg_names[0]} = filter({", ".join(arg_names)})
except:
# if the hook fails, remove it
self._hooks.remove(filter)
raise
2020-01-13 05:38:05 +01:00
"""
if self.legacy_hook:
out += f"""\
# legacy support
{arg_names[0]} = anki.hooks.runFilter({self.legacy_args()})
2020-01-13 05:38:05 +01:00
"""
out += f"""\
return {arg_names[0]}
2020-01-13 05:38:05 +01:00
"""
2021-10-02 15:51:42 +02:00
return f"{out}\n\n"
2020-01-13 05:38:05 +01:00
def write_file(path: str, hooks: List[Hook], prefix: str, suffix: str):
2020-01-13 05:38:05 +01:00
hooks.sort(key=attrgetter("name"))
2021-10-02 15:51:42 +02:00
code = f"{prefix}\n"
2020-01-13 05:38:05 +01:00
for hook in hooks:
code += hook.code()
2020-01-13 05:38:05 +01:00
2021-10-02 15:51:42 +02:00
code += f"\n{suffix}"
2020-01-13 05:38:05 +01:00
# work around issue with latest black
2021-05-18 22:19:09 +02:00
if sys.platform == "win32" and "HOME" in os.environ:
os.environ["USERPROFILE"] = os.environ["HOME"]
with open(path, "wb") as file:
file.write(code.encode("utf8"))
subprocess.run([sys.executable, "-m", "black", "-q", path], check=True)