mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-05 17:00:58 +01:00
Merge branch 'msubnest' into mcont
This commit is contained in:
commit
00afc3aff0
14 changed files with 318 additions and 68 deletions
21
news/history-api.rst
Normal file
21
news/history-api.rst
Normal file
|
@ -0,0 +1,21 @@
|
|||
**Added:**
|
||||
|
||||
* ``History`` methods ``__iter__`` and ``__getitem__``
|
||||
|
||||
* ``tools.get_portions`` that yields parts of an iterable
|
||||
|
||||
**Changed:**
|
||||
|
||||
* ``_curr_session_parser`` now iterates over ``History``
|
||||
|
||||
**Deprecated:** None
|
||||
|
||||
**Removed:**
|
||||
|
||||
* ``History`` method ``show``
|
||||
|
||||
* ``_hist_get_portion`` in favor of ``tools.get_portions``
|
||||
|
||||
**Fixed:** None
|
||||
|
||||
**Security:** None
|
14
news/ptk-completion-display.rst
Normal file
14
news/ptk-completion-display.rst
Normal file
|
@ -0,0 +1,14 @@
|
|||
**Added:** None
|
||||
|
||||
**Changed:**
|
||||
|
||||
* ``prompt_toolkit`` completions now only show the rightmost portion
|
||||
of a given completion in the dropdown
|
||||
|
||||
**Deprecated:** None
|
||||
|
||||
**Removed:** None
|
||||
|
||||
**Fixed:** None
|
||||
|
||||
**Security:** None
|
13
news/test_xsh.rst
Normal file
13
news/test_xsh.rst
Normal file
|
@ -0,0 +1,13 @@
|
|||
**Added:**
|
||||
|
||||
* Added a py.test plugin to collect `test_*.xsh` files and run `test_*()` functions.
|
||||
|
||||
**Changed:** None
|
||||
|
||||
**Deprecated:** None
|
||||
|
||||
**Removed:** None
|
||||
|
||||
**Fixed:** None
|
||||
|
||||
**Security:** None
|
3
setup.py
3
setup.py
|
@ -310,7 +310,8 @@ def main():
|
|||
skw['entry_points'] = {
|
||||
'pygments.lexers': ['xonsh = xonsh.pyghooks:XonshLexer',
|
||||
'xonshcon = xonsh.pyghooks:XonshConsoleLexer'],
|
||||
}
|
||||
'pytest11': ['xonsh = xonsh.pytest_plugin']
|
||||
}
|
||||
skw['cmdclass']['develop'] = xdevelop
|
||||
setup(**skw)
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import glob
|
||||
import builtins
|
||||
|
||||
import pytest
|
||||
from tools import DummyShell, sp
|
||||
|
||||
import xonsh.built_ins
|
||||
|
||||
from xonsh.built_ins import ensure_list_of_strs
|
||||
from xonsh.execer import Execer
|
||||
from xonsh.tools import XonshBlockError
|
||||
from xonsh.events import events
|
||||
import glob
|
||||
|
||||
from tools import DummyShell, sp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -68,24 +68,24 @@ def test_cmd_field(hist, xonsh_builtins):
|
|||
assert None == hist.outs[-1]
|
||||
|
||||
|
||||
cmds = ['ls', 'cat hello kitty', 'abc', 'def', 'touch me', 'grep from me']
|
||||
CMDS = ['ls', 'cat hello kitty', 'abc', 'def', 'touch me', 'grep from me']
|
||||
|
||||
@pytest.mark.parametrize('inp, commands, offset', [
|
||||
('', cmds, (0, 1)),
|
||||
('-r', list(reversed(cmds)), (len(cmds)- 1, -1)),
|
||||
('0', cmds[0:1], (0, 1)),
|
||||
('1', cmds[1:2], (1, 1)),
|
||||
('-2', cmds[-2:-1], (len(cmds) -2 , 1)),
|
||||
('1:3', cmds[1:3], (1, 1)),
|
||||
('1::2', cmds[1::2], (1, 2)),
|
||||
('-4:-2', cmds[-4:-2], (len(cmds) - 4, 1))
|
||||
('', CMDS, (0, 1)),
|
||||
('-r', list(reversed(CMDS)), (len(CMDS)- 1, -1)),
|
||||
('0', CMDS[0:1], (0, 1)),
|
||||
('1', CMDS[1:2], (1, 1)),
|
||||
('-2', CMDS[-2:-1], (len(CMDS) -2 , 1)),
|
||||
('1:3', CMDS[1:3], (1, 1)),
|
||||
('1::2', CMDS[1::2], (1, 2)),
|
||||
('-4:-2', CMDS[-4:-2], (len(CMDS) - 4, 1))
|
||||
])
|
||||
def test_show_cmd_numerate(inp, commands, offset, hist, xonsh_builtins, capsys):
|
||||
"""Verify that CLI history commands work."""
|
||||
base_idx, step = offset
|
||||
xonsh_builtins.__xonsh_history__ = hist
|
||||
xonsh_builtins.__xonsh_env__['HISTCONTROL'] = set()
|
||||
for ts,cmd in enumerate(cmds): # populate the shell history
|
||||
for ts,cmd in enumerate(CMDS): # populate the shell history
|
||||
hist.append({'inp': cmd, 'rtn': 0, 'ts':(ts+1, ts+1.5)})
|
||||
|
||||
exp = ('{}: {}'.format(base_idx + idx * step, cmd)
|
||||
|
@ -185,3 +185,19 @@ def test_parser_show(args, exp):
|
|||
'timestamp': False}
|
||||
ns = _hist_parse_args(shlex.split(args))
|
||||
assert ns.__dict__ == exp_ns
|
||||
|
||||
|
||||
@pytest.mark.parametrize('index, exp', [
|
||||
(-1, 'grep from me'),
|
||||
('hello', 'cat hello kitty'),
|
||||
((-1, -1), 'me'),
|
||||
(('hello', 0), 'cat'),
|
||||
((-1, slice(0,2)), 'grep from'),
|
||||
(('kitty', slice(1,3)), 'hello kitty')
|
||||
])
|
||||
def test_history_getitem(index, exp, hist, xonsh_builtins):
|
||||
xonsh_builtins.__xonsh_env__['HISTCONTROL'] = set()
|
||||
for ts,cmd in enumerate(CMDS): # populate the shell history
|
||||
hist.append({'inp': cmd, 'rtn': 0, 'ts':(ts+1, ts+1.5)})
|
||||
|
||||
assert hist[index] == exp
|
||||
|
|
|
@ -24,7 +24,7 @@ from xonsh.tools import (
|
|||
to_dynamic_cwd_tuple, to_logfile_opt, pathsep_to_set, set_to_pathsep,
|
||||
is_string_seq, pathsep_to_seq, seq_to_pathsep, is_nonstring_seq_of_strings,
|
||||
pathsep_to_upper_seq, seq_to_upper_pathsep, expandvars, is_int_as_str, is_slice_as_str,
|
||||
ensure_timestamp,
|
||||
ensure_timestamp, get_portions
|
||||
)
|
||||
from xonsh.commands_cache import CommandsCache
|
||||
from xonsh.built_ins import expand_path
|
||||
|
@ -832,6 +832,7 @@ def test_bool_or_int_to_str(inp, exp):
|
|||
|
||||
@pytest.mark.parametrize('inp, exp', [
|
||||
(42, slice(42, 43)),
|
||||
(0, slice(0, 1)),
|
||||
(None, slice(None, None, None)),
|
||||
(slice(1,2), slice(1,2)),
|
||||
('-1', slice(-1, None, None)),
|
||||
|
@ -851,6 +852,21 @@ def test_ensure_slice(inp, exp):
|
|||
assert exp == obs
|
||||
|
||||
|
||||
@pytest.mark.parametrize('inp, exp', [
|
||||
((range(50), slice(25, 40)),
|
||||
list(i for i in range(25,40))),
|
||||
|
||||
(([1,2,3,4,5,6,7,8,9,10], [slice(1,4), slice(6, None)]),
|
||||
[2, 3, 4, 7, 8, 9, 10]),
|
||||
|
||||
(([1,2,3,4,5], [slice(-2, None), slice(-5, -3)]),
|
||||
[4, 5, 1, 2]),
|
||||
])
|
||||
def test_get_portions(inp, exp):
|
||||
obs = get_portions(*inp)
|
||||
assert list(obs) == exp
|
||||
|
||||
|
||||
@pytest.mark.parametrize('inp', [
|
||||
'42.3',
|
||||
'3:asd5:1',
|
||||
|
|
18
tests/test_xonsh.xsh
Normal file
18
tests/test_xonsh.xsh
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
|
||||
def test_simple():
|
||||
assert 1 + 1 == 2
|
||||
|
||||
|
||||
def test_envionment():
|
||||
$USER = 'snail'
|
||||
x = 'USER'
|
||||
assert x in ${...}
|
||||
assert ${'U' + 'SER'} == 'snail'
|
||||
|
||||
|
||||
def test_xonsh_party():
|
||||
x = 'xonsh'
|
||||
y = 'party'
|
||||
out = $(echo @(x + ' ' + y))
|
||||
assert out == 'xonsh party\n'
|
|
@ -1,7 +1,7 @@
|
|||
__version__ = '0.4.5'
|
||||
|
||||
# amalgamate exclude jupyter_kernel parser_table parser_test_table pyghooks
|
||||
# amalgamate exclude winutils wizard
|
||||
# amalgamate exclude winutils wizard pytest_plugin
|
||||
import os as _os
|
||||
if _os.getenv('XONSH_DEBUG', ''):
|
||||
pass
|
||||
|
|
|
@ -186,12 +186,11 @@ def _splitpath(path):
|
|||
|
||||
def _splitpath_helper(path, sofar=()):
|
||||
folder, path = os.path.split(path)
|
||||
if path == "":
|
||||
if path:
|
||||
sofar = sofar + (path, )
|
||||
if not folder or folder == xt.get_sep():
|
||||
return sofar[::-1]
|
||||
elif folder == "":
|
||||
return (sofar + (path, ))[::-1]
|
||||
else:
|
||||
return _splitpath_helper(folder, sofar + (path, ))
|
||||
return _splitpath_helper(folder, sofar)
|
||||
|
||||
|
||||
def subsequence_match(ref, typed, csc):
|
||||
|
|
129
xonsh/history.py
129
xonsh/history.py
|
@ -8,17 +8,16 @@ import time
|
|||
import uuid
|
||||
import argparse
|
||||
import builtins
|
||||
import collections
|
||||
import datetime
|
||||
import functools
|
||||
import itertools
|
||||
import threading
|
||||
import collections
|
||||
import collections.abc as cabc
|
||||
|
||||
from xonsh.lazyasd import lazyobject
|
||||
from xonsh.lazyjson import LazyJSON, ljdump, LJNode
|
||||
from xonsh.tools import (ensure_slice, to_history_tuple,
|
||||
expanduser_abs_path, ensure_timestamp)
|
||||
from xonsh.tools import (ensure_slice, to_history_tuple, is_string,
|
||||
get_portions, expanduser_abs_path, ensure_timestamp)
|
||||
from xonsh.diff_history import _dh_create_parser, _dh_main_action
|
||||
|
||||
|
||||
|
@ -262,7 +261,7 @@ def _all_xonsh_parser(**kwargs):
|
|||
"""
|
||||
Returns all history as found in XONSH_DATA_DIR.
|
||||
|
||||
return format: (name, start_time, index)
|
||||
return format: (cmd, start_time, index)
|
||||
"""
|
||||
data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR')
|
||||
data_dir = expanduser_abs_path(data_dir)
|
||||
|
@ -285,14 +284,11 @@ def _all_xonsh_parser(**kwargs):
|
|||
def _curr_session_parser(hist=None, **kwargs):
|
||||
"""
|
||||
Take in History object and return command list tuple with
|
||||
format: (name, start_time, index)
|
||||
format: (cmd, start_time, index)
|
||||
"""
|
||||
if hist is None:
|
||||
hist = builtins.__xonsh_history__
|
||||
start_times = (start for start, end in hist.tss)
|
||||
names = (name.rstrip() for name in hist.inps)
|
||||
for ind, (c, t) in enumerate(zip(names, start_times)):
|
||||
yield (c, t, ind)
|
||||
return iter(hist)
|
||||
|
||||
|
||||
def _zsh_hist_parser(location=None, **kwargs):
|
||||
|
@ -394,20 +390,6 @@ def _hist_create_parser():
|
|||
return p
|
||||
|
||||
|
||||
def _hist_get_portion(commands, slices):
|
||||
"""Yield from portions of history commands."""
|
||||
if len(slices) == 1:
|
||||
s = slices[0]
|
||||
try:
|
||||
yield from itertools.islice(commands, s.start, s.stop, s.step)
|
||||
return
|
||||
except ValueError: # islice failed
|
||||
pass
|
||||
commands = list(commands)
|
||||
for s in slices:
|
||||
yield from commands[s]
|
||||
|
||||
|
||||
def _hist_filter_ts(commands, start_time, end_time):
|
||||
"""Yield only the commands between start and end time."""
|
||||
for cmd in commands:
|
||||
|
@ -439,7 +421,7 @@ def _hist_get(session='session', *, slices=None, datetime_format=None,
|
|||
if slices:
|
||||
# transform/check all slices
|
||||
slices = [ensure_slice(s) for s in slices]
|
||||
cmds = _hist_get_portion(cmds, slices)
|
||||
cmds = get_portions(cmds, slices)
|
||||
if start_time or end_time:
|
||||
if start_time is None:
|
||||
start_time = 0.0
|
||||
|
@ -488,6 +470,37 @@ def _hist_show(ns, *args, **kwargs):
|
|||
class History(object):
|
||||
"""Xonsh session history.
|
||||
|
||||
Indexing
|
||||
--------
|
||||
History object acts like a sequence that can be indexed in a special way
|
||||
that adds extra functionality. At the moment only history from the
|
||||
current session can be retrieved. Note that the most recent command
|
||||
is the last item in history.
|
||||
|
||||
The index acts as a filter with two parts, command and argument,
|
||||
separated by comma. Based on the type of each part different
|
||||
filtering can be achieved,
|
||||
|
||||
for the command part:
|
||||
|
||||
- an int returns the command in that position.
|
||||
- a slice returns a list of commands.
|
||||
- a string returns the most recent command containing the string.
|
||||
|
||||
for the argument part:
|
||||
|
||||
- an int returns the argument of the command in that position.
|
||||
- a slice returns a part of the command based on the argument
|
||||
position.
|
||||
|
||||
The argument part of the filter can be omitted but the command part is
|
||||
required.
|
||||
|
||||
Command arguments are separated by white space.
|
||||
|
||||
If the filtering produces only one result it is
|
||||
returned as a string else a list of strings is returned.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
rtns : sequence of ints
|
||||
|
@ -605,16 +618,66 @@ class History(object):
|
|||
self.buffer.clear()
|
||||
return hf
|
||||
|
||||
def show(self, *args, **kwargs):
|
||||
"""Return shell history as a list
|
||||
def __iter__(self):
|
||||
"""Get current session history.
|
||||
|
||||
Valid options:
|
||||
`session` - returns xonsh history from current session
|
||||
`xonsh` - returns xonsh history from all sessions
|
||||
`zsh` - returns all zsh history
|
||||
`bash` - returns all bash history
|
||||
Yields
|
||||
------
|
||||
tuple
|
||||
``tuple`` of the form (cmd, start_time, index).
|
||||
"""
|
||||
return list(_hist_get(*args, **kwargs))
|
||||
start_times = (start for start, end in self.tss)
|
||||
names = (name.rstrip() for name in self.inps)
|
||||
for ind, (c, t) in enumerate(zip(names, start_times)):
|
||||
yield (c, t, ind)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Retrieve history parts based on filtering rules,
|
||||
see ``History`` docs for more info. Accepts one of
|
||||
int, string, slice or tuple of length two.
|
||||
"""
|
||||
if isinstance(item, tuple):
|
||||
cmd_pat, arg_pat = item
|
||||
else:
|
||||
cmd_pat, arg_pat = item, None
|
||||
cmds = (c for c, *_ in self)
|
||||
cmds = self._cmd_filter(cmds, cmd_pat)
|
||||
if arg_pat is not None:
|
||||
cmds = self._args_filter(cmds, arg_pat)
|
||||
cmds = list(cmds)
|
||||
if len(cmds) == 1:
|
||||
return cmds[0]
|
||||
else:
|
||||
return cmds
|
||||
|
||||
@staticmethod
|
||||
def _cmd_filter(cmds, pat):
|
||||
if isinstance(pat, (int, slice)):
|
||||
s = ensure_slice(pat)
|
||||
yield from get_portions(cmds, s)
|
||||
elif is_string(pat):
|
||||
for command in reversed(list(cmds)):
|
||||
if pat in command:
|
||||
yield command
|
||||
else:
|
||||
raise TypeError('Command filter must be '
|
||||
'string, int or slice')
|
||||
|
||||
@staticmethod
|
||||
def _args_filter(cmds, pat):
|
||||
args = None
|
||||
if isinstance(pat, (int, slice)):
|
||||
s = ensure_slice(pat)
|
||||
for command in cmds:
|
||||
yield ' '.join(command.split()[s])
|
||||
else:
|
||||
raise TypeError('Argument filter must be '
|
||||
'int or slice')
|
||||
return args
|
||||
|
||||
def __setitem__(self, *args):
|
||||
raise PermissionError('You cannot change history! '
|
||||
'you can create new though.')
|
||||
|
||||
|
||||
def _hist_info(ns, hist):
|
||||
|
|
|
@ -4,7 +4,7 @@ import os
|
|||
import builtins
|
||||
|
||||
from prompt_toolkit.layout.dimension import LayoutDimension
|
||||
from prompt_toolkit.completion import Completer, Completion
|
||||
from prompt_toolkit.completion import Completer, Completion, _commonprefix
|
||||
|
||||
|
||||
class PromptToolkitCompleter(Completer):
|
||||
|
@ -40,17 +40,13 @@ class PromptToolkitCompleter(Completer):
|
|||
pass
|
||||
elif len(os.path.commonprefix(completions)) <= len(prefix):
|
||||
self.reserve_space()
|
||||
# don't mess with envvar and path expansion comps
|
||||
if any(x in prefix for x in ['$', '/']):
|
||||
for comp in completions:
|
||||
yield Completion(comp, -l)
|
||||
else: # don't show common prefixes in attr completions
|
||||
prefix, _, compprefix = prefix.rpartition('.')
|
||||
for comp in completions:
|
||||
if comp.rsplit('.', 1)[0] in prefix:
|
||||
comp = comp.rsplit('.', 1)[-1]
|
||||
l = len(compprefix) if compprefix in comp else 0
|
||||
yield Completion(comp, -l)
|
||||
c_prefix = _commonprefix([a.strip('\'/').rsplit('/', 1)[0]
|
||||
for a in completions])
|
||||
for comp in completions:
|
||||
if comp.endswith('/') and not c_prefix.startswith('/'):
|
||||
c_prefix = ''
|
||||
display = comp[len(c_prefix):].lstrip('/')
|
||||
yield Completion(comp, -l, display=display)
|
||||
|
||||
def reserve_space(self):
|
||||
cli = builtins.__xonsh_shell__.shell.prompter.cli
|
||||
|
|
66
xonsh/pytest_plugin.py
Normal file
66
xonsh/pytest_plugin.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pytest plugin for testing xsh files."""
|
||||
import sys
|
||||
import importlib
|
||||
from traceback import format_list, extract_tb
|
||||
|
||||
import pytest
|
||||
from xonsh.imphooks import install_hook
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
install_hook()
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items):
|
||||
items.sort(key=lambda x: 0 if isinstance(x, XshFunction) else 1)
|
||||
|
||||
|
||||
def _limited_traceback(excinfo):
|
||||
""" Return a formatted traceback with all the stack
|
||||
from this frame (i.e __file__) up removed
|
||||
"""
|
||||
tb = extract_tb(excinfo.tb)
|
||||
try:
|
||||
idx = [__file__ in e for e in tb].index(True)
|
||||
return format_list(tb[idx+1:])
|
||||
except ValueError:
|
||||
return format_list(tb)
|
||||
|
||||
|
||||
def pytest_collect_file(parent, path):
|
||||
if path.ext.lower() == ".xsh" and path.basename.startswith("test_"):
|
||||
return XshFile(path, parent)
|
||||
|
||||
|
||||
class XshFile(pytest.File):
|
||||
def collect(self):
|
||||
sys.path.append(self.fspath.dirname)
|
||||
mod = importlib.import_module(self.fspath.purebasename)
|
||||
sys.path.pop(0)
|
||||
tests = [t for t in dir(mod) if t.startswith('test_')]
|
||||
for test_name in tests:
|
||||
obj = getattr(mod, test_name)
|
||||
if hasattr(obj, '__call__'):
|
||||
yield XshFunction(name=test_name, parent=self,
|
||||
test_func=obj, test_module=mod)
|
||||
|
||||
|
||||
class XshFunction(pytest.Item):
|
||||
def __init__(self, name, parent, test_func, test_module):
|
||||
super().__init__(name, parent)
|
||||
self._test_func = test_func
|
||||
self._test_module = test_module
|
||||
|
||||
def runtest(self):
|
||||
self._test_func()
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" called when self.runtest() raises an exception. """
|
||||
formatted_tb = _limited_traceback(excinfo)
|
||||
formatted_tb.insert(0, "xonsh execution failed\n")
|
||||
formatted_tb.append('{}: {}'.format(excinfo.type.__name__, excinfo.value))
|
||||
return "".join(formatted_tb)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, 0, "xonsh test: {}".format(self.name)
|
|
@ -25,6 +25,7 @@ import ctypes
|
|||
import datetime
|
||||
import functools
|
||||
import glob
|
||||
import itertools
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
@ -939,14 +940,14 @@ def SLICE_REG():
|
|||
|
||||
def ensure_slice(x):
|
||||
"""Try to convert an object into a slice, complain on failure"""
|
||||
if not x:
|
||||
if not x and x != 0:
|
||||
return slice(None)
|
||||
elif isinstance(x, slice):
|
||||
elif is_slice(x):
|
||||
return x
|
||||
try:
|
||||
x = int(x)
|
||||
if x != -1:
|
||||
s = slice(x, x+1)
|
||||
s = slice(x, x + 1)
|
||||
else:
|
||||
s = slice(-1, None, None)
|
||||
except ValueError:
|
||||
|
@ -965,6 +966,28 @@ def ensure_slice(x):
|
|||
return s
|
||||
|
||||
|
||||
def get_portions(it, slices):
|
||||
"""Yield from portions of an iterable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
it: iterable
|
||||
slices: a slice or a list of slice objects
|
||||
"""
|
||||
if is_slice(slices):
|
||||
slices = [slices]
|
||||
if len(slices) == 1:
|
||||
s = slices[0]
|
||||
try:
|
||||
yield from itertools.islice(it, s.start, s.stop, s.step)
|
||||
return
|
||||
except ValueError: # islice failed
|
||||
pass
|
||||
it = list(it)
|
||||
for s in slices:
|
||||
yield from it[s]
|
||||
|
||||
|
||||
def is_slice_as_str(x):
|
||||
"""
|
||||
Test if string x is a slice. If not a string return False.
|
||||
|
|
Loading…
Add table
Reference in a new issue