command: path completion

This commit is contained in:
Aldo Cortesi 2017-12-15 16:00:36 +13:00
parent ea891b43f8
commit a8ae006f2e
4 changed files with 51 additions and 145 deletions

View File

@ -1,7 +1,10 @@
import abc
import glob
import os
import typing
import urwid
from urwid.text_layout import calc_coords
import typing
import abc
import mitmproxy.master
import mitmproxy.command
@ -34,6 +37,30 @@ class ListCompleter(Completer):
return ret
# Generates the completion options for a specific starting input
def pathOptions(start: str) -> typing.Sequence[str]:
if not start:
start = "./"
path = os.path.expanduser(start)
ret = []
if os.path.isdir(path):
files = glob.glob(os.path.join(path, "*"))
prefix = start
else:
files = glob.glob(path + "*")
prefix = os.path.dirname(start)
prefix = prefix or "./"
for f in files:
display = os.path.normpath(os.path.join(prefix, os.path.basename(f)))
if os.path.isdir(f):
display += "/"
ret.append(display)
if not ret:
ret = [start]
ret.sort()
return ret
CompletionState = typing.NamedTuple(
"CompletionState",
[
@ -93,6 +120,15 @@ class CommandBuffer():
),
parse = parts,
)
elif last.type == mitmproxy.command.Path:
self.completion = CompletionState(
completer = ListCompleter(
"",
pathOptions(parts[1].value)
),
parse = parts,
)
if self.completion:
nxt = self.completion.completer.cycle()
buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt

View File

@ -1,71 +0,0 @@
import glob
import os.path
import urwid
class _PathCompleter:
def __init__(self, _testing=False):
"""
_testing: disables reloading of the lookup table to make testing
possible.
"""
self.lookup, self.offset = None, None
self.final = None
self._testing = _testing
def reset(self):
self.lookup = None
self.offset = -1
def complete(self, txt):
"""
Returns the next completion for txt, or None if there is no
completion.
"""
path = os.path.expanduser(txt)
if not self.lookup:
if not self._testing:
# Lookup is a set of (display value, actual value) tuples.
self.lookup = []
if os.path.isdir(path):
files = glob.glob(os.path.join(path, "*"))
prefix = txt
else:
files = glob.glob(path + "*")
prefix = os.path.dirname(txt)
prefix = prefix or "./"
for f in files:
display = os.path.join(prefix, os.path.basename(f))
if os.path.isdir(f):
display += "/"
self.lookup.append((display, f))
if not self.lookup:
self.final = path
return path
self.lookup.sort()
self.offset = -1
self.lookup.append((txt, txt))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.final = ret[1]
return ret[0]
class PathEdit(urwid.Edit, _PathCompleter):
def __init__(self, prompt, last_path):
urwid.Edit.__init__(self, prompt, last_path)
_PathCompleter.__init__(self)
def keypress(self, size, key):
if key == "tab":
comp = self.complete(self.get_edit_text())
self.set_edit_text(comp)
self.set_edit_pos(len(comp))
else:
self.reset()
return urwid.Edit.keypress(self, size, key)

View File

@ -1,5 +1,18 @@
import os
from mitmproxy.tools.console.commander import commander
from mitmproxy.test import taddons
from mitmproxy.test import tutils
def test_pathOptions():
cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion"))
ret = [x[len(cd):] for x in commander.pathOptions(cd)]
assert ret == ['/aaa', '/aab', '/aac', '/bbb/']
ret = [x[len(cd):] for x in commander.pathOptions(os.path.join(cd, "a"))]
assert ret == ['/aaa', '/aab', '/aac']
class TestListCompleter:

View File

@ -1,72 +0,0 @@
import os
from os.path import normpath
from unittest import mock
from mitmproxy.tools.console import pathedit
from mitmproxy.test import tutils
class TestPathCompleter:
def test_lookup_construction(self):
c = pathedit._PathCompleter()
cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion"))
ca = os.path.join(cd, "a")
assert c.complete(ca).endswith(normpath("/completion/aaa"))
assert c.complete(ca).endswith(normpath("/completion/aab"))
c.reset()
ca = os.path.join(cd, "aaa")
assert c.complete(ca).endswith(normpath("/completion/aaa"))
assert c.complete(ca).endswith(normpath("/completion/aaa"))
c.reset()
assert c.complete(cd).endswith(normpath("/completion/aaa"))
def test_completion(self):
c = pathedit._PathCompleter(True)
c.reset()
c.lookup = [
("a", "x/a"),
("aa", "x/aa"),
]
assert c.complete("a") == "a"
assert c.final == "x/a"
assert c.complete("a") == "aa"
assert c.complete("a") == "a"
c = pathedit._PathCompleter(True)
r = c.complete("l")
assert c.final.endswith(r)
c.reset()
assert c.complete("/nonexistent") == "/nonexistent"
assert c.final == "/nonexistent"
c.reset()
assert c.complete("~") != "~"
c.reset()
s = "thisisatotallynonexistantpathforsure"
assert c.complete(s) == s
assert c.final == s
class TestPathEdit:
def test_keypress(self):
pe = pathedit.PathEdit("", "")
with mock.patch('urwid.widget.Edit.get_edit_text') as get_text, \
mock.patch('urwid.widget.Edit.set_edit_text') as set_text:
cd = os.path.normpath(tutils.test_data.path("mitmproxy/completion"))
get_text.return_value = os.path.join(cd, "a")
# Pressing tab should set completed path
pe.keypress((1,), "tab")
set_text_called_with = set_text.call_args[0][0]
assert set_text_called_with.endswith(normpath("/completion/aaa"))
# Pressing any other key should reset
pe.keypress((1,), "a")
assert pe.lookup is None