Merge branch 'msubnest' into mcont

This commit is contained in:
Anthony Scopatz 2016-08-31 00:48:37 -04:00
commit 00afc3aff0
14 changed files with 318 additions and 68 deletions

21
news/history-api.rst Normal file
View 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

View 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
View 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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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
View 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'

View file

@ -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

View file

@ -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):

View file

@ -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):

View file

@ -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
View 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)

View file

@ -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.