change completion API
* InputHelper returns full words, not just suffixes. I liked the fact that suffixes made it impossible to violate the "all matches will start with your prefix" invariant, but in practice it was fiddly to work with. * add ih.when_wordlist_is_available(), so the frontend can block (after claiming the nameplate) until it can return a complete wordlist to readline. This helps the user experience, because readline wasn't really built to work with completions that change over time * make the Wordlist responsible for appending hyphens to all non-final word completions. InputHelper remains responsible for hyphens on nameplates. This makes the frontend simpler, but I may change it again in the future if it helps non-readline GUI frontends. * CodeInputter: after claiming, wait for the wordlist rather than returning an empty list * PGPWordList: change to match This has the unfortunate side-effect that e.g. typing "3-yucatan-tu TAB" shows you completions that include the entire phrase: "3-yucatan-tumor 3-yucatan-tunnel", rather than only mentioning the final word. I'd like to fix this eventually.
This commit is contained in:
parent
04926d0be8
commit
d331c51c03
60
docs/api.md
60
docs/api.md
|
@ -183,41 +183,45 @@ The code-entry Helper object has the following API:
|
||||||
`get_nameplate_completions()` after the response will use the new list.
|
`get_nameplate_completions()` after the response will use the new list.
|
||||||
Calling this after `h.choose_nameplate` will raise
|
Calling this after `h.choose_nameplate` will raise
|
||||||
`AlreadyChoseNameplateError`.
|
`AlreadyChoseNameplateError`.
|
||||||
* `completions = h.get_nameplate_completions(prefix)`: returns
|
* `matches = h.get_nameplate_completions(prefix)`: returns (synchronously) a
|
||||||
(synchronously) a set of suffixes for the given nameplate prefix. For
|
set of completions for the given nameplate prefix, along with the hyphen
|
||||||
example, if the server reports nameplates 1, 12, 13, 24, and 170 are in
|
that always follows the nameplate (and separates the nameplate from the
|
||||||
use, `get_nameplate_completions("1")` will return `{"", "2", "3", "70"}`.
|
rest of the code). For example, if the server reports nameplates 1, 12, 13,
|
||||||
You may want to sort these before displaying them to the user. Raises
|
24, and 170 are in use, `get_nameplate_completions("1")` will return
|
||||||
`AlreadyChoseNameplateError` if called after `h.choose_nameplate`.
|
`{"1-", "12-", "13-", "170-"}`. You may want to sort these before
|
||||||
|
displaying them to the user. Raises `AlreadyChoseNameplateError` if called
|
||||||
|
after `h.choose_nameplate`.
|
||||||
* `h.choose_nameplate(nameplate)`: accepts a string with the chosen
|
* `h.choose_nameplate(nameplate)`: accepts a string with the chosen
|
||||||
nameplate. May only be called once, after which
|
nameplate. May only be called once, after which
|
||||||
`AlreadyChoseNameplateError` is raised. (in this future, this might
|
`AlreadyChoseNameplateError` is raised. (in this future, this might
|
||||||
return a Deferred that fires (with None) when the nameplate's wordlist is
|
return a Deferred that fires (with None) when the nameplate's wordlist is
|
||||||
known (which happens after the nameplate is claimed, requiring a roundtrip
|
known (which happens after the nameplate is claimed, requiring a roundtrip
|
||||||
to the server)).
|
to the server)).
|
||||||
* `completions = h.get_word_completions(prefix)`: return (synchronously) a
|
* `d = h.when_wordlist_is_available()`: return a Deferred that fires (with
|
||||||
set of suffixes for the given words prefix. The possible completions depend
|
None) when the wordlist is known. This can be used to block a readline
|
||||||
upon the wordlist in use for the previously-claimed nameplate, so calling
|
frontend which has just called `h.choose_nameplate()` until the resulting
|
||||||
this before `choose_nameplate` will raise `MustChooseNameplateFirstError`.
|
wordlist is known, which can improve the tab-completion behavior.
|
||||||
|
* `matches = h.get_word_completions(prefix)`: return (synchronously) a set of
|
||||||
|
completions for the given words prefix. This will include a trailing hyphen
|
||||||
|
if more words are expected. The possible completions depend upon the
|
||||||
|
wordlist in use for the previously-claimed nameplate, so calling this
|
||||||
|
before `choose_nameplate` will raise `MustChooseNameplateFirstError`.
|
||||||
Calling this after `h.choose_words()` will raise `AlreadyChoseWordsError`.
|
Calling this after `h.choose_words()` will raise `AlreadyChoseWordsError`.
|
||||||
Given a prefix like "su", this returns a set of strings which are
|
Given a prefix like "su", this returns a set of strings which are potential
|
||||||
appropriate to append to the prefix (e.g. `{"pportive", "rrender",
|
matches (e.g. `{"supportive-", "surrender-", "suspicious-"}`. The prefix
|
||||||
"spicious"}`, for expansion into "supportive", "surrender", and
|
should not include the nameplate, but *should* include whatever words and
|
||||||
"suspicious". The prefix should not include the nameplate, but *should*
|
hyphens have been typed so far (the default wordlist uses alternate lists,
|
||||||
include whatever words and hyphens have been typed so far (the default
|
where even numbered words have three syllables, and odd numbered words have
|
||||||
wordlist uses alternate lists, where even numbered words have three
|
two, so the completions depend upon how many words are present, not just
|
||||||
syllables, and odd numbered words have two, so the completions depend upon
|
the partial last word). E.g. `get_word_completions("pr")` will return
|
||||||
how many words are present, not just the partial last word). E.g.
|
`{"processor-", "provincial-", "proximate-"}`, while
|
||||||
`get_word_completions("pr")` will return `{"ocessor", "ovincial",
|
`get_word_completions("opulent-pr")` will return `{"opulent-preclude",
|
||||||
"oximate"}`, while `get_word_completions("opulent-pr")` will return
|
"opulent-prefer", "opulent-preshrunk", "opulent-printer",
|
||||||
`{"eclude", "efer", "eshrunk", "inter", "owler"}`. If the wordlist is not
|
"opulent-prowler"}` (note the lack of a trailing hyphen, because the
|
||||||
yet known, this returns an empty set. It will include an empty string in
|
wordlist is expecting a code of length two). If the wordlist is not yet
|
||||||
the returned set if the prefix is complete (the last word is an exact match
|
known, this returns an empty set. All return values will
|
||||||
for something in the completion list), but will include additional strings
|
`.startwith(prefix)`. The frontend is responsible for sorting the results
|
||||||
if the completion list includes extensions of the last word. The
|
before display.
|
||||||
completions will never include a hyphen: the UI frontend must supply these
|
|
||||||
if desired. The frontend is also responsible for sorting the results before
|
|
||||||
display.
|
|
||||||
* `h.choose_words(words)`: call this when the user is finished typing in the
|
* `h.choose_words(words)`: call this when the user is finished typing in the
|
||||||
code. It does not return anything, but will cause the Wormhole's
|
code. It does not return anything, but will cause the Wormhole's
|
||||||
`w.when_code()` (or corresponding delegate) to fire, and triggers the
|
`w.when_code()` (or corresponding delegate) to fire, and triggers the
|
||||||
|
|
|
@ -2,6 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
from attr import attrs, attrib
|
from attr import attrs, attrib
|
||||||
from attr.validators import provides
|
from attr.validators import provides
|
||||||
|
from twisted.internet import defer
|
||||||
from automat import MethodicalMachine
|
from automat import MethodicalMachine
|
||||||
from . import _interfaces, errors
|
from . import _interfaces, errors
|
||||||
|
|
||||||
|
@ -19,11 +20,19 @@ class Input(object):
|
||||||
self._all_nameplates = set()
|
self._all_nameplates = set()
|
||||||
self._nameplate = None
|
self._nameplate = None
|
||||||
self._wordlist = None
|
self._wordlist = None
|
||||||
|
self._wordlist_waiters = []
|
||||||
|
|
||||||
def wire(self, code, lister):
|
def wire(self, code, lister):
|
||||||
self._C = _interfaces.ICode(code)
|
self._C = _interfaces.ICode(code)
|
||||||
self._L = _interfaces.ILister(lister)
|
self._L = _interfaces.ILister(lister)
|
||||||
|
|
||||||
|
def when_wordlist_is_available(self):
|
||||||
|
if self._wordlist:
|
||||||
|
return defer.succeed(None)
|
||||||
|
d = defer.Deferred()
|
||||||
|
self._wordlist_waiters.append(d)
|
||||||
|
return d
|
||||||
|
|
||||||
@m.state(initial=True)
|
@m.state(initial=True)
|
||||||
def S0_idle(self): pass # pragma: no cover
|
def S0_idle(self): pass # pragma: no cover
|
||||||
@m.state()
|
@m.state()
|
||||||
|
@ -72,11 +81,12 @@ class Input(object):
|
||||||
self._all_nameplates = all_nameplates
|
self._all_nameplates = all_nameplates
|
||||||
@m.output()
|
@m.output()
|
||||||
def _get_nameplate_completions(self, prefix):
|
def _get_nameplate_completions(self, prefix):
|
||||||
lp = len(prefix)
|
|
||||||
completions = set()
|
completions = set()
|
||||||
for nameplate in self._all_nameplates:
|
for nameplate in self._all_nameplates:
|
||||||
if nameplate.startswith(prefix):
|
if nameplate.startswith(prefix):
|
||||||
completions.add(nameplate[lp:])
|
# TODO: it's a little weird that Input is responsible for the
|
||||||
|
# hyphen on nameplates, but WordList owns it for words
|
||||||
|
completions.add(nameplate+"-")
|
||||||
return completions
|
return completions
|
||||||
@m.output()
|
@m.output()
|
||||||
def record_all_nameplates(self, nameplate):
|
def record_all_nameplates(self, nameplate):
|
||||||
|
@ -87,6 +97,11 @@ class Input(object):
|
||||||
from ._rlcompleter import debug
|
from ._rlcompleter import debug
|
||||||
debug(" -record_wordlist")
|
debug(" -record_wordlist")
|
||||||
self._wordlist = wordlist
|
self._wordlist = wordlist
|
||||||
|
@m.output()
|
||||||
|
def notify_wordlist_waiters(self, wordlist):
|
||||||
|
while self._wordlist_waiters:
|
||||||
|
d = self._wordlist_waiters.pop()
|
||||||
|
d.callback(None)
|
||||||
|
|
||||||
@m.output()
|
@m.output()
|
||||||
def no_word_completions(self, prefix):
|
def no_word_completions(self, prefix):
|
||||||
|
@ -128,7 +143,8 @@ class Input(object):
|
||||||
# wormholes that don't use input_code (i.e. they use allocate_code or
|
# wormholes that don't use input_code (i.e. they use allocate_code or
|
||||||
# generate_code) will never start() us, but Nameplate will give us a
|
# generate_code) will never start() us, but Nameplate will give us a
|
||||||
# wordlist anyways (as soon as the nameplate is claimed), so handle it.
|
# wordlist anyways (as soon as the nameplate is claimed), so handle it.
|
||||||
S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist])
|
S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist,
|
||||||
|
notify_wordlist_waiters])
|
||||||
S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate,
|
S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate,
|
||||||
outputs=[record_nameplates])
|
outputs=[record_nameplates])
|
||||||
# but wormholes that *do* use input_code should not get got_wordlist
|
# but wormholes that *do* use input_code should not get got_wordlist
|
||||||
|
@ -152,7 +168,8 @@ class Input(object):
|
||||||
enter=S2_typing_code_no_wordlist, outputs=[])
|
enter=S2_typing_code_no_wordlist, outputs=[])
|
||||||
S2_typing_code_no_wordlist.upon(got_wordlist,
|
S2_typing_code_no_wordlist.upon(got_wordlist,
|
||||||
enter=S3_typing_code_yes_wordlist,
|
enter=S3_typing_code_yes_wordlist,
|
||||||
outputs=[record_wordlist])
|
outputs=[record_wordlist,
|
||||||
|
notify_wordlist_waiters])
|
||||||
S2_typing_code_no_wordlist.upon(refresh_nameplates,
|
S2_typing_code_no_wordlist.upon(refresh_nameplates,
|
||||||
enter=S2_typing_code_no_wordlist,
|
enter=S2_typing_code_no_wordlist,
|
||||||
outputs=[raise_already_chose_nameplate1])
|
outputs=[raise_already_chose_nameplate1])
|
||||||
|
@ -215,6 +232,8 @@ class Helper(object):
|
||||||
return self._input.get_nameplate_completions(prefix)
|
return self._input.get_nameplate_completions(prefix)
|
||||||
def choose_nameplate(self, nameplate):
|
def choose_nameplate(self, nameplate):
|
||||||
self._input.choose_nameplate(nameplate)
|
self._input.choose_nameplate(nameplate)
|
||||||
|
def when_wordlist_is_available(self):
|
||||||
|
return self._input.when_wordlist_is_available()
|
||||||
def get_word_completions(self, prefix):
|
def get_word_completions(self, prefix):
|
||||||
return self._input.get_word_completions(prefix)
|
return self._input.get_word_completions(prefix)
|
||||||
def choose_words(self, words):
|
def choose_words(self, words):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from six.moves import input
|
||||||
from attr import attrs, attrib
|
from attr import attrs, attrib
|
||||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||||
from twisted.internet.threads import deferToThread, blockingCallFromThread
|
from twisted.internet.threads import deferToThread, blockingCallFromThread
|
||||||
|
from .errors import KeyFormatError, AlreadyInputNameplateError
|
||||||
|
|
||||||
errf = None
|
errf = None
|
||||||
if 0:
|
if 0:
|
||||||
|
@ -68,40 +69,61 @@ class CodeInputter(object):
|
||||||
nameplate = text # partial
|
nameplate = text # partial
|
||||||
|
|
||||||
# 'text' is one of these categories:
|
# 'text' is one of these categories:
|
||||||
# "": complete on nameplates
|
# "" or "12": complete on nameplates (all that match, maybe just one)
|
||||||
# "12" (multiple matches): complete on nameplates
|
|
||||||
# "123" (single match): return "123-" (no commit, no refresh)
|
# "123-": if we haven't already committed to a nameplate, commit and
|
||||||
# nope: need to let readline add letters
|
# wait for the wordlist. Then (either way) return the whole wordlist.
|
||||||
# so: return "123-"
|
|
||||||
# "123-": commit to nameplate (if not already), complete on words
|
# "123-supp": if we haven't already committed to a nameplate, commit
|
||||||
|
# and wait for the wordlist. Then (either way) return all current
|
||||||
|
# matches.
|
||||||
|
|
||||||
if self._committed_nameplate:
|
if self._committed_nameplate:
|
||||||
if not got_nameplate or nameplate != self._committed_nameplate:
|
if not got_nameplate or nameplate != self._committed_nameplate:
|
||||||
# they deleted past the committment point: we can't use
|
# they deleted past the committment point: we can't use
|
||||||
# this. For now, bail, but in the future let's find a
|
# this. For now, bail, but in the future let's find a
|
||||||
# gentler way to encourage them to not do that.
|
# gentler way to encourage them to not do that.
|
||||||
raise ValueError("nameplate (NN-) already entered, cannot go back")
|
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate)
|
||||||
if not got_nameplate:
|
if not got_nameplate:
|
||||||
# we're completing on nameplates: "" or "12" or "123"
|
# we're completing on nameplates: "" or "12" or "123"
|
||||||
self.bcft(ih.refresh_nameplates) # results arrive later
|
self.bcft(ih.refresh_nameplates) # results arrive later
|
||||||
debug(" getting nameplates")
|
debug(" getting nameplates")
|
||||||
completions = self.bcft(ih.get_nameplate_completions, nameplate)
|
completions = self.bcft(ih.get_nameplate_completions, nameplate)
|
||||||
else: # "123-"
|
else: # "123-" or "123-supp"
|
||||||
# time to commit to this nameplate, if they haven't already
|
# time to commit to this nameplate, if they haven't already
|
||||||
if not self._committed_nameplate:
|
if not self._committed_nameplate:
|
||||||
debug(" choose_nameplate(%s)" % nameplate)
|
debug(" choose_nameplate(%s)" % nameplate)
|
||||||
self.bcft(ih.choose_nameplate, nameplate)
|
self.bcft(ih.choose_nameplate, nameplate)
|
||||||
self._committed_nameplate = nameplate
|
self._committed_nameplate = nameplate
|
||||||
|
|
||||||
|
# Now we want to wait for the wordlist to be available. If
|
||||||
|
# the user just typed "12-supp TAB", we'll claim "12" but
|
||||||
|
# will need a server roundtrip to discover that "supportive"
|
||||||
|
# is the only match. If we don't block, we'd return an empty
|
||||||
|
# wordlist to readline (which will beep and show no
|
||||||
|
# completions). *Then* when the user hits TAB again a moment
|
||||||
|
# later (after the wordlist has arrived, but the user hasn't
|
||||||
|
# modified the input line since the previous empty response),
|
||||||
|
# readline would show one match but not complete anything.
|
||||||
|
|
||||||
|
# In general we want to avoid returning empty lists to
|
||||||
|
# readline. If the user hits TAB when typing in the nameplate
|
||||||
|
# (before the sender has established one, or before we're
|
||||||
|
# heard about it from the server), it can't be helped. But
|
||||||
|
# for the rest of the code, a simple wait-for-wordlist will
|
||||||
|
# improve the user experience.
|
||||||
|
self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM
|
||||||
# and we're completing on words now
|
# and we're completing on words now
|
||||||
debug(" getting words")
|
debug(" getting words")
|
||||||
completions = self.bcft(ih.get_word_completions, words)
|
completions = [nameplate+"-"+c
|
||||||
|
for c in self.bcft(ih.get_word_completions, words)]
|
||||||
|
|
||||||
# rlcompleter wants full strings
|
# rlcompleter wants full strings
|
||||||
return sorted([text+c for c in completions])
|
return sorted(completions)
|
||||||
|
|
||||||
def finish(self, text):
|
def finish(self, text):
|
||||||
if "-" not in text:
|
if "-" not in text:
|
||||||
raise ValueError("incomplete wormhole code")
|
raise KeyFormatError("incomplete wormhole code")
|
||||||
nameplate, words = text.split("-", 1)
|
nameplate, words = text.split("-", 1)
|
||||||
|
|
||||||
if self._committed_nameplate:
|
if self._committed_nameplate:
|
||||||
|
@ -109,7 +131,7 @@ class CodeInputter(object):
|
||||||
# they deleted past the committment point: we can't use
|
# they deleted past the committment point: we can't use
|
||||||
# this. For now, bail, but in the future let's find a
|
# this. For now, bail, but in the future let's find a
|
||||||
# gentler way to encourage them to not do that.
|
# gentler way to encourage them to not do that.
|
||||||
raise ValueError("nameplate (NN-) already entered, cannot go back")
|
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate)
|
||||||
else:
|
else:
|
||||||
debug(" choose_nameplate(%s)" % nameplate)
|
debug(" choose_nameplate(%s)" % nameplate)
|
||||||
self._input_helper.choose_nameplate(nameplate)
|
self._input_helper.choose_nameplate(nameplate)
|
||||||
|
|
|
@ -172,7 +172,7 @@ class PGPWordList(object):
|
||||||
completions = set()
|
completions = set()
|
||||||
for word in words:
|
for word in words:
|
||||||
if word.startswith(last_partial_word):
|
if word.startswith(last_partial_word):
|
||||||
suffix = word[lp:]
|
suffix = prefix[:-lp] + word
|
||||||
# append a hyphen if we expect more words
|
# append a hyphen if we expect more words
|
||||||
if count+1 < num_words:
|
if count+1 < num_words:
|
||||||
suffix += "-"
|
suffix += "-"
|
||||||
|
|
|
@ -31,9 +31,10 @@ class WrongPasswordError(WormholeError):
|
||||||
|
|
||||||
class KeyFormatError(WormholeError):
|
class KeyFormatError(WormholeError):
|
||||||
"""
|
"""
|
||||||
The key you entered contains spaces. Magic-wormhole expects keys to be
|
The key you entered contains spaces or was missing a dash. Magic-wormhole
|
||||||
separated by dashes. Please reenter the key you were given separating the
|
expects the numerical nameplate and the code words to be separated by
|
||||||
words with dashes.
|
dashes. Please reenter the key you were given separating the words with
|
||||||
|
dashes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class ReflectionAttack(WormholeError):
|
class ReflectionAttack(WormholeError):
|
||||||
|
@ -67,6 +68,9 @@ class AlreadyChoseNameplateError(WormholeError):
|
||||||
class AlreadyChoseWordsError(WormholeError):
|
class AlreadyChoseWordsError(WormholeError):
|
||||||
"""The InputHelper was asked to do get_word_completions() after
|
"""The InputHelper was asked to do get_word_completions() after
|
||||||
choose_words() was called, or choose_words() was called a second time."""
|
choose_words() was called, or choose_words() was called a second time."""
|
||||||
|
class AlreadyInputNameplateError(WormholeError):
|
||||||
|
"""The CodeInputter was asked to do completion on a nameplate, when we
|
||||||
|
had already committed to a different one."""
|
||||||
class WormholeClosed(Exception):
|
class WormholeClosed(Exception):
|
||||||
"""Deferred-returning API calls errback with WormholeClosed if the
|
"""Deferred-returning API calls errback with WormholeClosed if the
|
||||||
wormhole was already closed, or if it closes before a real result can be
|
wormhole was already closed, or if it closes before a real result can be
|
||||||
|
|
|
@ -336,19 +336,22 @@ class Input(unittest.TestCase):
|
||||||
self.assertIsInstance(helper, _input.Helper)
|
self.assertIsInstance(helper, _input.Helper)
|
||||||
self.assertEqual(events, [("l.refresh",)])
|
self.assertEqual(events, [("l.refresh",)])
|
||||||
events[:] = []
|
events[:] = []
|
||||||
|
d = helper.when_wordlist_is_available()
|
||||||
|
self.assertNoResult(d)
|
||||||
helper.refresh_nameplates()
|
helper.refresh_nameplates()
|
||||||
self.assertEqual(events, [("l.refresh",)])
|
self.assertEqual(events, [("l.refresh",)])
|
||||||
events[:] = []
|
events[:] = []
|
||||||
with self.assertRaises(errors.MustChooseNameplateFirstError):
|
with self.assertRaises(errors.MustChooseNameplateFirstError):
|
||||||
helper.get_word_completions("prefix")
|
helper.get_word_completions("prefix")
|
||||||
i.got_nameplates({"1", "12", "34", "35", "367"})
|
i.got_nameplates({"1", "12", "34", "35", "367"})
|
||||||
|
self.assertNoResult(d)
|
||||||
self.assertEqual(helper.get_nameplate_completions(""),
|
self.assertEqual(helper.get_nameplate_completions(""),
|
||||||
{"1", "12", "34", "35", "367"})
|
{"1-", "12-", "34-", "35-", "367-"})
|
||||||
self.assertEqual(helper.get_nameplate_completions("1"),
|
self.assertEqual(helper.get_nameplate_completions("1"),
|
||||||
{"", "2"})
|
{"1-", "12-"})
|
||||||
self.assertEqual(helper.get_nameplate_completions("2"), set())
|
self.assertEqual(helper.get_nameplate_completions("2"), set())
|
||||||
self.assertEqual(helper.get_nameplate_completions("3"),
|
self.assertEqual(helper.get_nameplate_completions("3"),
|
||||||
{"4", "5", "67"})
|
{"34-", "35-", "367-"})
|
||||||
helper.choose_nameplate("34")
|
helper.choose_nameplate("34")
|
||||||
with self.assertRaises(errors.AlreadyChoseNameplateError):
|
with self.assertRaises(errors.AlreadyChoseNameplateError):
|
||||||
helper.refresh_nameplates()
|
helper.refresh_nameplates()
|
||||||
|
@ -357,10 +360,16 @@ class Input(unittest.TestCase):
|
||||||
self.assertEqual(events, [("c.got_nameplate", "34")])
|
self.assertEqual(events, [("c.got_nameplate", "34")])
|
||||||
events[:] = []
|
events[:] = []
|
||||||
# no wordlist yet
|
# no wordlist yet
|
||||||
|
self.assertNoResult(d)
|
||||||
self.assertEqual(helper.get_word_completions(""), set())
|
self.assertEqual(helper.get_word_completions(""), set())
|
||||||
wl = FakeWordList()
|
wl = FakeWordList()
|
||||||
i.got_wordlist(wl)
|
i.got_wordlist(wl)
|
||||||
wl._completions = {"bc", "bcd", "e"}
|
self.assertEqual(self.successResultOf(d), None)
|
||||||
|
# a new Deferred should fire right away
|
||||||
|
d = helper.when_wordlist_is_available()
|
||||||
|
self.assertEqual(self.successResultOf(d), None)
|
||||||
|
|
||||||
|
wl._completions = {"abc-", "abcd-", "ae-"}
|
||||||
self.assertEqual(helper.get_word_completions("a"), wl._completions)
|
self.assertEqual(helper.get_word_completions("a"), wl._completions)
|
||||||
self.assertEqual(wl._get_completions_prefix, "a")
|
self.assertEqual(wl._get_completions_prefix, "a")
|
||||||
with self.assertRaises(errors.AlreadyChoseNameplateError):
|
with self.assertRaises(errors.AlreadyChoseNameplateError):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from twisted.internet.threads import deferToThread
|
||||||
from .._rlcompleter import (input_with_completion,
|
from .._rlcompleter import (input_with_completion,
|
||||||
_input_code_with_completion,
|
_input_code_with_completion,
|
||||||
CodeInputter, warn_readline)
|
CodeInputter, warn_readline)
|
||||||
|
from ..errors import KeyFormatError, AlreadyInputNameplateError
|
||||||
APPID = "appid"
|
APPID = "appid"
|
||||||
|
|
||||||
class Input(unittest.TestCase):
|
class Input(unittest.TestCase):
|
||||||
|
@ -231,7 +232,7 @@ class Completion(unittest.TestCase):
|
||||||
c = CodeInputter(helper, reactor)
|
c = CodeInputter(helper, reactor)
|
||||||
cabc = c._commit_and_build_completions
|
cabc = c._commit_and_build_completions
|
||||||
|
|
||||||
# in this test, we pretend that nameplates 1 and 12 are active.
|
# in this test, we pretend that nameplates 1,12,34 are active.
|
||||||
|
|
||||||
# 43 TAB -> nothing (and refresh_nameplates)
|
# 43 TAB -> nothing (and refresh_nameplates)
|
||||||
gnc.configure_mock(return_value=[])
|
gnc.configure_mock(return_value=[])
|
||||||
|
@ -243,50 +244,64 @@ class Completion(unittest.TestCase):
|
||||||
rn.reset_mock()
|
rn.reset_mock()
|
||||||
gnc.reset_mock()
|
gnc.reset_mock()
|
||||||
|
|
||||||
# 1 TAB -> 1, 12 (and refresh_nameplates)
|
# 1 TAB -> 1-, 12- (and refresh_nameplates)
|
||||||
gnc.configure_mock(return_value=["", "2"])
|
gnc.configure_mock(return_value=["1-", "12-"])
|
||||||
matches = yield deferToThread(cabc, "1")
|
matches = yield deferToThread(cabc, "1")
|
||||||
self.assertEqual(matches, ["1", "12"])
|
self.assertEqual(matches, ["1-", "12-"])
|
||||||
self.assertEqual(rn.mock_calls, [mock.call()])
|
self.assertEqual(rn.mock_calls, [mock.call()])
|
||||||
self.assertEqual(gnc.mock_calls, [mock.call("1")])
|
self.assertEqual(gnc.mock_calls, [mock.call("1")])
|
||||||
self.assertEqual(cn.mock_calls, [])
|
self.assertEqual(cn.mock_calls, [])
|
||||||
rn.reset_mock()
|
rn.reset_mock()
|
||||||
gnc.reset_mock()
|
gnc.reset_mock()
|
||||||
|
|
||||||
# current: 12 TAB -> (and refresh_nameplates)
|
# 12 TAB -> 12- (and refresh_nameplates)
|
||||||
# want: 12 TAB -> 12- (and choose_nameplate)
|
# I wouldn't mind if it didn't refresh the nameplates here, but meh
|
||||||
gnc.configure_mock(return_value=[""])
|
gnc.configure_mock(return_value=["12-"])
|
||||||
matches = yield deferToThread(cabc, "12")
|
matches = yield deferToThread(cabc, "12")
|
||||||
self.assertEqual(matches, ["12"]) # 12-
|
self.assertEqual(matches, ["12-"])
|
||||||
self.assertEqual(rn.mock_calls, [mock.call()])
|
self.assertEqual(rn.mock_calls, [mock.call()])
|
||||||
self.assertEqual(gnc.mock_calls, [mock.call("12")])
|
self.assertEqual(gnc.mock_calls, [mock.call("12")])
|
||||||
self.assertEqual(cn.mock_calls, []) # 12
|
self.assertEqual(cn.mock_calls, [])
|
||||||
rn.reset_mock()
|
rn.reset_mock()
|
||||||
gnc.reset_mock()
|
gnc.reset_mock()
|
||||||
|
|
||||||
# current: 12-a TAB -> and ark aaah!zombies!! (and choose nameplate)
|
# 12- TAB -> 12- {all words} (claim, no refresh)
|
||||||
gnc.configure_mock(side_effect=ValueError)
|
gnc.configure_mock(return_value=["12-"])
|
||||||
gwc.configure_mock(return_value=["nd", "rk", "aah!zombies!!"])
|
gwc.configure_mock(return_value=["and-", "ark-", "aaah!zombies!!-"])
|
||||||
matches = yield deferToThread(cabc, "12-a")
|
matches = yield deferToThread(cabc, "12-")
|
||||||
# matches are always sorted
|
self.assertEqual(matches, ["12-aaah!zombies!!-", "12-and-", "12-ark-"])
|
||||||
self.assertEqual(matches, ["12-aaah!zombies!!", "12-and", "12-ark"])
|
|
||||||
self.assertEqual(rn.mock_calls, [])
|
self.assertEqual(rn.mock_calls, [])
|
||||||
self.assertEqual(gnc.mock_calls, [])
|
self.assertEqual(gnc.mock_calls, [])
|
||||||
self.assertEqual(cn.mock_calls, [mock.call("12")])
|
self.assertEqual(cn.mock_calls, [mock.call("12")])
|
||||||
self.assertEqual(gwc.mock_calls, [mock.call("a")])
|
self.assertEqual(gwc.mock_calls, [mock.call("")])
|
||||||
cn.reset_mock()
|
cn.reset_mock()
|
||||||
gwc.reset_mock()
|
gwc.reset_mock()
|
||||||
|
|
||||||
# current: 12-and-b TAB -> bat bet but
|
# TODO: another path with "3 TAB" then "34-an TAB", so the claim
|
||||||
|
# happens in the second call (and it waits for the wordlist)
|
||||||
|
|
||||||
|
# 12-a TAB -> 12-and- 12-ark- 12-aaah!zombies!!-
|
||||||
gnc.configure_mock(side_effect=ValueError)
|
gnc.configure_mock(side_effect=ValueError)
|
||||||
gwc.configure_mock(return_value=["at", "et", "ut"])
|
gwc.configure_mock(return_value=["and-", "ark-", "aaah!zombies!!-"])
|
||||||
|
matches = yield deferToThread(cabc, "12-a")
|
||||||
|
# matches are always sorted
|
||||||
|
self.assertEqual(matches, ["12-aaah!zombies!!-", "12-and-", "12-ark-"])
|
||||||
|
self.assertEqual(rn.mock_calls, [])
|
||||||
|
self.assertEqual(gnc.mock_calls, [])
|
||||||
|
self.assertEqual(cn.mock_calls, [])
|
||||||
|
self.assertEqual(gwc.mock_calls, [mock.call("a")])
|
||||||
|
gwc.reset_mock()
|
||||||
|
|
||||||
|
# 12-and-b TAB -> 12-and-bat 12-and-bet 12-and-but
|
||||||
|
gnc.configure_mock(side_effect=ValueError)
|
||||||
|
# wordlist knows the code length, so doesn't add hyphens to these
|
||||||
|
gwc.configure_mock(return_value=["and-bat", "and-bet", "and-but"])
|
||||||
matches = yield deferToThread(cabc, "12-and-b")
|
matches = yield deferToThread(cabc, "12-and-b")
|
||||||
self.assertEqual(matches, ["12-and-bat", "12-and-bet", "12-and-but"])
|
self.assertEqual(matches, ["12-and-bat", "12-and-bet", "12-and-but"])
|
||||||
self.assertEqual(rn.mock_calls, [])
|
self.assertEqual(rn.mock_calls, [])
|
||||||
self.assertEqual(gnc.mock_calls, [])
|
self.assertEqual(gnc.mock_calls, [])
|
||||||
self.assertEqual(cn.mock_calls, [])
|
self.assertEqual(cn.mock_calls, [])
|
||||||
self.assertEqual(gwc.mock_calls, [mock.call("and-b")])
|
self.assertEqual(gwc.mock_calls, [mock.call("and-b")])
|
||||||
cn.reset_mock()
|
|
||||||
gwc.reset_mock()
|
gwc.reset_mock()
|
||||||
|
|
||||||
c.finish("12-and-bat")
|
c.finish("12-and-bat")
|
||||||
|
@ -295,7 +310,7 @@ class Completion(unittest.TestCase):
|
||||||
def test_incomplete_code(self):
|
def test_incomplete_code(self):
|
||||||
helper = mock.Mock()
|
helper = mock.Mock()
|
||||||
c = CodeInputter(helper, "reactor")
|
c = CodeInputter(helper, "reactor")
|
||||||
with self.assertRaises(ValueError) as e:
|
with self.assertRaises(KeyFormatError) as e:
|
||||||
c.finish("1")
|
c.finish("1")
|
||||||
self.assertEqual(str(e.exception), "incomplete wormhole code")
|
self.assertEqual(str(e.exception), "incomplete wormhole code")
|
||||||
|
|
||||||
|
@ -303,38 +318,40 @@ class Completion(unittest.TestCase):
|
||||||
def test_rollback_nameplate_during_completion(self):
|
def test_rollback_nameplate_during_completion(self):
|
||||||
helper = mock.Mock()
|
helper = mock.Mock()
|
||||||
gwc = helper.get_word_completions = mock.Mock()
|
gwc = helper.get_word_completions = mock.Mock()
|
||||||
gwc.configure_mock(return_value=["de", "urt"])
|
gwc.configure_mock(return_value=["code", "court"])
|
||||||
c = CodeInputter(helper, reactor)
|
c = CodeInputter(helper, reactor)
|
||||||
cabc = c._commit_and_build_completions
|
cabc = c._commit_and_build_completions
|
||||||
matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
|
matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
|
||||||
self.assertEqual(helper.mock_calls,
|
self.assertEqual(helper.mock_calls,
|
||||||
[mock.call.choose_nameplate("1"),
|
[mock.call.choose_nameplate("1"),
|
||||||
|
mock.call.when_wordlist_is_available(),
|
||||||
mock.call.get_word_completions("co")])
|
mock.call.get_word_completions("co")])
|
||||||
self.assertEqual(matches, ["1-code", "1-court"])
|
self.assertEqual(matches, ["1-code", "1-court"])
|
||||||
helper.reset_mock()
|
helper.reset_mock()
|
||||||
with self.assertRaises(ValueError) as e:
|
with self.assertRaises(AlreadyInputNameplateError) as e:
|
||||||
yield deferToThread(cabc, "2-co")
|
yield deferToThread(cabc, "2-co")
|
||||||
self.assertEqual(str(e.exception),
|
self.assertEqual(str(e.exception),
|
||||||
"nameplate (NN-) already entered, cannot go back")
|
"nameplate (1-) already entered, cannot go back")
|
||||||
self.assertEqual(helper.mock_calls, [])
|
self.assertEqual(helper.mock_calls, [])
|
||||||
|
|
||||||
@inlineCallbacks
|
@inlineCallbacks
|
||||||
def test_rollback_nameplate_during_finish(self):
|
def test_rollback_nameplate_during_finish(self):
|
||||||
helper = mock.Mock()
|
helper = mock.Mock()
|
||||||
gwc = helper.get_word_completions = mock.Mock()
|
gwc = helper.get_word_completions = mock.Mock()
|
||||||
gwc.configure_mock(return_value=["de", "urt"])
|
gwc.configure_mock(return_value=["code", "court"])
|
||||||
c = CodeInputter(helper, reactor)
|
c = CodeInputter(helper, reactor)
|
||||||
cabc = c._commit_and_build_completions
|
cabc = c._commit_and_build_completions
|
||||||
matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
|
matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
|
||||||
self.assertEqual(helper.mock_calls,
|
self.assertEqual(helper.mock_calls,
|
||||||
[mock.call.choose_nameplate("1"),
|
[mock.call.choose_nameplate("1"),
|
||||||
|
mock.call.when_wordlist_is_available(),
|
||||||
mock.call.get_word_completions("co")])
|
mock.call.get_word_completions("co")])
|
||||||
self.assertEqual(matches, ["1-code", "1-court"])
|
self.assertEqual(matches, ["1-code", "1-court"])
|
||||||
helper.reset_mock()
|
helper.reset_mock()
|
||||||
with self.assertRaises(ValueError) as e:
|
with self.assertRaises(AlreadyInputNameplateError) as e:
|
||||||
c.finish("2-code")
|
c.finish("2-code")
|
||||||
self.assertEqual(str(e.exception),
|
self.assertEqual(str(e.exception),
|
||||||
"nameplate (NN-) already entered, cannot go back")
|
"nameplate (1-) already entered, cannot go back")
|
||||||
self.assertEqual(helper.mock_calls, [])
|
self.assertEqual(helper.mock_calls, [])
|
||||||
|
|
||||||
@mock.patch("wormhole._rlcompleter.stderr")
|
@mock.patch("wormhole._rlcompleter.stderr")
|
||||||
|
|
|
@ -7,16 +7,18 @@ class Completions(unittest.TestCase):
|
||||||
def test_completions(self):
|
def test_completions(self):
|
||||||
wl = PGPWordList()
|
wl = PGPWordList()
|
||||||
gc = wl.get_completions
|
gc = wl.get_completions
|
||||||
self.assertEqual(gc("ar", 2), {"mistice-", "ticle-"})
|
self.assertEqual(gc("ar", 2), {"armistice-", "article-"})
|
||||||
self.assertEqual(gc("armis", 2), {"tice-"})
|
self.assertEqual(gc("armis", 2), {"armistice-"})
|
||||||
self.assertEqual(gc("armistice", 2), {"-"})
|
self.assertEqual(gc("armistice", 2), {"armistice-"})
|
||||||
self.assertEqual(gc("armistice-ba", 2),
|
self.assertEqual(gc("armistice-ba", 2),
|
||||||
{"boon", "ckfield", "ckward", "njo"})
|
{"armistice-baboon", "armistice-backfield",
|
||||||
|
"armistice-backward", "armistice-banjo"})
|
||||||
self.assertEqual(gc("armistice-ba", 3),
|
self.assertEqual(gc("armistice-ba", 3),
|
||||||
{"boon-", "ckfield-", "ckward-", "njo-"})
|
{"armistice-baboon-", "armistice-backfield-",
|
||||||
self.assertEqual(gc("armistice-baboon", 2), {""})
|
"armistice-backward-", "armistice-banjo-"})
|
||||||
self.assertEqual(gc("armistice-baboon", 3), {"-"})
|
self.assertEqual(gc("armistice-baboon", 2), {"armistice-baboon"})
|
||||||
self.assertEqual(gc("armistice-baboon", 4), {"-"})
|
self.assertEqual(gc("armistice-baboon", 3), {"armistice-baboon-"})
|
||||||
|
self.assertEqual(gc("armistice-baboon", 4), {"armistice-baboon-"})
|
||||||
|
|
||||||
class Choose(unittest.TestCase):
|
class Choose(unittest.TestCase):
|
||||||
def test_choose_words(self):
|
def test_choose_words(self):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user