From 706f9bca82d463f6f1bd17d5dc609807e4a1e8a9 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Sat, 2 Sep 2017 17:52:25 -0400 Subject: python: Rewrite with CFFI --- plugins/python/python.py | 497 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 plugins/python/python.py (limited to 'plugins/python/python.py') diff --git a/plugins/python/python.py b/plugins/python/python.py new file mode 100644 index 00000000..3845a79f --- /dev/null +++ b/plugins/python/python.py @@ -0,0 +1,497 @@ +from __future__ import print_function + +import os +import sys +from contextlib import contextmanager +import importlib +import signal +import site +import traceback +import weakref +from _hexchat_embedded import ffi, lib + +VERSION = b'2.0' # Sync with hexchat.__version__ +PLUGIN_NAME = ffi.new('char[]', b'Python') +PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' + % (sys.version_info[0], sys.version_info[1])) +PLUGIN_VERSION = ffi.new('char[]', VERSION) +hexchat = None +local_interp = None +hexchat_stdout = None +plugins = set() + + +@contextmanager +def redirected_stdout(): + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + yield + sys.stdout = hexchat_stdout + sys.stderr = hexchat_stdout + + +if os.environ.get('HEXCHAT_LOG_PYTHON'): + def log(*args): + with redirected_stdout(): + print(*args) +else: + def log(*args): + pass + + +class Stdout: + def __init__(self): + self.buffer = bytearray() + + def write(self, string): + string = string.encode() + idx = string.rfind(b'\n') + if idx is not -1: + self.buffer += string[:idx] + lib.hexchat_print(lib.ph, bytes(self.buffer)) + self.buffer = bytearray(string[idx + 1:]) + else: + self.buffer += string + + def isatty(self): + # FIXME: help() locks app despite this? + return False + + +class Attribute: + def __init__(self): + self.time = 0 + + def __repr__(self): + return ''.format(id(self)) + + +class Hook: + def __init__(self, plugin, callback, userdata, is_unload): + self.is_unload = is_unload + self.plugin = weakref.proxy(plugin) + self.callback = callback + self.userdata = userdata + self.hexchat_hook = None + self.handle = ffi.new_handle(weakref.proxy(self)) + + def __del__(self): + log('Removing hook', id(self)) + if self.is_unload is False: + assert self.hexchat_hook is not None + lib.hexchat_unhook(lib.ph, self.hexchat_hook) + + +if sys.version_info[0] is 2: + def compile_file(data, filename): + return compile(data, filename, 'exec', dont_inherit=True) + + def compile_line(string): + try: + return compile(string, '', 'eval', dont_inherit=True) + except SyntaxError: + # For some reason `print` is invalid for eval + # This will hide any return value though + return compile(string, '', 'exec', dont_inherit=True) +else: + def compile_file(data, filename): + return compile(data, filename, 'exec', optimize=2, dont_inherit=True) + + def compile_line(string): + return compile(string, '', 'eval', optimize=2, dont_inherit=True) + + +class Plugin: + def __init__(self): + self.ph = None + self.name = '' + self.filename = '' + self.version = '' + self.description = '' + self.hooks = set() + self.globals = { + '__plugin': weakref.proxy(self), + '__name__': '__main__', + } + + def add_hook(self, callback, userdata, is_unload=False): + hook = Hook(self, callback, userdata, is_unload=is_unload) + self.hooks.add(hook) + return hook + + def remove_hook(self, hook): + for h in self.hooks: + if id(h) == hook: + ud = hook.userdata + self.hooks.remove(h) + return ud + else: + log('Hook not found') + + def loadfile(self, filename): + try: + self.filename = filename + with open(filename) as f: + data = f.read() + compiled = compile_file(data, filename) + exec(compiled, self.globals) + + try: + self.name = self.globals['__module_name__'] + except KeyError: + lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set') + return False + + self.version = self.globals.get('__module_version__', '') + self.description = self.globals.get('__module_description__', '') + self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), + self.name.encode(), + self.description.encode(), + self.version.encode(), + ffi.NULL) + except Exception as e: + lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode()) + traceback.print_exc() + return False + return True + + def __del__(self): + log('unloading', self.filename) + for hook in self.hooks: + if hook.is_unload is True: + try: + hook.callback(hook.userdata) + except Exception as e: + log('Failed to run hook:', e) + traceback.print_exc() + del self.hooks + if self.ph is not None: + lib.hexchat_plugingui_remove(lib.ph, self.ph) + + +if sys.version_info[0] is 2: + def __decode(string): + return string +else: + def __decode(string): + return string.decode() + + +# There can be empty entries between non-empty ones so find the actual last value +def wordlist_len(words): + for i in range(31, 1, -1): + if ffi.string(words[i]): + return i + return 0 + + +def create_wordlist(words): + size = wordlist_len(words) + return [__decode(ffi.string(words[i])) for i in range(1, size + 1)] + + +# This function only exists for compat reasons with the C plugin +# It turns the word list from print hooks into a word_eol list +# This makes no sense to do... +def create_wordeollist(words): + words = reversed(words) + last = None + accum = None + ret = [] + for word in words: + if accum is None: + accum = word + elif word: + last = accum + accum = ' '.join((word, last)) + ret.insert(0, accum) + return ret + + +def to_cb_ret(value): + if value is None: + return 0 + else: + return int(value) + + +@ffi.def_extern() +def _on_command_hook(word, word_eol, userdata): + hook = ffi.from_handle(userdata) + word = create_wordlist(word) + word_eol = create_wordlist(word_eol) + return to_cb_ret(hook.callback(word, word_eol, hook.userdata)) + + +@ffi.def_extern() +def _on_print_hook(word, userdata): + hook = ffi.from_handle(userdata) + word = create_wordlist(word) + word_eol = create_wordeollist(word) + return to_cb_ret(hook.callback(word, word_eol, hook.userdata)) + + +@ffi.def_extern() +def _on_print_attrs_hook(word, attrs, userdata): + hook = ffi.from_handle(userdata) + word = create_wordlist(word) + word_eol = create_wordeollist(word) + attr = Attribute() + attr.time = attrs.server_time_utc + return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr)) + + +@ffi.def_extern() +def _on_server_hook(word, word_eol, userdata): + hook = ffi.from_handle(userdata) + word = create_wordlist(word) + word_eol = create_wordlist(word_eol) + return to_cb_ret(hook.callback(word, word_eol, hook.userdata)) + + +@ffi.def_extern() +def _on_server_attrs_hook(word, word_eol, attrs, userdata): + hook = ffi.from_handle(userdata) + word = create_wordlist(word) + word_eol = create_wordlist(word_eol) + attr = Attribute() + attr.time = attrs.server_time_utc + return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr)) + + +@ffi.def_extern() +def _on_timer_hook(userdata): + hook = ffi.from_handle(userdata) + if hook.callback(hook.userdata) is True: + return 1 + else: + hook.is_unload = True # Don't unhook + for h in hook.plugin.hooks: + if h == hook: + hook.plugin.hooks.remove(h) + break + return 0 + + +@ffi.def_extern(error=3) +def _on_say_command(word, word_eol, userdata): + channel = ffi.string(lib.hexchat_get_info(lib.ph, b'channel')) + if channel == b'>>python<<': + python = ffi.string(word_eol[1]) + lib.hexchat_print(lib.ph, b'>>> ' + python) + exec_in_interp(__decode(python)) + return 0 + + +def load_filename(filename): + filename = os.path.expanduser(filename) + if not os.path.isabs(filename): + configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir'))) + filename = os.path.join(configdir, 'addons', filename) + if filename and not any(plugin.filename == filename for plugin in plugins): + plugin = Plugin() + if plugin.loadfile(filename): + plugins.add(plugin) + return True + return False + + +def unload_name(name): + if name: + for plugin in plugins: + if name in (plugin.name, plugin.filename, + os.path.basename(plugin.filename)): + plugins.remove(plugin) + return True + return False + + +def reload_name(name): + if name: + for plugin in plugins: + if name in (plugin.name, plugin.filename, + os.path.basename(plugin.filename)): + filename = plugin.filename + plugins.remove(plugin) + return load_filename(filename) + return False + + +@contextmanager +def change_cwd(path): + old_cwd = os.getcwd() + os.chdir(path) + yield + os.chdir(old_cwd) + + +def autoload(): + configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir'))) + addondir = os.path.join(configdir, 'addons') + try: + with change_cwd(addondir): # Maintaining old behavior + for f in os.listdir(addondir): + if f.endswith('.py'): + log('Autoloading', f) + # TODO: Set cwd + load_filename(os.path.join(addondir, f)) + except FileNotFoundError as e: + log('Autoload failed', e) + + +def list_plugins(): + if not plugins: + lib.hexchat_print(lib.ph, b'No python modules loaded') + return + + lib.hexchat_print(lib.ph, b'Name Version Filename Description') + lib.hexchat_print(lib.ph, b'---- ------- -------- -----------') + for plugin in plugins: + basename = os.path.basename(plugin.filename).encode() + name = plugin.name.encode() + version = plugin.version.encode() if plugin.version else b'' + description = plugin.description.encode() if plugin.description else b'' + string = b'%-12s %-8s %-20s %-10s' %(name, version, basename, description) + lib.hexchat_print(lib.ph, string) + lib.hexchat_print(lib.ph, b'') + + +def exec_in_interp(python): + global local_interp + + if not python: + return + + if local_interp is None: + local_interp = Plugin() + local_interp.locals = {} + local_interp.globals['hexchat'] = hexchat + + code = compile_line(python) + try: + ret = eval(code, local_interp.globals, local_interp.locals) + if ret is not None: + lib.hexchat_print(lib.ph, '{}'.format(ret).encode()) + except Exception as e: + traceback.print_exc(file=hexchat_stdout) + + +@ffi.def_extern() +def _on_load_command(word, word_eol, userdata): + filename = ffi.string(word[2]) + if filename.endswith(b'.py'): + load_filename(__decode(filename)) + return 3 + return 0 + + +@ffi.def_extern() +def _on_unload_command(word, word_eol, userdata): + filename = ffi.string(word[2]) + if filename.endswith(b'.py'): + unload_name(__decode(filename)) + return 3 + return 0 + + +@ffi.def_extern() +def _on_reload_command(word, word_eol, userdata): + filename = ffi.string(word[2]) + if filename.endswith(b'.py'): + reload_name(__decode(filename)) + return 3 + return 0 + + +@ffi.def_extern(error=3) +def _on_py_command(word, word_eol, userdata): + subcmd = __decode(ffi.string(word[2])).lower() + + if subcmd == 'exec': + python = __decode(ffi.string(word_eol[3])) + exec_in_interp(python) + elif subcmd == 'load': + filename = __decode(ffi.string(word[3])) + load_filename(filename) + elif subcmd == 'unload': + name = __decode(ffi.string(word[3])) + if not unload_name(name): + lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name') + elif subcmd == 'reload': + name = __decode(ffi.string(word[3])) + if not reload_name(name): + lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name') + elif subcmd == 'console': + lib.hexchat_command(lib.ph, b'QUERY >>python<<') + elif subcmd == 'list': + list_plugins() + elif subcmd == 'about': + lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION) + else: + lib.hexchat_command(lib.ph, b'HELP PY') + + return 3 + + +@ffi.def_extern() +def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir): + global hexchat + global hexchat_stdout + + signal.signal(signal.SIGINT, signal.SIG_DFL) + + plugin_name[0] = PLUGIN_NAME + plugin_desc[0] = PLUGIN_DESC + plugin_version[0] = PLUGIN_VERSION + + try: + libdir = __decode(ffi.string(libdir)) + modpath = os.path.join(libdir, '..', 'python') + sys.path.append(os.path.abspath(modpath)) + hexchat = importlib.import_module('hexchat') + except (UnicodeDecodeError, ImportError) as e: + lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode()) + return 0 + + hexchat_stdout = Stdout() + sys.stdout = hexchat_stdout + sys.stderr = hexchat_stdout + + lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL) + lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL) + lib.hexchat_hook_command(lib.ph, b'UNLOAD', 0, lib._on_unload_command, ffi.NULL, ffi.NULL) + lib.hexchat_hook_command(lib.ph, b'RELOAD', 0, lib._on_reload_command, ffi.NULL, ffi.NULL) + lib.hexchat_hook_command(lib.ph, b'PY', 0, lib._on_py_command, b'''Usage: /PY LOAD + UNLOAD + RELOAD + LIST + EXEC + CONSOLE + ABOUT''', ffi.NULL) + + lib.hexchat_print(lib.ph, b'Python interface loaded') + autoload() + return 1 + + +@ffi.def_extern() +def _on_plugin_deinit(): + global local_interp + global hexchat + global hexchat_stdout + global plugins + + plugins = set() + local_interp = None + hexchat = None + hexchat_stdout = None + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'): + try: + del sys.modules[mod] + except KeyError: + pass + + return 1 -- cgit 1.4.1 From a2ff661d40bcd49a0be973b7b60583fde64e09c2 Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 25 Jul 2018 21:41:05 +0200 Subject: python: Various cffi fixes - fixed /py exec behaviour - fixed hexchat.unload_hook() failing when passed a hook id - fixed get_list() calls in python3 --- plugins/python/_hexchat.py | 11 ++++++++++- plugins/python/python.py | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'plugins/python/python.py') diff --git a/plugins/python/_hexchat.py b/plugins/python/_hexchat.py index 52b3ec14..50ccfb83 100644 --- a/plugins/python/_hexchat.py +++ b/plugins/python/_hexchat.py @@ -150,6 +150,15 @@ class ListItem: return '<{} list item at {}>'.format(self._listname, id(self)) +# done this way for speed +if sys.version_info[0] == 2: + def get_getter(name): + return ord(name[0]) +else: + def get_getter(name): + return name[0] + + def get_list(name): # XXX: This function is extremely inefficient and could be interators and # lazily loaded properties, but for API compat we stay slow @@ -189,7 +198,7 @@ def get_list(name): while lib.hexchat_list_next(lib.ph, list_) is 1: item = ListItem(orig_name) for field in fields: - getter = getters.get(ord(field[0])) + getter = getters.get(get_getter(field)) if getter is not None: field_name = field[1:] setattr(item, __cached_decoded_str(field_name), getter(field_name)) diff --git a/plugins/python/python.py b/plugins/python/python.py index 3845a79f..e6a61b5e 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -98,7 +98,8 @@ else: return compile(data, filename, 'exec', optimize=2, dont_inherit=True) def compile_line(string): - return compile(string, '', 'eval', optimize=2, dont_inherit=True) + # newline appended to solve unexpected EOF issues + return compile(string + '\n', '', 'single', optimize=2, dont_inherit=True) class Plugin: @@ -122,7 +123,7 @@ class Plugin: def remove_hook(self, hook): for h in self.hooks: if id(h) == hook: - ud = hook.userdata + ud = h.userdata self.hooks.remove(h) return ud else: -- cgit 1.4.1 From ed55330153e7d85e753d1321c4e46e9bb6833735 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Wed, 5 Dec 2018 19:45:30 -0500 Subject: python: Fix console not eating commands --- plugins/python/python.py | 1 + 1 file changed, 1 insertion(+) (limited to 'plugins/python/python.py') diff --git a/plugins/python/python.py b/plugins/python/python.py index e6a61b5e..1eeb10b4 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -281,6 +281,7 @@ def _on_say_command(word, word_eol, userdata): python = ffi.string(word_eol[1]) lib.hexchat_print(lib.ph, b'>>> ' + python) exec_in_interp(__decode(python)) + return 1 return 0 -- cgit 1.4.1 From 3ebfa83fdd43335da1dd2d39f0bfae91d67b8c90 Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 26 Dec 2018 20:37:56 +0200 Subject: python: Made sure to set sys.argv if it is not set. fixes #2282 --- plugins/python/python.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'plugins/python/python.py') diff --git a/plugins/python/python.py b/plugins/python/python.py index 1eeb10b4..942b0ce5 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -10,6 +10,9 @@ import traceback import weakref from _hexchat_embedded import ffi, lib +if not hasattr(sys, 'argv'): + sys.argv = [''] + VERSION = b'2.0' # Sync with hexchat.__version__ PLUGIN_NAME = ffi.new('char[]', b'Python') PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' -- cgit 1.4.1 From f7713a6a64ee55d3c20e9e27b8f8a5e98385ff57 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Wed, 26 Dec 2018 16:15:25 -0600 Subject: python: Make the plugins table dynamically sized (#2291) Adjust the width of the columns depending on the length of the data in each element --- plugins/python/python.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'plugins/python/python.py') diff --git a/plugins/python/python.py b/plugins/python/python.py index 942b0ce5..371fbf40 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -349,15 +349,28 @@ def list_plugins(): lib.hexchat_print(lib.ph, b'No python modules loaded') return - lib.hexchat_print(lib.ph, b'Name Version Filename Description') - lib.hexchat_print(lib.ph, b'---- ------- -------- -----------') + tbl_headers = [b'Name', b'Version', b'Filename', b'Description'] + tbl = [ + tbl_headers, + [(b'-' * len(s)) for s in tbl_headers] + ] + for plugin in plugins: basename = os.path.basename(plugin.filename).encode() name = plugin.name.encode() version = plugin.version.encode() if plugin.version else b'' description = plugin.description.encode() if plugin.description else b'' - string = b'%-12s %-8s %-20s %-10s' %(name, version, basename, description) - lib.hexchat_print(lib.ph, string) + tbl.append((name, version, basename, description)) + + column_sizes = [ + max(len(item) for item in column) + for column in zip(*tbl) + ] + + for row in tbl: + lib.hexchat_print(lib.ph, b' '.join(item.ljust(column_sizes[i]) + for i, item in enumerate(row))) + lib.hexchat_print(lib.ph, b'') -- cgit 1.4.1 From a5a727122b66c9003b44fcdc199ad56dbe15a131 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Thu, 27 Dec 2018 13:46:02 -0600 Subject: python: Make sure `help()` doesn't cause hexchat to hang (#2290) * Make sure `help()` doesn't cause hexchat to hang Replace `pydoc.help` with a copy of `pydoc.Helper` with an empty `StringIO` instead of stdin * Handle BytesIO vs StringIO on 2.7 --- plugins/python/python.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'plugins/python/python.py') diff --git a/plugins/python/python.py b/plugins/python/python.py index 371fbf40..1afb36c4 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -1,6 +1,7 @@ from __future__ import print_function import os +import pydoc import sys from contextlib import contextmanager import importlib @@ -8,6 +9,12 @@ import signal import site import traceback import weakref + +if sys.version_info < (3, 0): + from io import BytesIO as HelpEater +else: + from io import StringIO as HelpEater + from _hexchat_embedded import ffi, lib if not hasattr(sys, 'argv'): @@ -57,7 +64,6 @@ class Stdout: self.buffer += string def isatty(self): - # FIXME: help() locks app despite this? return False @@ -474,6 +480,7 @@ def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir): hexchat_stdout = Stdout() sys.stdout = hexchat_stdout sys.stderr = hexchat_stdout + pydoc.help = pydoc.Helper(HelpEater(), HelpEater()) lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL) lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL) @@ -505,6 +512,7 @@ def _on_plugin_deinit(): hexchat_stdout = None sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ + pydoc.help = pydoc.Helper() for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'): try: -- cgit 1.4.1 From 7abeb10cf1f82fbad4d167f9e6f6918e1f47650b Mon Sep 17 00:00:00 2001 From: A_D Date: Wed, 26 Dec 2018 20:46:31 +0200 Subject: python: plugin cleanup and refactor --- plugins/python/_hexchat.py | 91 +++++++++++++++++++-------------- plugins/python/python.py | 99 +++++++++++++++++++++++------------- plugins/python/python_style_guide.md | 26 ++++++++++ 3 files changed, 143 insertions(+), 73 deletions(-) create mode 100644 plugins/python/python_style_guide.md (limited to 'plugins/python/python.py') diff --git a/plugins/python/_hexchat.py b/plugins/python/_hexchat.py index 50ccfb83..ebee5657 100644 --- a/plugins/python/_hexchat.py +++ b/plugins/python/_hexchat.py @@ -1,6 +1,7 @@ -from contextlib import contextmanager import inspect import sys +from contextlib import contextmanager + from _hexchat_embedded import ffi, lib __all__ = [ @@ -40,13 +41,15 @@ def __get_current_plugin(): while '__plugin' not in frame.f_globals: frame = frame.f_back assert frame is not None + return frame.f_globals['__plugin'] # Keeping API compat -if sys.version_info[0] is 2: +if sys.version_info[0] == 2: def __decode(string): return string + else: def __decode(string): return string.decode() @@ -64,16 +67,18 @@ def emit_print(event_name, *args, **kwargs): arg = args[i].encode() if len(args) > i else b'' cstring = ffi.new('char[]', arg) cargs.append(cstring) - if time is 0: + + if time == 0: return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs) - else: - attrs = lib.hexchat_event_attrs_create(lib.ph) - attrs.server_time_utc = time - ret = lib.hexchat_emit_print(lib.ph, attrs, event_name.encode(), *cargs) - lib.hexchat_event_attrs_free(lib.ph, attrs) - return ret + + attrs = lib.hexchat_event_attrs_create(lib.ph) + attrs.server_time_utc = time + ret = lib.hexchat_emit_print(lib.ph, attrs, event_name.encode(), *cargs) + lib.hexchat_event_attrs_free(lib.ph, attrs) + return ret +# TODO: this shadows itself. command should be changed to cmd def command(command): lib.hexchat_command(lib.ph, command.encode()) @@ -97,21 +102,24 @@ def get_info(name): # Surely there is a less dumb way? ptr = repr(ret).rsplit(' ', 1)[1][:-1] return ptr + return __decode(ffi.string(ret)) def get_prefs(name): string_out = ffi.new('char**') int_out = ffi.new('int*') - type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out) - if type is 0: + _type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out) + if _type == 0: return None - elif type is 1: + + if _type == 1: return __decode(ffi.string(string_out[0])) - elif type is 2 or type is 3: # XXX: 3 should be a bool, but keeps API + + if _type in (2, 3): # XXX: 3 should be a bool, but keeps API return int_out[0] - else: - assert False + + raise AssertionError('Out of bounds pref storage') def __cstrarray_to_list(arr): @@ -120,6 +128,7 @@ def __cstrarray_to_list(arr): while arr[i] != ffi.NULL: ret.append(ffi.string(arr[i])) i += 1 + return ret @@ -127,8 +136,7 @@ __FIELD_CACHE = {} def __get_fields(name): - return __FIELD_CACHE.setdefault(name, - __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name))) + return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name))) __FIELD_PROPERTY_CACHE = {} @@ -154,6 +162,7 @@ class ListItem: if sys.version_info[0] == 2: def get_getter(name): return ord(name[0]) + else: def get_getter(name): return name[0] @@ -179,6 +188,7 @@ def get_list(name): string = lib.hexchat_list_str(lib.ph, list_, field) if string != ffi.NULL: return __decode(ffi.string(string)) + return '' def ptr_getter(field): @@ -186,6 +196,7 @@ def get_list(name): ptr = lib.hexchat_list_str(lib.ph, list_, field) ctx = ffi.cast('hexchat_context*', ptr) return Context(ctx) + return None getters = { @@ -195,25 +206,27 @@ def get_list(name): ord('p'): ptr_getter, } - while lib.hexchat_list_next(lib.ph, list_) is 1: + while lib.hexchat_list_next(lib.ph, list_) == 1: item = ListItem(orig_name) - for field in fields: - getter = getters.get(get_getter(field)) + for _field in fields: + getter = getters.get(get_getter(_field)) if getter is not None: - field_name = field[1:] + field_name = _field[1:] setattr(item, __cached_decoded_str(field_name), getter(field_name)) + ret.append(item) lib.hexchat_list_free(lib.ph, list_) return ret +# TODO: 'command' here shadows command above, and should be renamed to cmd def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None): plugin = __get_current_plugin() hook = plugin.add_hook(callback, userdata) handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook, - help.encode() if help is not None else ffi.NULL, - hook.handle) + help.encode() if help is not None else ffi.NULL, hook.handle) + hook.hexchat_hook = handle return id(hook) @@ -221,8 +234,7 @@ def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None) def hook_print(name, callback, userdata=None, priority=PRI_NORM): plugin = __get_current_plugin() hook = plugin.add_hook(callback, userdata) - handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, - hook.handle) + handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, hook.handle) hook.hexchat_hook = handle return id(hook) @@ -230,8 +242,7 @@ def hook_print(name, callback, userdata=None, priority=PRI_NORM): def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM): plugin = __get_current_plugin() hook = plugin.add_hook(callback, userdata) - handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, - hook.handle) + handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, hook.handle) hook.hexchat_hook = handle return id(hook) @@ -239,8 +250,7 @@ def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM): def hook_server(name, callback, userdata=None, priority=PRI_NORM): plugin = __get_current_plugin() hook = plugin.add_hook(callback, userdata) - handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, - hook.handle) + handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, hook.handle) hook.hexchat_hook = handle return id(hook) @@ -248,8 +258,7 @@ def hook_server(name, callback, userdata=None, priority=PRI_NORM): def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM): plugin = __get_current_plugin() hook = plugin.add_hook(callback, userdata) - handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, - hook.handle) + handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, hook.handle) hook.hexchat_hook = handle return id(hook) @@ -276,17 +285,18 @@ def unhook(handle): def set_pluginpref(name, value): if isinstance(value, str): return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode())) - elif isinstance(value, int): + + if isinstance(value, int): return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value)) - else: - # XXX: This should probably raise but this keeps API - return False + + # XXX: This should probably raise but this keeps API + return False def get_pluginpref(name): name = name.encode() string_out = ffi.new('char[512]') - if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) is not 1: + if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) != 1: return None string = ffi.string(string_out) @@ -308,8 +318,9 @@ def del_pluginpref(name): def list_pluginpref(): prefs_str = ffi.new('char[4096]') - if lib.hexchat_pluginpref_list(lib.ph, prefs_str) is 1: + if lib.hexchat_pluginpref_list(lib.ph, prefs_str) == 1: return __decode(prefs_str).split(',') + return [] @@ -320,6 +331,7 @@ class Context: def __eq__(self, value): if not isinstance(value, Context): return False + return self._ctx == value._ctx @contextmanager @@ -327,9 +339,9 @@ class Context: old_ctx = lib.hexchat_get_context(lib.ph) if not self.set(): # XXX: Behavior change, previously used wrong context - lib.hexchat_print(lib.ph, - b'Context object refers to closed context, ignoring call') + lib.hexchat_print(lib.ph, b'Context object refers to closed context, ignoring call') return + yield lib.hexchat_set_context(lib.ph, old_ctx) @@ -370,4 +382,5 @@ def find_context(server=None, channel=None): ctx = lib.hexchat_find_context(lib.ph, server, channel) if ctx == ffi.NULL: return None + return Context(ctx) diff --git a/plugins/python/python.py b/plugins/python/python.py index 1afb36c4..30694802 100644 --- a/plugins/python/python.py +++ b/plugins/python/python.py @@ -1,30 +1,30 @@ from __future__ import print_function +import importlib import os import pydoc -import sys -from contextlib import contextmanager -import importlib import signal -import site +import sys import traceback import weakref +from contextlib import contextmanager + +from _hexchat_embedded import ffi, lib if sys.version_info < (3, 0): from io import BytesIO as HelpEater else: from io import StringIO as HelpEater -from _hexchat_embedded import ffi, lib - if not hasattr(sys, 'argv'): sys.argv = [''] VERSION = b'2.0' # Sync with hexchat.__version__ PLUGIN_NAME = ffi.new('char[]', b'Python') -PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' - % (sys.version_info[0], sys.version_info[1])) +PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1])) PLUGIN_VERSION = ffi.new('char[]', VERSION) + +# TODO: Constants should be screaming snake case hexchat = None local_interp = None hexchat_stdout = None @@ -40,10 +40,11 @@ def redirected_stdout(): sys.stderr = hexchat_stdout -if os.environ.get('HEXCHAT_LOG_PYTHON'): +if os.getenv('HEXCHAT_LOG_PYTHON'): def log(*args): with redirected_stdout(): print(*args) + else: def log(*args): pass @@ -56,7 +57,7 @@ class Stdout: def write(self, string): string = string.encode() idx = string.rfind(b'\n') - if idx is not -1: + if idx != -1: self.buffer += string[:idx] lib.hexchat_print(lib.ph, bytes(self.buffer)) self.buffer = bytearray(string[idx + 1:]) @@ -91,13 +92,15 @@ class Hook: lib.hexchat_unhook(lib.ph, self.hexchat_hook) -if sys.version_info[0] is 2: +if sys.version_info[0] == 2: def compile_file(data, filename): return compile(data, filename, 'exec', dont_inherit=True) + def compile_line(string): try: return compile(string, '', 'eval', dont_inherit=True) + except SyntaxError: # For some reason `print` is invalid for eval # This will hide any return value though @@ -106,6 +109,7 @@ else: def compile_file(data, filename): return compile(data, filename, 'exec', optimize=2, dont_inherit=True) + def compile_line(string): # newline appended to solve unexpected EOF issues return compile(string + '\n', '', 'single', optimize=2, dont_inherit=True) @@ -135,8 +139,9 @@ class Plugin: ud = h.userdata self.hooks.remove(h) return ud - else: - log('Hook not found') + + log('Hook not found') + return None def loadfile(self, filename): try: @@ -148,21 +153,22 @@ class Plugin: try: self.name = self.globals['__module_name__'] + except KeyError: lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set') + return False self.version = self.globals.get('__module_version__', '') self.description = self.globals.get('__module_description__', '') - self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), - self.name.encode(), - self.description.encode(), - self.version.encode(), - ffi.NULL) + self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), self.name.encode(), + self.description.encode(), self.version.encode(), ffi.NULL) + except Exception as e: lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode()) traceback.print_exc() return False + return True def __del__(self): @@ -171,17 +177,20 @@ class Plugin: if hook.is_unload is True: try: hook.callback(hook.userdata) + except Exception as e: log('Failed to run hook:', e) traceback.print_exc() + del self.hooks if self.ph is not None: lib.hexchat_plugingui_remove(lib.ph, self.ph) -if sys.version_info[0] is 2: +if sys.version_info[0] == 2: def __decode(string): return string + else: def __decode(string): return string.decode() @@ -192,6 +201,7 @@ def wordlist_len(words): for i in range(31, 1, -1): if ffi.string(words[i]): return i + return 0 @@ -205,24 +215,26 @@ def create_wordlist(words): # This makes no sense to do... def create_wordeollist(words): words = reversed(words) - last = None accum = None ret = [] for word in words: if accum is None: accum = word + elif word: last = accum accum = ' '.join((word, last)) + ret.insert(0, accum) + return ret def to_cb_ret(value): if value is None: return 0 - else: - return int(value) + + return int(value) @ffi.def_extern() @@ -274,13 +286,14 @@ def _on_timer_hook(userdata): hook = ffi.from_handle(userdata) if hook.callback(hook.userdata) is True: return 1 - else: - hook.is_unload = True # Don't unhook - for h in hook.plugin.hooks: - if h == hook: - hook.plugin.hooks.remove(h) - break - return 0 + + hook.is_unload = True # Don't unhook + for h in hook.plugin.hooks: + if h == hook: + hook.plugin.hooks.remove(h) + break + + return 0 @ffi.def_extern(error=3) @@ -291,6 +304,7 @@ def _on_say_command(word, word_eol, userdata): lib.hexchat_print(lib.ph, b'>>> ' + python) exec_in_interp(__decode(python)) return 1 + return 0 @@ -298,33 +312,36 @@ def load_filename(filename): filename = os.path.expanduser(filename) if not os.path.isabs(filename): configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir'))) + filename = os.path.join(configdir, 'addons', filename) + if filename and not any(plugin.filename == filename for plugin in plugins): plugin = Plugin() if plugin.loadfile(filename): plugins.add(plugin) return True + return False def unload_name(name): if name: for plugin in plugins: - if name in (plugin.name, plugin.filename, - os.path.basename(plugin.filename)): + if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)): plugins.remove(plugin) return True + return False def reload_name(name): if name: for plugin in plugins: - if name in (plugin.name, plugin.filename, - os.path.basename(plugin.filename)): + if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)): filename = plugin.filename plugins.remove(plugin) return load_filename(filename) + return False @@ -346,6 +363,7 @@ def autoload(): log('Autoloading', f) # TODO: Set cwd load_filename(os.path.join(addondir, f)) + except FileNotFoundError as e: log('Autoload failed', e) @@ -376,7 +394,6 @@ def list_plugins(): for row in tbl: lib.hexchat_print(lib.ph, b' '.join(item.ljust(column_sizes[i]) for i, item in enumerate(row))) - lib.hexchat_print(lib.ph, b'') @@ -396,6 +413,7 @@ def exec_in_interp(python): ret = eval(code, local_interp.globals, local_interp.locals) if ret is not None: lib.hexchat_print(lib.ph, '{}'.format(ret).encode()) + except Exception as e: traceback.print_exc(file=hexchat_stdout) @@ -406,6 +424,7 @@ def _on_load_command(word, word_eol, userdata): if filename.endswith(b'.py'): load_filename(__decode(filename)) return 3 + return 0 @@ -415,6 +434,7 @@ def _on_unload_command(word, word_eol, userdata): if filename.endswith(b'.py'): unload_name(__decode(filename)) return 3 + return 0 @@ -424,6 +444,7 @@ def _on_reload_command(word, word_eol, userdata): if filename.endswith(b'.py'): reload_name(__decode(filename)) return 3 + return 0 @@ -434,23 +455,30 @@ def _on_py_command(word, word_eol, userdata): if subcmd == 'exec': python = __decode(ffi.string(word_eol[3])) exec_in_interp(python) + elif subcmd == 'load': filename = __decode(ffi.string(word[3])) load_filename(filename) + elif subcmd == 'unload': name = __decode(ffi.string(word[3])) if not unload_name(name): lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name') + elif subcmd == 'reload': name = __decode(ffi.string(word[3])) if not reload_name(name): lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name') + elif subcmd == 'console': lib.hexchat_command(lib.ph, b'QUERY >>python<<') + elif subcmd == 'list': list_plugins() + elif subcmd == 'about': lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION) + else: lib.hexchat_command(lib.ph, b'HELP PY') @@ -473,8 +501,10 @@ def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir): modpath = os.path.join(libdir, '..', 'python') sys.path.append(os.path.abspath(modpath)) hexchat = importlib.import_module('hexchat') + except (UnicodeDecodeError, ImportError) as e: lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode()) + return 0 hexchat_stdout = Stdout() @@ -517,6 +547,7 @@ def _on_plugin_deinit(): for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'): try: del sys.modules[mod] + except KeyError: pass diff --git a/plugins/python/python_style_guide.md b/plugins/python/python_style_guide.md new file mode 100644 index 00000000..41db2474 --- /dev/null +++ b/plugins/python/python_style_guide.md @@ -0,0 +1,26 @@ +# HexChat Python Module Style Guide + +(This is a work in progress). + +## General rules + +- PEP8 as general fallback recommendations +- Max line length: 120 +- Avoid overcomplex compound statements. i.e. dont do this: `somevar = x if x == y else z if a == b and c == b else x` + +## Indentation style + +### Multi-line functions + +```python +foo(really_long_arg_1, + really_long_arg_2) +``` + +### Mutli-line lists/dicts + +```python +foo = { + 'bar': 'baz', +} +``` -- cgit 1.4.1