Commit 84e59b05 authored by mathieui's avatar mathieui

Don’t call input completion() functions inside completion methods

Use a placeholder object that can run it afterwards, so that we don’t
have side effects inside those functions.
parent 6c270b36
......@@ -88,9 +88,13 @@ structured as key (command name) -> tuple(command function, help string, complet
Completions are a bit tricky, but it’s easy once you get used to it:
They take an **Input** (a _windows_ class) as a parameter, named the_input
everywhere in the sources. To effectively have a completion, you have to call
**the_input.auto_completion()** or **the_input.new_completion()** with the relevant
parameters before returning from the function.
everywhere in the sources. To effectively have a completion, you have to create
a :py:class:`poezio.core.structs.Completion` object initialized with the
completion you want to call
(**the_input.auto_completion()** or **the_input.new_completion()**) with the
relevant parameters and return it with the function. Previously you would call
the function directly from the completion method, but having side effects
inside it makes it harder to test.
.. code-block:: python
......
......@@ -53,6 +53,7 @@ For affiliations
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
from poezio.core.structs import Completion
class Plugin(BasePlugin):
"""
......@@ -113,7 +114,7 @@ class Plugin(BasePlugin):
compare_users = lambda x: x.last_talked
word_list = [user.nick for user in sorted(tab.users, key=compare_users, reverse=True)\
if user.nick != tab.own_nick]
return the_input.auto_completion(word_list, '')
return Completion(the_input.auto_completion, word_list, '')
......@@ -66,6 +66,7 @@ Example of the syntax:
from poezio.plugin import BasePlugin
from poezio.common import shell_split
from poezio.core.structs import Completion
class Plugin(BasePlugin):
......@@ -140,7 +141,7 @@ class Plugin(BasePlugin):
"Completion for /unalias"
aliases = [alias for alias in self.commands]
aliases.sort()
return the_input.auto_completion(aliases, '', quotify=False)
return Completion(the_input.auto_completion, aliases, '', quotify=False)
def get_command(self, name):
"""Returns the function associated with a command"""
......
......@@ -22,6 +22,7 @@ Configuration options
The time during which the file should stay in cache on the receiving side.
"""
from poezio.core.structs import Completion
from poezio.plugin import BasePlugin
from poezio import tabs
......@@ -72,4 +73,4 @@ class Plugin(BasePlugin):
mime_type = guess_type(filename)[0]
if mime_type is not None and mime_type.startswith('image/'):
images.append(filename)
return the_input.auto_completion(images, quotify=False)
return Completion(the_input.auto_completion, images, quotify=False)
......@@ -131,6 +131,7 @@ Example configuration
from poezio.plugin import BasePlugin
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
from poezio import common
from poezio import tabs
......@@ -270,7 +271,7 @@ class Plugin(BasePlugin):
sections.remove(section)
except:
pass
return the_input.new_completion(sections, pos)
return Completion(the_input.new_completion, sections, pos)
@command_args_parser.quoted(1, 1)
def command_irc_join(self, args):
......@@ -375,6 +376,6 @@ class Plugin(BasePlugin):
sections = self.config.sections()
if 'irc' in sections:
sections.remove('irc')
return the_input.new_completion(sections, 1)
return Completion(the_input.new_completion, sections, 1)
......@@ -49,6 +49,7 @@ Usage
from poezio.plugin import BasePlugin
from poezio.common import shell_split
from poezio.core.structs import Completion
from os.path import basename as base
from poezio import tabs
import mpd
......@@ -84,4 +85,4 @@ class Plugin(BasePlugin):
self.api.information('Cannot send result (%s)' % s, 'Error')
def completion_mpd(self, the_input):
return the_input.auto_completion(['full'], quotify=False)
return Completion(the_input.auto_completion, ['full'], quotify=False)
......@@ -194,6 +194,7 @@ from poezio.plugin import BasePlugin
from poezio.tabs import ConversationTab, DynamicConversationTab, PrivateTab
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
OTR_DIR = os.path.join(os.getenv('XDG_DATA_HOME') or
'~/.local/share', 'poezio', 'otr')
......@@ -912,7 +913,7 @@ class Plugin(BasePlugin):
Completion for /otr
"""
comp = ['start', 'fpr', 'ourfpr', 'refresh', 'end', 'trust', 'untrust']
return the_input.new_completion(comp, 1, quotify=False)
return Completion(the_input.new_completion, comp, 1, quotify=False)
@command_args_parser.quoted(1, 2)
def command_smp(self, args):
......@@ -972,7 +973,7 @@ class Plugin(BasePlugin):
def completion_smp(the_input):
"""Completion for /otrsmp"""
if the_input.get_argument_position() == 1:
return the_input.new_completion(['ask', 'answer', 'abort'], 1, quotify=False)
return Completion(the_input.new_completion, ['ask', 'answer', 'abort'], 1, quotify=False)
def get_tlv(tlvs, cls):
"""Find the instance of a class in a list"""
......
......@@ -27,6 +27,7 @@ from poezio.plugin import BasePlugin
from poezio.roster import roster
from poezio.common import safeJID
from poezio.contact import Contact, Resource
from poezio.core.structs import Completion
from poezio import tabs
import time
......@@ -74,7 +75,7 @@ class Plugin(BasePlugin):
users = [user.nick for user in self.api.current_tab().users]
l = self.resources()
users.extend(l)
return the_input.auto_completion(users, '', quotify=False)
return Completion(the_input.auto_completion, users, '', quotify=False)
@command_args_parser.raw
def command_private_ping(self, arg):
......@@ -115,5 +116,5 @@ class Plugin(BasePlugin):
return l
def completion_ping(self, the_input):
return the_input.auto_completion(self.resources(), '', quotify=False)
return Completion(the_input.auto_completion, self.resources(), '', quotify=False)
......@@ -44,6 +44,7 @@ Options
time of the message.
"""
from poezio.core.structs import Completion
from poezio.plugin import BasePlugin
from poezio.xhtml import clean_text
from poezio import common
......@@ -101,5 +102,5 @@ class Plugin(BasePlugin):
messages = list(filter(message_match, messages))
elif len(args) > 1:
return False
return the_input.auto_completion([clean_text(msg.txt) for msg in messages[::-1]], '')
return Completion(the_input.auto_completion, [clean_text(msg.txt) for msg in messages[::-1]], '')
......@@ -47,10 +47,11 @@ Will remind you to get up every 1 hour 23 minutes.
"""
from poezio.core.structs import Completion
from poezio.plugin import BasePlugin
import curses
from poezio import common
from poezio import timed_events
from poezio import common
import curses
class Plugin(BasePlugin):
......@@ -107,10 +108,10 @@ class Plugin(BasePlugin):
if txt.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion(["60", "5m", "15m", "30m", "1h", "10h", "1d"], '')
return Completion(the_input.auto_completion, ["60", "5m", "15m", "30m", "1h", "10h", "1d"], '')
def completion_done(self, the_input):
return the_input.auto_completion(["%s" % key for key in self.tasks], '')
return Completion(the_input.auto_completion, ["%s" % key for key in self.tasks], '')
def command_done(self, arg="0"):
try:
......
......@@ -19,6 +19,7 @@ This plugin adds a command to the chat tabs.
"""
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
from poezio import tabs
from poezio import common
......@@ -54,7 +55,7 @@ class Plugin(BasePlugin):
if txt.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion(["60", "5m", "15m", "30m", "1h", "10h", "1d"], '')
return Completion(the_input.auto_completion, ["60", "5m", "15m", "30m", "1h", "10h", "1d"], '')
def say(self, args=None):
if not args:
......
......@@ -20,6 +20,7 @@ This plugin defines two new commands for MUC tabs: :term:`/tell` and :term:`/unt
"""
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
from poezio import tabs
......@@ -77,6 +78,6 @@ class Plugin(BasePlugin):
def completion_untell(self, the_input):
tab = self.api.current_tab()
if not tab in self.tabs:
return the_input.auto_completion([], '')
return the_input.auto_completion(list(self.tabs[tab]), '', quotify=False)
return Completion(the_input.auto_completion, [], '')
return Completion(the_input.auto_completion, list(self.tabs[tab]), '', quotify=False)
......@@ -15,8 +15,7 @@ from poezio.common import safeJID
from poezio.config import config
from poezio.roster import roster
from poezio.core.structs import POSSIBLE_SHOW
from poezio.core.structs import POSSIBLE_SHOW, Completion
class CompletionCore:
def __init__(self, core):
......@@ -25,15 +24,14 @@ class CompletionCore:
def help(self, the_input):
"""Completion for /help."""
commands = sorted(self.core.commands.keys()) + sorted(self.core.current_tab().commands.keys())
return the_input.new_completion(commands, 1, quotify=False)
return Completion(the_input.new_completion, commands, 1, quotify=False)
def status(self, the_input):
"""
Completion of /status
"""
if the_input.get_argument_position() == 1:
return the_input.new_completion([status for status in POSSIBLE_SHOW], 1, ' ', quotify=False)
return Completion(the_input.new_completion, [status for status in POSSIBLE_SHOW], 1, ' ', quotify=False)
def presence(self, the_input):
......@@ -42,9 +40,9 @@ class CompletionCore:
"""
arg = the_input.get_argument_position()
if arg == 1:
return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=True)
return Completion(the_input.auto_completion, [jid for jid in roster.jids()], '', quotify=True)
elif arg == 2:
return the_input.auto_completion([status for status in POSSIBLE_SHOW], '', quotify=True)
return Completion(the_input.auto_completion, [status for status in POSSIBLE_SHOW], '', quotify=True)
def theme(self, the_input):
......@@ -63,7 +61,7 @@ class CompletionCore:
theme_files = [name[:-3] for name in names if name.endswith('.py') and name != '__init__.py']
if 'default' not in theme_files:
theme_files.append('default')
return the_input.new_completion(theme_files, 1, '', quotify=False)
return Completion(the_input.new_completion, theme_files, 1, '', quotify=False)
def win(self, the_input):
......@@ -72,7 +70,7 @@ class CompletionCore:
for tab in self.core.tabs:
l.extend(tab.matching_names())
l = [i[1] for i in l]
return the_input.new_completion(l, 1, '', quotify=False)
return Completion(the_input.new_completion, l, 1, '', quotify=False)
def join(self, the_input):
......@@ -107,7 +105,7 @@ class CompletionCore:
relevant_rooms.extend(sorted(room[0] for room in bookmarks.items() if room[1]))
if the_input.last_completion:
return the_input.new_completion([], 1, quotify=True)
return Completion(the_input.new_completion, [], 1, quotify=True)
if jid.user:
# we are writing the server: complete the server
......@@ -116,18 +114,18 @@ class CompletionCore:
if tab.joined:
serv_list.append('%s@%s' % (jid.user, safeJID(tab.name).host))
serv_list.extend(relevant_rooms)
return the_input.new_completion(serv_list, 1, quotify=True)
return Completion(the_input.new_completion, serv_list, 1, quotify=True)
elif args[1].startswith('/'):
# we completing only a resource
return the_input.new_completion(['/%s' % self.core.own_nick], 1, quotify=True)
return Completion(the_input.new_completion, ['/%s' % self.core.own_nick], 1, quotify=True)
else:
return the_input.new_completion(relevant_rooms, 1, quotify=True)
return Completion(the_input.new_completion, relevant_rooms, 1, quotify=True)
def version(self, the_input):
"""Completion for /version"""
comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
return the_input.new_completion(sorted(comp), 1, quotify=False)
return Completion(the_input.new_completion, sorted(comp), 1, quotify=False)
def list(self, the_input):
......@@ -137,7 +135,7 @@ class CompletionCore:
if tab.name not in muc_serv_list:
muc_serv_list.append(safeJID(tab.name).server)
if muc_serv_list:
return the_input.new_completion(muc_serv_list, 1, quotify=False)
return Completion(the_input.new_completion, muc_serv_list, 1, quotify=False)
def move_tab(self, the_input):
......@@ -146,7 +144,7 @@ class CompletionCore:
if n == 1:
nodes = [tab.name for tab in self.core.tabs if tab]
nodes.remove('Roster')
return the_input.new_completion(nodes, 1, ' ', quotify=True)
return Completion(the_input.new_completion, nodes, 1, ' ', quotify=True)
def runkey(self, the_input):
......@@ -156,7 +154,7 @@ class CompletionCore:
list_ = []
list_.extend(self.core.key_func.keys())
list_.extend(self.core.current_tab().key_func.keys())
return the_input.new_completion(list_, 1, quotify=False)
return Completion(the_input.new_completion, list_, 1, quotify=False)
def bookmark(self, the_input):
......@@ -165,7 +163,7 @@ class CompletionCore:
n = the_input.get_argument_position(quoted=True)
if n == 2:
return the_input.new_completion(['true', 'false'], 2, quotify=True)
return Completion(the_input.new_completion, ['true', 'false'], 2, quotify=True)
if n >= 3:
return False
......@@ -185,23 +183,23 @@ class CompletionCore:
if nick not in nicks:
nicks.append(nick)
jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
return the_input.new_completion(jids_list, 1, quotify=True)
return Completion(the_input.new_completion, jids_list, 1, quotify=True)
muc_list = [tab.name for tab in self.core.get_tabs(tabs.MucTab)]
muc_list.sort()
muc_list.append('*')
return the_input.new_completion(muc_list, 1, quotify=True)
return Completion(the_input.new_completion, muc_list, 1, quotify=True)
def remove_bookmark(self, the_input):
"""Completion for /remove_bookmark"""
return the_input.new_completion([bm.jid for bm in self.core.bookmarks], 1, quotify=False)
return Completion(the_input.new_completion, [bm.jid for bm in self.core.bookmarks], 1, quotify=False)
def decline(self, the_input):
"""Completion for /decline"""
n = the_input.get_argument_position(quoted=True)
if n == 1:
return the_input.auto_completion(sorted(self.core.pending_invites.keys()), 1, '', quotify=True)
return Completion(the_input.auto_completion, sorted(self.core.pending_invites.keys()), 1, '', quotify=True)
def bind(self, the_input):
......@@ -213,7 +211,7 @@ class CompletionCore:
else:
return False
return the_input.new_completion(args, n, '', quotify=False)
return Completion(the_input.new_completion, args, n, '', quotify=False)
def message(self, the_input):
......@@ -230,7 +228,7 @@ class CompletionCore:
for jid in roster.jids():
if not len(roster[jid]):
l.append(jid)
return the_input.new_completion(l, 1, '', quotify=True)
return Completion(the_input.new_completion, l, 1, '', quotify=True)
def invite(self, the_input):
......@@ -242,14 +240,14 @@ class CompletionCore:
bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact]))
off = sorted(jid for jid in roster.jids() if jid not in bares)
comp = comp + bares + off
return the_input.new_completion(comp, n, quotify=True)
return Completion(the_input.new_completion, comp, n, quotify=True)
elif n == 2:
rooms = []
for tab in self.core.get_tabs(tabs.MucTab):
if tab.joined:
rooms.append(tab.name)
rooms.sort()
return the_input.new_completion(rooms, n, '', quotify=True)
return Completion(the_input.new_completion, rooms, n, '', quotify=True)
def activity(self, the_input):
......@@ -257,20 +255,20 @@ class CompletionCore:
n = the_input.get_argument_position(quoted=True)
args = common.shell_split(the_input.text)
if n == 1:
return the_input.new_completion(sorted(pep.ACTIVITIES.keys()), n, quotify=True)
return Completion(the_input.new_completion, sorted(pep.ACTIVITIES.keys()), n, quotify=True)
elif n == 2:
if args[1] in pep.ACTIVITIES:
l = list(pep.ACTIVITIES[args[1]])
l.remove('category')
l.sort()
return the_input.new_completion(l, n, quotify=True)
return Completion(the_input.new_completion, l, n, quotify=True)
def mood(self, the_input):
"""Completion for /mood"""
n = the_input.get_argument_position(quoted=True)
if n == 1:
return the_input.new_completion(sorted(pep.MOODS.keys()), 1, quotify=True)
return Completion(the_input.new_completion, sorted(pep.MOODS.keys()), 1, quotify=True)
def last_activity(self, the_input):
......@@ -281,7 +279,7 @@ class CompletionCore:
if n >= 2:
return False
comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
return the_input.new_completion(sorted(comp), 1, '', quotify=False)
return Completion(the_input.new_completion, sorted(comp), 1, '', quotify=False)
def server_cycle(self, the_input):
......@@ -290,7 +288,7 @@ class CompletionCore:
for tab in self.core.get_tabs(tabs.MucTab):
serv = safeJID(tab.name).server
serv_list.add(serv)
return the_input.new_completion(sorted(serv_list), 1, ' ')
return Completion(the_input.new_completion, sorted(serv_list), 1, ' ')
def set(self, the_input):
......@@ -303,7 +301,7 @@ class CompletionCore:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if plugin_name not in self.core.plugin_manager.plugins:
return the_input.new_completion([], n, quotify=True)
return Completion(the_input.new_completion, [], n, quotify=True)
plugin = self.core.plugin_manager.plugins[plugin_name]
end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()]
else:
......@@ -315,7 +313,7 @@ class CompletionCore:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if plugin_name not in self.core.plugin_manager.plugins:
return the_input.new_completion([''], n, quotify=True)
return Completion(the_input.new_completion, [''], n, quotify=True)
plugin = self.core.plugin_manager.plugins[plugin_name]
end_list = set(plugin.config.options(section or plugin_name))
if plugin.config.default:
......@@ -334,7 +332,7 @@ class CompletionCore:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if plugin_name not in self.core.plugin_manager.plugins:
return the_input.new_completion([''], n, quotify=True)
return Completion(the_input.new_completion, [''], n, quotify=True)
plugin = self.core.plugin_manager.plugins[plugin_name]
end_list = [str(plugin.config.get(args[2], '', section or plugin_name)), '']
else:
......@@ -344,7 +342,7 @@ class CompletionCore:
end_list = [str(config.get(args[2], '', args[1])), '']
else:
return False
return the_input.new_completion(end_list, n, quotify=True)
return Completion(the_input.new_completion, end_list, n, quotify=True)
def set_default(self, the_input):
......@@ -355,13 +353,13 @@ class CompletionCore:
if n >= len(args):
args.append('')
if n == 1 or (n == 2 and config.has_section(args[1])):
return self.set(the_input)
return Completion(self.set, the_input)
return False
def toggle(self, the_input):
"Completion for /toggle"
return the_input.new_completion(config.options('Poezio'), 1, quotify=False)
return Completion(the_input.new_completion, config.options('Poezio'), 1, quotify=False)
def bookmark_local(self, the_input):
......@@ -387,8 +385,8 @@ class CompletionCore:
if nick not in nicks:
nicks.append(nick)
jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
return the_input.new_completion(jids_list, 1, quotify=True)
return Completion(the_input.new_completion, jids_list, 1, quotify=True)
muc_list = [tab.name for tab in self.core.get_tabs(tabs.MucTab)]
muc_list.append('*')
return the_input.new_completion(muc_list, 1, quotify=True)
return Completion(the_input.new_completion, muc_list, 1, quotify=True)
......@@ -3,7 +3,7 @@ Module defining structures useful to the core class and related methods
"""
__all__ = ['ERROR_AND_STATUS_CODES', 'DEPRECATED_ERRORS', 'POSSIBLE_SHOW',
'Status', 'Command']
'Status', 'Command', 'Completion']
# http://xmpp.org/extensions/xep-0045.html#errorstatus
ERROR_AND_STATUS_CODES = {
......@@ -62,3 +62,17 @@ class Command:
self.comp = comp
self.short_desc = short_desc
self.usage = usage
class Completion:
"""
A completion result essentially currying the input completion call.
"""
__slots__ = ['func', 'args', 'kwargs', 'comp_list']
def __init__(self, func, comp_list, *args, **kwargs):
self.func = func
self.comp_list = comp_list
self.args = args
self.kwargs = kwargs
def run(self):
return self.func(self.comp_list, *self.args, **self.kwargs)
......@@ -10,7 +10,7 @@ from os import path
import logging
from poezio import tabs
from poezio.core.structs import Command
from poezio.core.structs import Command, Completion
from poezio.plugin import PluginAPI
from poezio.config import config
......@@ -284,7 +284,7 @@ class PluginManager(object):
and name != '__init__.py' and not name.startswith('.')]
plugins_files.sort()
position = the_input.get_argument_position(quoted=False)
return the_input.new_completion(plugins_files, position, '',
return Completion(the_input.new_completion, plugins_files, position, '',
quotify=False)
def completion_unload(self, the_input):
......@@ -292,7 +292,7 @@ class PluginManager(object):
completion function that completes the name of loaded plugins
"""
position = the_input.get_argument_position(quoted=False)
return the_input.new_completion(sorted(self.plugins.keys()), position,
return Completion(the_input.new_completion, sorted(self.plugins.keys()), position,
'', quotify=False)
def on_plugins_dir_change(self, new_value):
......
......@@ -20,7 +20,7 @@ import string
import time
from xml.etree import cElementTree as ET
from poezio.core.structs import Command
from poezio.core.structs import Command, Completion
from poezio import timed_events
from poezio import windows
from poezio import xhtml
......@@ -230,7 +230,10 @@ class Tab(object):
if command.comp is None:
return False # There's no completion function
else:
return command.comp(the_input)
comp = command.comp(the_input)
if comp:
return comp.run()
return comp
return False
def execute_command(self, provided_text):
......@@ -633,7 +636,7 @@ class ChatTab(Tab):
def completion_correct(self, the_input):
if self.last_sent_message and the_input.get_argument_position() == 1:
return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False)
return Completion(the_input.auto_completion, [self.last_sent_message['body']], '', quotify=False)
@property
def inactive(self):
......
......@@ -32,6 +32,7 @@ from poezio.logger import logger
from poezio.roster import roster
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
from poezio.core.structs import Completion
SHOW_NAME = {
......@@ -243,7 +244,7 @@ class MucTab(ChatTab):
comp.sort()
userlist.extend(comp)
return the_input.auto_completion(userlist, quotify=False)
return Completion(the_input.auto_completion, userlist, quotify=False)
def completion_info(self, the_input):
"""Completion for /info"""
......@@ -251,7 +252,7 @@ class MucTab(ChatTab):
userlist = []
for user in sorted(self.users, key=compare_users, reverse=True):
userlist.append(user.nick)