diff --git a/.appveyor.yml b/.appveyor.yml index a9b978b9e..466c26adc 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,4 +1,4 @@ -version: 0.8.3.{build} +version: 0.8.5.{build} os: Windows Server 2012 R2 environment: diff --git a/.appveyor_install.cmd b/.appveyor_install.cmd index e6e9cb426..b467755f7 100644 --- a/.appveyor_install.cmd +++ b/.appveyor_install.cmd @@ -5,7 +5,10 @@ IF "%XONSH_TEST_ENV%" == "MSYS2" ( %MSYS2_PATH%\usr\bin\pacman.exe -Syu --noconfirm %MSYS2_PATH%\usr\bin\pacman.exe -S --noconfirm python3 python3-pip %MSYS2_PATH%\usr\bin\bash.exe -c "/usr/bin/pip install -r requirements-tests.txt" + %MSYS2_PATH%\usr\bin\bash.exe -c "/usr/bin/python setup.py install" ) ELSE ( echo "Windows Environment" %PYTHON%\Scripts\pip install -r requirements-tests.txt --upgrade --upgrade-strategy eager + %PYTHON%\python.exe --version + %PYTHON%\python.exe setup.py install ) diff --git a/.appveyor_test.cmd b/.appveyor_test.cmd index 318273b8a..05d005f2e 100644 --- a/.appveyor_test.cmd +++ b/.appveyor_test.cmd @@ -5,8 +5,10 @@ IF "%XONSH_TEST_ENV%" == "MSYS2" ( REM We monkey path `py._path.local.PosixPath` here such that it does not REM allow to create symlinks which are not supported by MSYS2 anyway. As a REM result the other pytest code uses a workaround. - call %MSYS2_PATH%\usr\bin\bash.exe -c "/usr/bin/python -u -c 'import py._path.local; del py._path.local.PosixPath.mksymlinkto; import pytest; raise SystemExit(pytest.main())'" || EXIT 1 + SET "PATH=%MSYS2_PATH%\usr\bin;%PATH%" + call bash.exe -c "/usr/bin/xonsh run-tests.xsh" || EXIT 1 ) ELSE ( echo "Windows Environment" - call %PYTHON%\Scripts\py.test || EXIT 1 + SET "PATH=%PYTHON%\Scripts;%PATH%" + call xonsh run-tests.xsh || EXIT 1 ) diff --git a/.circleci/config.yml b/.circleci/config.yml index b820d67ea..893daefb2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,7 +43,9 @@ jobs: pip install . --no-deps - run: command: | - /home/circleci/miniconda/envs/py34-xonsh-test/bin/pytest --timeout=10 + export PATH="$HOME/miniconda/bin:$PATH" + source activate ${ENV_NAME} + xonsh run-tests.xsh --timeout=10 build_35: machine: true environment: @@ -87,7 +89,9 @@ jobs: pip install . --no-deps - run: command: | - /home/circleci/miniconda/envs/py35-xonsh-test/bin/pytest --timeout=10 + export PATH="$HOME/miniconda/bin:$PATH" + source activate ${ENV_NAME} + xonsh run-tests.xsh --timeout=10 build_36: machine: true environment: @@ -131,7 +135,9 @@ jobs: pip install . --no-deps - run: command: | - /home/circleci/miniconda/envs/py36-xonsh-test/bin/pytest --timeout=10 --flake8 --cov=./xonsh + export PATH="$HOME/miniconda/bin:$PATH" + source activate ${ENV_NAME} + xonsh run-tests.xsh --timeout=10 --flake8 --cov=./xonsh build_37: machine: true environment: @@ -175,7 +181,9 @@ jobs: pip install . --no-deps - run: command: | - /home/circleci/miniconda/envs/py37-xonsh-test/bin/pytest --timeout=10 --flake8 --cov=./xonsh + export PATH="$HOME/miniconda/bin:$PATH" + source activate ${ENV_NAME} + xonsh run-tests.xsh --timeout=10 --flake8 --cov=./xonsh build_black: machine: true steps: diff --git a/.travis.yml b/.travis.yml index 48efbf5b3..b00d812a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,6 +51,7 @@ install: python setup.py install; else pip install --upgrade -r requirements-tests.txt; + python setup.py install; fi before_script: @@ -71,6 +72,6 @@ script: cd ..; doctr deploy --deploy-repo xonsh/xonsh-docs .; else - py.test --timeout=10; + xonsh run-tests.xsh --timeout=10; fi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b10d48000..40efe308f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,125 @@ Xonsh Change Log .. current developments +v0.8.5 +==================== + +**Added:** + +* Add alias to `base16 shell `_ + +* Installation / Usage + 1. To install use pip + + .. code-block:: bash + + python3 -m pip install xontrib-base16-shell + + 2. Add on ``~/.xonshrc`` + + .. code:: python + :number-lines: + + $BASE16_SHELL = $HOME + "/.config/base16-shell/" + xontrib load base16_shell + + + 3. See image + + .. image:: https://raw.githubusercontent.com/ErickTucto/xontrib-base16-shell/master/docs/terminal.png + :width: 600px + :alt: terminal.png +* New ``DumbShell`` class that kicks in whenever ``$TERM == "dumb"``. + This usually happens in emacs. Currently, this class inherits from + the ``ReadlineShell`` but adds some light customization to make + sure that xonsh looks good in the resultant terminal emulator. +* Aliases from foreign shells (e.g. Bash) that are more than single expressions, + or contain sub-shell executions, are now evaluated and run in the foreign shell. + Previously, xonsh would attempt to translate the alias from sh-lang into + xonsh. These restrictions have been removed. For example, the following now + works: + + .. code-block:: sh + + $ source-bash 'alias eee="echo aaa \$(echo b)"' + $ eee + aaa b + +* New ``ForeignShellBaseAlias``, ``ForeignShellFunctionAlias``, and + ``ForeignShellExecAlias`` classes have been added which manage foreign shell + alias execution. + + +**Changed:** + +* String aliases will now first be checked to see if they contain sub-expressions + that require evaluations, such as ``@(expr)``, ``$[cmd]``, etc. If they do, + then an ``ExecAlias`` will be constructed, rather than a simple list-of-strs + substitutiuon alias being used. For example: + + .. code-block:: sh + + $ aliases['uuu'] = "echo ccc $(echo ddd)" + $ aliases['uuu'] + ExecAlias('echo ccc $(echo ddd)\n', filename='') + $ uuu + ccc ddd + +* The ``parse_aliases()`` function now requires the shell name. +* ``ForeignShellFunctionAlias`` now inherits from ``ForeignShellBaseAlias`` + rather than ``object``. + + +**Fixed:** + +* Fixed issues where the prompt-toolkit v2 shell would print an extra newline + after Python evaluations in interactive mode. + + + + +v0.8.4 +==================== + +**Added:** + +* Added the possibility of arbitrary paths to the help strings in ``vox activate`` and + ``vox remove``; also updated the documentation accordingly. +* New ``xonsh.aliases.ExecAlias`` class enables multi-statement aliases. +* New ``xonsh.ast.isexpression()`` function will return a boolean of whether + code is a simple xonsh expression or not. +* Added top-level ``run-tests.xsh`` script for safely running the test suite. + + +**Changed:** + +* String aliases are no longer split with ``shlex.split()``, but instead use + ``xonsh.lexer.Lexer.split()``. +* Update xonsh/prompt/cwd.py _collapsed_pwd to print 2 chars if a directory begins with "." +* test which determines whether a directory is a virtualenv + + previously it used to check the existence of 'pyvenv.cfg' + now it checks if 'bin/python' is executable + + +**Fixed:** + +* Fixed issue with ``and`` & ``or`` being incorrectly tokenized in implicit + subprocesses. Auto-wrapping of certain subprocesses will now correctly work. + For example:: + + $ echo x-and-y + x-and-y +* Fix EOFError when press `control+d` +* fix no candidates if no permission files in PATH +* Fixed interpretation of color names with PTK2 and Pygments 2.3. +* Several ResourceWarnings: unclosed file in tests +* AttributeError crash when using --timings flag +* issue #2929 + + + + v0.8.3 ==================== diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d4c4d588d..7b07560a7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -20,15 +20,16 @@ jobs: architecture: 'x64' # Conda Environment -# Create and activate a Conda environment. + # Create and activate a Conda environment. - task: CondaEnvironment@1 inputs: packageSpecs: 'python=$(python.version) pygments prompt_toolkit ply pytest pytest-timeout numpy psutil matplotlib flake8 coverage pyflakes pytest-cov pytest-flake8 codecov' installOptions: '-c conda-forge' updateConda: false - script: | - pytest --timeout=10 --junitxml=junit/test-results.xml - displayName: 'pytest' + pip install . + xonsh run-tests.xsh --timeout=10 --junitxml=junit/test-results.xml + displayName: 'Tests' # Publish build results - task: PublishTestResults@2 diff --git a/docs/api/dumb_shell.rst b/docs/api/dumb_shell.rst new file mode 100644 index 000000000..0ebedcd50 --- /dev/null +++ b/docs/api/dumb_shell.rst @@ -0,0 +1,10 @@ +.. _xonsh_dumb_shell: + +****************************************************** +Dumb Shell (``xonsh.dumb_shell``) +****************************************************** + +.. automodule:: xonsh.dumb_shell + :members: + :undoc-members: + :inherited-members: diff --git a/docs/api/index.rst b/docs/api/index.rst index c299400a8..b9345b2ce 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -74,6 +74,7 @@ For those of you who want the gritty details. pyghooks jupyter_kernel jupyter_shell + dumb_shell wizard xonfig codecache diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 276d27993..7024feaf6 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -1122,7 +1122,14 @@ matching only occurs for the first element of a subprocess command. The keys of ``aliases`` are strings that act as commands in subprocess-mode. The values are lists of strings, where the first element is the command, and the rest are the arguments. You can also set the value to a string, in which -case it will be converted to a list automatically with ``shlex.split``. +one of two things will happen: + +1. If the string is a xonsh expression, it will be converted to a list + automatically with xonsh's ``Lexer.split()`` method. +2. If the string is more complex (representing a block of xonsh code), + the alias will be registered as an ``ExecAlias``, which is a callable + alias. This block of code will then be executed whenever the alias is + run. For example, the following creates several aliases for the ``git`` version control software. Both styles (list of strings and single diff --git a/news/andash.rst b/news/andash.rst deleted file mode 100644 index f1ea2680a..000000000 --- a/news/andash.rst +++ /dev/null @@ -1,28 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* Fixed issue with ``and`` & ``or`` being incoreectly tokenized in implicit - subprocesses. Auto-wrapping of certain subprocesses will now correctly work. - For example:: - - $ echo x-and-y - x-and-y - -**Security:** - -* diff --git a/news/doco-arbitrary-paths-in-vox.rst b/news/doco-arbitrary-paths-in-vox.rst deleted file mode 100644 index b8e01a873..000000000 --- a/news/doco-arbitrary-paths-in-vox.rst +++ /dev/null @@ -1,24 +0,0 @@ -**Added:** - -* Added the possibility of arbitrary paths to the help strings in ``vox activate`` and -``vox remove``; also updated the documentation accordingly. - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* - -**Security:** - -* diff --git a/news/fix-exec-candidates.rst b/news/fix-exec-candidates.rst deleted file mode 100644 index cde24f4bf..000000000 --- a/news/fix-exec-candidates.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* fix no candidates if no permission files in PATH - -**Security:** - -* diff --git a/news/fix-resource-warnings.rst b/news/fix-resource-warnings.rst deleted file mode 100644 index 07b69015c..000000000 --- a/news/fix-resource-warnings.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* Several ResourceWarnings: unclosed file in tests - -**Security:** - -* diff --git a/news/jake.rst b/news/jake.rst deleted file mode 100644 index ceee8efaa..000000000 --- a/news/jake.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* - -**Changed:** - -* - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* AttributeError crash when using --timings flag - -**Security:** - -* diff --git a/news/update-collapsed-pwd.rst b/news/update-collapsed-pwd.rst deleted file mode 100644 index 16bd13e83..000000000 --- a/news/update-collapsed-pwd.rst +++ /dev/null @@ -1,23 +0,0 @@ -**Added:** - -* - -**Changed:** - -* Update xonsh/prompt/cwd.py _collapsed_pwd to print 2 chars if a directory begins with "." - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* - -**Security:** - -* diff --git a/news/vox_no_pyvenv_cfg.rst b/news/vox_no_pyvenv_cfg.rst deleted file mode 100644 index 146e6bff9..000000000 --- a/news/vox_no_pyvenv_cfg.rst +++ /dev/null @@ -1,26 +0,0 @@ -**Added:** - -* - -**Changed:** - -* test which determines whether a directory is a virtualenv - - previously it used to check the existence of 'pyvenv.cfg' - now it checks if 'bin/python' is executable - -**Deprecated:** - -* - -**Removed:** - -* - -**Fixed:** - -* issue #2929 - -**Security:** - -* diff --git a/rever.xsh b/rever.xsh index cb298672e..82502c3b2 100644 --- a/rever.xsh +++ b/rever.xsh @@ -12,6 +12,8 @@ $VERSION_BUMP_PATTERNS = [ $CHANGELOG_FILENAME = 'CHANGELOG.rst' $CHANGELOG_TEMPLATE = 'TEMPLATE.rst' +$PYTEST_COMMAND = "./run-tests.xsh" + $TAG_REMOTE = 'git@github.com:xonsh/xonsh.git' $TAG_TARGET = 'master' diff --git a/run-tests.xsh b/run-tests.xsh new file mode 100755 index 000000000..a869f0330 --- /dev/null +++ b/run-tests.xsh @@ -0,0 +1,10 @@ +#!/usr/bin/env xonsh +$RAISE_SUBPROC_ERROR = True + +run_separately = [ + 'tests/test_ptk_highlight.py', + ] + +![pytest @($ARGS[1:]) --ignore @(run_separately)] +for fname in run_separately: + ![pytest @($ARGS[1:]) @(fname)] diff --git a/tests/conftest.py b/tests/conftest.py index 3d7eea26f..9b100da4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,8 +33,24 @@ def source_path(): return os.path.dirname(pwd) -def ensure_attached_session(session): +def ensure_attached_session(monkeypatch, session): for i in range(1, 11): + + # next try to monkey patch with raising. + try: + monkeypatch.setattr(builtins, "__xonsh__", session, raising=True) + except AttributeError: + pass + if hasattr(builtins, "__xonsh__"): + break + # first try to monkey patch without raising. + try: + monkeypatch.setattr(builtins, "__xonsh__", session, raising=False) + except AttributeError: + pass + if hasattr(builtins, "__xonsh__"): + break + # now just try to apply it builtins.__xonsh__ = session if hasattr(builtins, "__xonsh__"): break @@ -56,11 +72,15 @@ def xonsh_execer(monkeypatch): "xonsh.built_ins.load_builtins.__code__", (lambda *args, **kwargs: None).__code__, ) + added_session = False if not hasattr(builtins, "__xonsh__"): - ensure_attached_session(XonshSession()) + added_session = True + ensure_attached_session(monkeypatch, XonshSession()) execer = Execer(unload=False) builtins.__xonsh__.execer = execer - return execer + yield execer + if added_session: + monkeypatch.delattr(builtins, "__xonsh__", raising=False) @pytest.fixture @@ -71,7 +91,7 @@ def monkeypatch_stderr(monkeypatch): yield -@pytest.yield_fixture +@pytest.fixture def xonsh_events(): yield events for name, oldevent in vars(events).items(): @@ -81,13 +101,13 @@ def xonsh_events(): setattr(events, name, newevent) -@pytest.yield_fixture -def xonsh_builtins(xonsh_events): +@pytest.fixture +def xonsh_builtins(monkeypatch, xonsh_events): """Mock out most of the builtins xonsh attributes.""" old_builtins = set(dir(builtins)) execer = getattr(getattr(builtins, "__xonsh__", None), "execer", None) session = XonshSession(execer=execer, ctx={}) - ensure_attached_session(session) + ensure_attached_session(monkeypatch, session) builtins.__xonsh__.env = DummyEnv() if ON_WINDOWS: builtins.__xonsh__.env["PATHEXT"] = [".EXE", ".BAT", ".CMD"] @@ -131,6 +151,7 @@ def xonsh_builtins(xonsh_events): # be firing events on the global instance. builtins.events = xonsh_events yield builtins + monkeypatch.delattr(builtins, "__xonsh__", raising=False) for attr in set(dir(builtins)) - old_builtins: if hasattr(builtins, attr): delattr(builtins, attr) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index e7531abe9..9326fb641 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -17,17 +17,19 @@ def cd(args, stdin=None): return args -ALIASES = Aliases( - {"o": ["omg", "lala"]}, - color_ls=["ls", "--color=true"], - ls="ls '- -'", - cd=cd, - indirect_cd="cd ..", -) -RAW = ALIASES._raw +def make_aliases(): + ales = Aliases( + {"o": ["omg", "lala"]}, + color_ls=["ls", "--color=true"], + ls="ls '- -'", + cd=cd, + indirect_cd="cd ..", + ) + return ales -def test_imports(): +def test_imports(xonsh_execer, xonsh_builtins): + ales = make_aliases() expected = { "o": ["omg", "lala"], "ls": ["ls", "- -"], @@ -35,22 +37,27 @@ def test_imports(): "cd": cd, "indirect_cd": ["cd", ".."], } - assert RAW == expected + raw = ales._raw + assert raw == expected -def test_eval_normal(xonsh_builtins): - assert ALIASES.get("o") == ["omg", "lala"] +def test_eval_normal(xonsh_execer, xonsh_builtins): + ales = make_aliases() + assert ales.get("o") == ["omg", "lala"] -def test_eval_self_reference(xonsh_builtins): - assert ALIASES.get("ls") == ["ls", "- -"] +def test_eval_self_reference(xonsh_execer, xonsh_builtins): + ales = make_aliases() + assert ales.get("ls") == ["ls", "- -"] -def test_eval_recursive(xonsh_builtins): - assert ALIASES.get("color_ls") == ["ls", "- -", "--color=true"] +def test_eval_recursive(xonsh_execer, xonsh_builtins): + ales = make_aliases() + assert ales.get("color_ls") == ["ls", "- -", "--color=true"] @skip_if_on_windows -def test_eval_recursive_callable_partial(xonsh_builtins): +def test_eval_recursive_callable_partial(xonsh_execer, xonsh_builtins): + ales = make_aliases() xonsh_builtins.__xonsh__.env = Env(HOME=os.path.expanduser("~")) - assert ALIASES.get("indirect_cd")(["arg2", "arg3"]) == ["..", "arg2", "arg3"] + assert ales.get("indirect_cd")(["arg2", "arg3"]) == ["..", "arg2", "arg3"] diff --git a/tests/test_ast.py b/tests/test_ast.py index 2097b48ee..0c5b912e9 100644 --- a/tests/test_ast.py +++ b/tests/test_ast.py @@ -2,7 +2,7 @@ import ast as pyast from xonsh import ast -from xonsh.ast import Tuple, Name, Store, min_line, Call, BinOp, pdump +from xonsh.ast import Tuple, Name, Store, min_line, Call, BinOp, pdump, isexpression import pytest @@ -49,7 +49,7 @@ def test_gather_load_store_names_tuple(): "l = 1", ], ) -def test_multilline_num(line1): +def test_multilline_num(xonsh_execer, line1): code = line1 + "\nls -l\n" tree = check_parse(code) lsnode = tree.body[1] @@ -115,10 +115,28 @@ def test_unmodified(inp): assert nodes_equal(exp, obs) -@pytest.mark.parametrize("test_input", [ - "echo; echo && echo\n", - "echo; echo && echo a\n", - "true && false && true\n", -]) + +@pytest.mark.parametrize( + "test_input", + ["echo; echo && echo\n", "echo; echo && echo a\n", "true && false && true\n"], +) def test_whitespace_subproc(test_input): assert check_parse(test_input) + + +@pytest.mark.parametrize( + "inp,exp", + [ + ("1+1", True), + ("1+1;", True), + ("1+1\n", True), + ("1+1; 2+2", False), + ("1+1; 2+2;", False), + ("1+1; 2+2\n", False), + ("1+1; 2+2;\n", False), + ("x = 42", False), + ], +) +def test_isexpression(xonsh_execer, inp, exp): + obs = isexpression(inp) + assert exp is obs diff --git a/tests/test_dirstack_unc.py b/tests/test_dirstack_unc.py index 4688a7a81..5e89c1c99 100644 --- a/tests/test_dirstack_unc.py +++ b/tests/test_dirstack_unc.py @@ -37,7 +37,7 @@ pytestmark = pytest.mark.skipif( ) -@pytest.yield_fixture(scope="module") +@pytest.fixture(scope="module") def shares_setup(tmpdir_factory): """create some shares to play with on current machine. @@ -218,8 +218,8 @@ def test_uncpushd_push_base_push_rempath(xonsh_builtins): pass -# really? Need to cut-and-paste 2 flavors of this? yield_fixture requires yield in defined function body, not callee -@pytest.yield_fixture() +# really? Need to cut-and-paste 2 flavors of this? fixture requires yield in defined function body, not callee +@pytest.fixture() def with_unc_check_enabled(): if not ON_WINDOWS: return @@ -251,7 +251,7 @@ def with_unc_check_enabled(): winreg.CloseKey(key) -@pytest.yield_fixture() +@pytest.fixture() def with_unc_check_disabled(): # just like the above, but value is 1 to *disable* unc check if not ON_WINDOWS: return diff --git a/tests/test_foreign_shells.py b/tests/test_foreign_shells.py index d9ea3c103..d8789542f 100644 --- a/tests/test_foreign_shells.py +++ b/tests/test_foreign_shells.py @@ -64,7 +64,7 @@ def test_parse_aliases(): "__XONSH_ALIAS_END__\n" "more filth" ) - obs = parse_aliases(s) + obs = parse_aliases(s, 'bash') assert exp == obs diff --git a/tests/test_history.py b/tests/test_history.py index a329957c6..5f07d565f 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -15,7 +15,7 @@ from xonsh.history.main import history_main, _xh_parse_args, construct_history CMDS = ["ls", "cat hello kitty", "abc", "def", "touch me", "grep from me"] -@pytest.yield_fixture +@pytest.fixture def hist(): h = JsonHistory( filename="xonsh-HISTORY-TEST.json", here="yup", sessionid="SESSIONID", gc=False diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index ac7912fa4..5b8b3d932 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -10,7 +10,7 @@ from xonsh.history.main import history_main import pytest -@pytest.yield_fixture +@pytest.fixture def hist(): h = SqliteHistory( filename="xonsh-HISTORY-TEST.sqlite", sessionid="SESSIONID", gc=False diff --git a/tests/test_imphooks.py b/tests/test_imphooks.py index 95b70a6eb..dad072a65 100644 --- a/tests/test_imphooks.py +++ b/tests/test_imphooks.py @@ -13,8 +13,8 @@ from xonsh.built_ins import unload_builtins imphooks.install_import_hooks() -@pytest.yield_fixture(autouse=True) -def imp_env(): +@pytest.fixture(autouse=True) +def imp_env(xonsh_builtins): execer = Execer(unload=False) builtins.__xonsh__.env = Env({"PATH": [], "PATHEXT": []}) yield diff --git a/tests/test_lib/test_os.xsh b/tests/test_lib/test_os.xsh index 2662e121a..573140208 100644 --- a/tests/test_lib/test_os.xsh +++ b/tests/test_lib/test_os.xsh @@ -1,9 +1,16 @@ import os import tempfile + from xonsh.lib.os import indir, rmtree +import pytest + +from tools import ON_WINDOWS + def test_indir(): + if ON_WINDOWS: + pytest.skip("On Windows") with tempfile.TemporaryDirectory() as tmpdir: assert ![pwd].output.strip() != tmpdir with indir(tmpdir): diff --git a/tests/test_lib/test_subprocess.xsh b/tests/test_lib/test_subprocess.xsh index d5774ea1c..76d1e56d9 100644 --- a/tests/test_lib/test_subprocess.xsh +++ b/tests/test_lib/test_subprocess.xsh @@ -4,10 +4,14 @@ import tempfile from xonsh.lib.os import indir from xonsh.lib.subprocess import run, check_call, check_output, CalledProcessError -from tools import skip_if_on_windows +import pytest + +from tools import ON_WINDOWS def test_run(): + if ON_WINDOWS: + pytest.skip("On Windows") with tempfile.TemporaryDirectory() as tmpdir: with indir(tmpdir): run(['touch', 'hello.txt']) @@ -19,6 +23,8 @@ def test_run(): def test_check_call(): + if ON_WINDOWS: + pytest.skip("On Windows") with tempfile.TemporaryDirectory() as tmpdir: with indir(tmpdir): check_call(['touch', 'hello.txt']) @@ -29,8 +35,9 @@ def test_check_call(): assert 'tst_dir/hello.txt' in g`tst_dir/*.txt` -@skip_if_on_windows def test_check_call_raises(): + if ON_WINDOWS: + pytest.skip("On Windows") try: check_call('false') got_raise = False @@ -40,6 +47,8 @@ def test_check_call_raises(): def test_check_output(): + if ON_WINDOWS: + pytest.skip("On Windows") with tempfile.TemporaryDirectory() as tmpdir: with indir(tmpdir): check_call(['touch', 'hello.txt']) @@ -49,4 +58,3 @@ def test_check_output(): p = check_output(['touch', 'hello.txt'], cwd='tst_dir') assert p.decode('utf-8') == '' assert 'tst_dir/hello.txt' in g`tst_dir/*.txt` - diff --git a/tests/test_ptk_highlight.py b/tests/test_ptk_highlight.py index 0c65c9140..3a594cf57 100644 --- a/tests/test_ptk_highlight.py +++ b/tests/test_ptk_highlight.py @@ -19,11 +19,12 @@ from tools import skip_if_on_windows from xonsh.platform import ON_WINDOWS from xonsh.built_ins import load_builtins, unload_builtins +from xonsh.execer import Execer from xonsh.pyghooks import XonshLexer -@pytest.yield_fixture(autouse=True) -def load_command_cache(): +@pytest.fixture(autouse=True) +def load_command_cache(xonsh_builtins): load_builtins() if ON_WINDOWS: for key in ("cd", "bash"): @@ -58,10 +59,12 @@ def test_bin_ls(): check_token("/bin/ls -al", [(Name.Builtin, "/bin/ls")]) +@skip_if_on_windows def test_py_print(): check_token('print("hello")', [(Keyword, "print"), (String.Double, "hello")]) +@skip_if_on_windows def test_invalid_cmd(): check_token("non-existance-cmd -al", [(Name, "non")]) # parse as python check_token( @@ -71,6 +74,7 @@ def test_invalid_cmd(): check_token("(1, )", [(Punctuation, "("), (Number.Integer, "1")]) +@skip_if_on_windows def test_multi_cmd(): check_token( "cd && cd", [(Name.Builtin, "cd"), (Operator, "&&"), (Name.Builtin, "cd")] @@ -81,6 +85,7 @@ def test_multi_cmd(): ) +@skip_if_on_windows def test_nested(): check_token( 'echo @("hello")', @@ -117,6 +122,7 @@ def test_nested(): ) +@skip_if_on_windows def test_path(tmpdir): test_dir = str(tmpdir.mkdir("xonsh-test-highlight-path")) check_token( @@ -132,10 +138,12 @@ def test_path(tmpdir): check_token(test_dir, [(Name.Constant, test_dir)]) +@skip_if_on_windows def test_subproc_args(): check_token("cd 192.168.0.1", [(Text, "192.168.0.1")]) +@skip_if_on_windows def test_backtick(): check_token( r"echo g`.*\w+`", @@ -149,6 +157,7 @@ def test_backtick(): ) +@skip_if_on_windows def test_macro(): check_token( r"g!(42, *, 65)", diff --git a/tests/test_ptk_multiline.py b/tests/test_ptk_multiline.py index 994456030..81dc953c0 100644 --- a/tests/test_ptk_multiline.py +++ b/tests/test_ptk_multiline.py @@ -18,7 +18,7 @@ from tools import DummyEnv, skip_if_lt_ptk2 Context = namedtuple("Context", ["indent", "buffer", "accept", "cli", "cr"]) -@pytest.yield_fixture(scope="module") +@pytest.fixture(scope="module") def ctx(): """Context in which the ptk multiline functionality will be tested.""" builtins.__xonsh__ = XonshSession() diff --git a/tests/test_replay.py b/tests/test_replay.py index 1e6b39add..6c0051ef6 100644 --- a/tests/test_replay.py +++ b/tests/test_replay.py @@ -15,7 +15,7 @@ from tools import skip_if_on_darwin HISTDIR = os.path.join(os.path.dirname(__file__), "histories") -@pytest.yield_fixture(scope="module", autouse=True) +@pytest.fixture(scope="module", autouse=True) def ctx(): """Create a global Shell instance to use in all the test.""" ctx = {"PATH": []} diff --git a/tests/test_tools.py b/tests/test_tools.py index 74ff7e834..e7971d7b0 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -64,6 +64,7 @@ from xonsh.tools import ( is_balanced, subexpr_before_unbalanced, swap_values, + get_line_continuation, get_logical_line, replace_logical_line, check_quotes, @@ -521,7 +522,8 @@ def test_replace_logical_line(src, idx, exp_line, exp_n): idx -= 1 replace_logical_line(lines, logical, idx, exp_n) exp = src.replace("\\\n", "").strip() - obs = "\n".join(lines).replace("\\\n", "").strip() + lc = get_line_continuation() + "\n" + obs = "\n".join(lines).replace(lc, "").strip() assert exp == obs diff --git a/tests/test_xontribs.py b/tests/test_xontribs.py index cd45bcfa0..03f42b26b 100644 --- a/tests/test_xontribs.py +++ b/tests/test_xontribs.py @@ -9,7 +9,7 @@ def test_load_xontrib_metadata(): xontrib_metadata() -@pytest.yield_fixture +@pytest.fixture def tmpmod(tmpdir): """ Same as tmpdir but also adds/removes it to the front of sys.path. diff --git a/tests/tools.py b/tests/tools.py index 4ea12ec93..62781254d 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -30,6 +30,9 @@ ON_CONDA = True in [ conda in pytest.__file__.lower() for conda in ["conda", "anaconda", "miniconda"] ] ON_TRAVIS = "TRAVIS" in os.environ and "CI" in os.environ +ON_AZURE_PIPELINES = os.environ.get("TF_BUILD", "") == "True" +print("ON_AZURE_PIPELINES", repr(ON_AZURE_PIPELINES)) +print("os.environ['TF_BUILD']", repr(os.environ.get("TF_BUILD", ""))) TEST_DIR = os.path.dirname(__file__) # pytest skip decorators @@ -48,6 +51,8 @@ skip_if_on_msys = pytest.mark.skipif( skip_if_on_windows = pytest.mark.skipif(ON_WINDOWS, reason="Unix stuff") +skip_if_on_azure_pipelines = pytest.mark.skipif(ON_AZURE_PIPELINES, reason="not suitable for azure") + skip_if_on_unix = pytest.mark.skipif(not ON_WINDOWS, reason="Windows stuff") skip_if_on_darwin = pytest.mark.skipif(ON_DARWIN, reason="not Mac friendly") diff --git a/xonsh/__init__.py b/xonsh/__init__.py index 360761256..f9325cf24 100644 --- a/xonsh/__init__.py +++ b/xonsh/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.8.3" +__version__ = "0.8.5" # amalgamate exclude jupyter_kernel parser_table parser_test_table pyghooks @@ -86,6 +86,8 @@ else: _sys.modules["xonsh.tracer"] = __amalgam__ aliases = __amalgam__ _sys.modules["xonsh.aliases"] = __amalgam__ + dumb_shell = __amalgam__ + _sys.modules["xonsh.dumb_shell"] = __amalgam__ built_ins = __amalgam__ _sys.modules["xonsh.built_ins"] = __amalgam__ execer = __amalgam__ diff --git a/xonsh/aliases.py b/xonsh/aliases.py index fc2169426..4d74a89f8 100644 --- a/xonsh/aliases.py +++ b/xonsh/aliases.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """Aliases for the xonsh shell.""" import os +import re import sys -import shlex import inspect import argparse import builtins @@ -24,14 +24,26 @@ from xonsh.platform import ( from xonsh.tools import unthreadable, print_color from xonsh.replay import replay_main from xonsh.timings import timeit_alias -from xonsh.tools import argvquote, escape_windows_cmd_string, to_bool, swap_values +from xonsh.tools import ( + argvquote, + escape_windows_cmd_string, + to_bool, + swap_values, + strip_simple_quotes, +) from xonsh.xontribs import xontribs_main +from xonsh.ast import isexpression import xonsh.completers._aliases as xca import xonsh.history.main as xhm import xonsh.xoreutils.which as xxw +@lazyobject +def SUB_EXEC_ALIAS_RE(): + return re.compile(r"@\(|\$\(|!\(|\$\[|!\[") + + class Aliases(cabc.MutableMapping): """Represents a location to hold and look up aliases.""" @@ -115,7 +127,17 @@ class Aliases(cabc.MutableMapping): def __setitem__(self, key, val): if isinstance(val, str): - self._raw[key] = shlex.split(val) + f = "" + if SUB_EXEC_ALIAS_RE.search(val) is not None: + # We have a sub-command, e.g. $(cmd), to evaluate + self._raw[key] = ExecAlias(val, filename=f) + elif isexpression(val): + # expansion substitution + lexer = builtins.__xonsh__.execer.parser.lexer + self._raw[key] = list(map(strip_simple_quotes, lexer.split(val))) + else: + # need to exec alias + self._raw[key] = ExecAlias(val, filename=f) else: self._raw[key] = val @@ -150,6 +172,37 @@ class Aliases(cabc.MutableMapping): p.pretty(dict(self)) +class ExecAlias: + """Provides a callable alias for xonsh source code.""" + + def __init__(self, src, filename=""): + """ + Parameters + ---------- + src : str + Source code that will be + """ + self.src = src if src.endswith("\n") else src + "\n" + self.filename = filename + + def __call__( + self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None + ): + execer = builtins.__xonsh__.execer + frame = stack[0][0] # execute as though we are at the call site + execer.exec( + self.src, glbs=frame.f_globals, locs=frame.f_locals, filename=self.filename + ) + + def __repr__(self): + return "ExecAlias({0!r}, filename={1!r})".format(self.src, self.filename) + + +# +# Actual aliases below +# + + def xonsh_exit(args, stdin=None): """Sends signal to exit shell.""" if not clean_jobs(): diff --git a/xonsh/ast.py b/xonsh/ast.py index c93b31a22..021e77429 100644 --- a/xonsh/ast.py +++ b/xonsh/ast.py @@ -3,6 +3,7 @@ # These are imported into our module namespace for the benefit of parser.py. # pylint: disable=unused-import import sys +import builtins from ast import ( Module, Num, @@ -302,6 +303,26 @@ def isdescendable(node): return isinstance(node, (UnaryOp, BoolOp)) +def isexpression(node, ctx=None, *args, **kwargs): + """Determines whether a node (or code string) is an expression, and + does not contain any statements. The execution context (ctx) and + other args and kwargs are passed down to the parser, as needed. + """ + # parse string to AST + if isinstance(node, str): + node = node if node.endswith("\n") else node + "\n" + ctx = builtins.__xonsh__.ctx if ctx is None else ctx + node = builtins.__xonsh__.execer.parse(node, ctx, *args, **kwargs) + # determin if expresission-like enough + if isinstance(node, (Expr, Expression)): + isexpr = True + elif isinstance(node, Module) and len(node.body) == 1: + isexpr = isinstance(node.body[0], (Expr, Expression)) + else: + isexpr = False + return isexpr + + class CtxAwareTransformer(NodeTransformer): """Transforms a xonsh AST based to use subprocess calls when the first name in an expression statement is not known in the context. diff --git a/xonsh/dumb_shell.py b/xonsh/dumb_shell.py new file mode 100644 index 000000000..9e4f8da18 --- /dev/null +++ b/xonsh/dumb_shell.py @@ -0,0 +1,12 @@ +"""A dumb shell for when $TERM == 'dumb', which usually happens in emacs.""" +import builtins + +from xonsh.readline_shell import ReadlineShell + + +class DumbShell(ReadlineShell): + """A dumb shell for when $TERM == 'dumb', which usually happens in emacs.""" + + def __init__(self, *args, **kwargs): + builtins.__xonsh__.env["XONSH_COLOR_STYLE"] = "emacs" + super().__init__(*args, **kwargs) diff --git a/xonsh/foreign_shells.py b/xonsh/foreign_shells.py index 6a2473971..2bea06a06 100644 --- a/xonsh/foreign_shells.py +++ b/xonsh/foreign_shells.py @@ -298,7 +298,7 @@ def foreign_shell_data( if use_tmpfile: os.remove(tmpfile.name) env = parse_env(s) - aliases = parse_aliases(s) + aliases = parse_aliases(s, shell=shell, sourcer=sourcer, extra_args=extra_args) funcs = parse_funcs(s, shell=shell, sourcer=sourcer, extra_args=extra_args) aliases.update(funcs) return env, aliases @@ -332,7 +332,12 @@ def ALIAS_RE(): ) -def parse_aliases(s): +@lazyobject +def FS_EXEC_ALIAS_RE(): + return re.compile(r";|`|\$\(") + + +def parse_aliases(s, shell, sourcer=None, extra_args=()): """Parses the aliases portion of string into a dict.""" m = ALIAS_RE.search(s) if m is None: @@ -352,7 +357,20 @@ def parse_aliases(s): # strip one single quote at the start and end of value if value[0] == "'" and value[-1] == "'": value = value[1:-1] - value = shlex.split(value) + # now compute actual alias + if FS_EXEC_ALIAS_RE.search(value) is None: + # simple list of args alias + value = shlex.split(value) + else: + # alias is more complex, use ExecAlias, but via shell + filename = "" + value = ForeignShellExecAlias( + src=value, + shell=shell, + filename=filename, + sourcer=sourcer, + extra_args=extra_args, + ) except ValueError as exc: warnings.warn( 'could not parse alias "{0}": {1!r}'.format(key, exc), RuntimeWarning @@ -400,7 +418,7 @@ def parse_funcs(s, shell, sourcer=None, extra_args=()): if not os.path.isabs(filename): filename = os.path.abspath(filename) wrapper = ForeignShellFunctionAlias( - name=funcname, + funcname=funcname, shell=shell, sourcer=sourcer, filename=filename, @@ -410,19 +428,17 @@ def parse_funcs(s, shell, sourcer=None, extra_args=()): return funcs -class ForeignShellFunctionAlias(object): +class ForeignShellBaseAlias(object): """This class is responsible for calling foreign shell functions as if they were aliases. This does not currently support taking stdin. """ - INPUT = '{sourcer} "{filename}"\n' "{funcname} {args}\n" + INPUT = "echo ForeignShellBaseAlias {shell} {filename} {args}\n" - def __init__(self, name, shell, filename, sourcer=None, extra_args=()): + def __init__(self, shell, filename, sourcer=None, extra_args=()): """ Parameters ---------- - name : str - function name shell : str Name or path to shell filename : str @@ -433,36 +449,29 @@ class ForeignShellFunctionAlias(object): Additional command line options to pass into the shell. """ sourcer = DEFAULT_SOURCERS.get(shell, "source") if sourcer is None else sourcer - self.name = name self.shell = shell self.filename = filename self.sourcer = sourcer self.extra_args = extra_args - def __eq__(self, other): - if ( - not hasattr(other, "name") - or not hasattr(other, "filename") - or not hasattr(other, "sourcer") - or not hasattr(other, "exta_args") - ): - return NotImplemented - return ( - (self.name == other.name) - and (self.shell == other.shell) - and (self.filename == other.filename) - and (self.sourcer == other.sourcer) - and (self.extra_args == other.extra_args) - ) + def _input_kwargs(self): + return { + "shell": self.shell, + "filename": self.filename, + "sourcer": self.sourcer, + "extra_args": self.extra_args, + } - def __call__(self, args, stdin=None): + def __eq__(self, other): + if not hasattr(other, "_input_kwargs") or not callable(other._input_kwargs): + return NotImplemented + return self._input_kwargs() == other._input_kwargs() + + def __call__( + self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None + ): args, streaming = self._is_streaming(args) - input = self.INPUT.format( - sourcer=self.sourcer, - filename=self.filename, - funcname=self.name, - args=" ".join(args), - ) + input = self.INPUT.format(args=" ".join(args), **self._input_kwargs()) cmd = [self.shell] + list(self.extra_args) + ["-c", input] env = builtins.__xonsh__.env denv = env.detype() @@ -478,7 +487,21 @@ class ForeignShellFunctionAlias(object): out = out.replace("\r\n", "\n") return out - def _is_streaming(self, args): + def __repr__(self): + return ( + self.__class__.__name__ + + "(" + + ", ".join( + [ + "{k}={v!r}".format(k=k, v=v) + for k, v in sorted(self._input_kwargs().items()) + ] + ) + + ")" + ) + + @staticmethod + def _is_streaming(args): """Test and modify args if --xonsh-stream is present.""" if "--xonsh-stream" not in args: return args, False @@ -487,6 +510,77 @@ class ForeignShellFunctionAlias(object): return args, True +class ForeignShellFunctionAlias(ForeignShellBaseAlias): + """This class is responsible for calling foreign shell functions as if + they were aliases. This does not currently support taking stdin. + """ + + INPUT = '{sourcer} "{filename}"\n' "{funcname} {args}\n" + + def __init__(self, funcname, shell, filename, sourcer=None, extra_args=()): + """ + Parameters + ---------- + funcname : str + function name + shell : str + Name or path to shell + filename : str + Where the function is defined, path to source. + sourcer : str or None, optional + Command to source foreign files with. + extra_args : tuple of str, optional + Additional command line options to pass into the shell. + """ + super().__init__( + shell=shell, filename=filename, sourcer=sourcer, extra_args=extra_args + ) + self.funcname = funcname + + def _input_kwargs(self): + inp = super()._input_kwargs() + inp["funcname"] = self.funcname + return inp + + +class ForeignShellExecAlias(ForeignShellBaseAlias): + """Provides a callable alias for source code in a foreign shell.""" + + INPUT = "{src} {args}\n" + + def __init__( + self, + src, + shell, + filename="", + sourcer=None, + extra_args=(), + ): + """ + Parameters + ---------- + src : str + Source code in the shell language + shell : str + Name or path to shell + filename : str + Where the function is defined, path to source. + sourcer : str or None, optional + Command to source foreign files with. + extra_args : tuple of str, optional + Additional command line options to pass into the shell. + """ + super().__init__( + shell=shell, filename=filename, sourcer=sourcer, extra_args=extra_args + ) + self.src = src.strip() + + def _input_kwargs(self): + inp = super()._input_kwargs() + inp["src"] = self.src + return inp + + @lazyobject def VALID_SHELL_PARAMS(): return frozenset( diff --git a/xonsh/main.py b/xonsh/main.py index ae9e857d0..854005e2f 100644 --- a/xonsh/main.py +++ b/xonsh/main.py @@ -257,7 +257,8 @@ def _pprint_displayhook(value): printed_val = repr(value) if HAS_PYGMENTS and env.get("COLOR_RESULTS"): tokens = list(pygments.lex(printed_val, lexer=pyghooks.XonshLexer())) - print_color(tokens) + end = "" if env.get("SHELL_TYPE") == "prompt_toolkit2" else "\n" + print_color(tokens, end=end) else: print(printed_val) # black & white case builtins._ = value diff --git a/xonsh/platform.py b/xonsh/platform.py index aef9d03cf..bec554e9e 100644 --- a/xonsh/platform.py +++ b/xonsh/platform.py @@ -194,7 +194,9 @@ def ptk_below_max_supported(): @functools.lru_cache(1) def best_shell_type(): - if ON_WINDOWS or has_prompt_toolkit(): + if builtins.__xonsh__.env.get("TERM", "") == "dumb": + return "dumb" + elif ON_WINDOWS or has_prompt_toolkit(): return "prompt_toolkit" else: return "readline" diff --git a/xonsh/ptk2/key_bindings.py b/xonsh/ptk2/key_bindings.py index 293c4b060..cf7bd38ba 100644 --- a/xonsh/ptk2/key_bindings.py +++ b/xonsh/ptk2/key_bindings.py @@ -148,7 +148,7 @@ def ctrl_d_condition(): empty. """ if builtins.__xonsh__.env.get("IGNOREEOF"): - raise EOFError + return False else: app = get_app() buffer_name = app.current_buffer.name diff --git a/xonsh/pyghooks.py b/xonsh/pyghooks.py index a581c777b..b3c1ada27 100644 --- a/xonsh/pyghooks.py +++ b/xonsh/pyghooks.py @@ -1431,7 +1431,7 @@ def XonshTerminal256Formatter(): ptk_version_info() and ptk_version_info() > (2, 0) and pygments_version_info() - and (2, 2) <= pygments_version_info() < (2, 3) + and (2, 2, 0) <= pygments_version_info() <= (2, 3, 0) ): # Monky patch pygments' dict of console codes # with the new color names used by PTK2 diff --git a/xonsh/shell.py b/xonsh/shell.py index d0bf54213..fb4282057 100644 --- a/xonsh/shell.py +++ b/xonsh/shell.py @@ -119,6 +119,8 @@ class Shell(object): shell_type_aliases = { "b": "best", "best": "best", + "d": "dumb", + "dumb": "dumb", "ptk": "prompt_toolkit", "ptk1": "prompt_toolkit1", "ptk2": "prompt_toolkit2", @@ -166,6 +168,8 @@ class Shell(object): shell_type = self.shell_type_aliases.get(shell_type, shell_type) if shell_type == "best" or shell_type is None: shell_type = best_shell_type() + elif env.get("TERM", "") == "dumb": + shell_type = "dumb" elif shell_type == "random": shell_type = random.choice(("readline", "prompt_toolkit")) if shell_type == "prompt_toolkit": @@ -195,6 +199,8 @@ class Shell(object): from xonsh.readline_shell import ReadlineShell as shell_class elif shell_type == "jupyter": from xonsh.jupyter_shell import JupyterShell as shell_class + elif shell_type == "dumb": + from xonsh.dumb_shell import DumbShell as shell_class else: raise XonshError("{} is not recognized as a shell type".format(shell_type)) self.shell = shell_class(execer=self.execer, ctx=self.ctx, **kwargs) diff --git a/xonsh/tools.py b/xonsh/tools.py index a3394d36e..687b331d7 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -1979,6 +1979,38 @@ def RE_COMPLETE_STRING(): return re.compile(ptrn, re.DOTALL) +def strip_simple_quotes(s): + """Gets rid of single quotes, double quotes, single triple quotes, and + single double quotes from a string, if present front and back of a string. + Otherwiswe, does nothing. + """ + starts_single = s.startswith("'") + starts_double = s.startswith('"') + if not starts_single and not starts_double: + return s + elif starts_single: + ends_single = s.endswith("'") + if not ends_single: + return s + elif s.startswith("'''") and s.endswith("'''") and len(s) >= 6: + return s[3:-3] + elif len(s) >= 2: + return s[1:-1] + else: + return s + else: + # starts double + ends_double = s.endswith('"') + if not ends_double: + return s + elif s.startswith('"""') and s.endswith('"""') and len(s) >= 6: + return s[3:-3] + elif len(s) >= 2: + return s[1:-1] + else: + return s + + def check_for_partial_string(x): """Returns the starting index (inclusive), ending index (exclusive), and starting quote string of the most recent Python string found in the input. diff --git a/xonsh/xontribs.json b/xonsh/xontribs.json index 831edfaa7..b54921cc6 100644 --- a/xonsh/xontribs.json +++ b/xonsh/xontribs.json @@ -28,6 +28,11 @@ "its behavior. To see the modifications as they are applied (in unified diff", "format), please set ``$XONSH_DEBUG`` to ``2`` or higher."] }, + {"name": "base16_shell", + "package": "xontrib-base16-shell", + "url": "https://github.com/ErickTucto/xontrib-base16-shell", + "description": ["Change base16 shell themes"] + }, {"name": "coreutils", "package": "xonsh", "url": "http://xon.sh", diff --git a/xontrib/vox.py b/xontrib/vox.py index 4c2fcc9ba..7fae9ed9a 100644 --- a/xontrib/vox.py +++ b/xontrib/vox.py @@ -50,18 +50,18 @@ class VoxHandler: help='Activate virtual environment' ) activate.add_argument('name', metavar='ENV', - help='The environment to activate. ENV can be '+ - 'either a name from the venvs shown by '+ - 'vox list or the path to an arbitrary venv') + help=('The environment to activate. ENV can be ' + 'either a name from the venvs shown by vox' + 'list or the path to an arbitrary venv')) subparsers.add_parser('deactivate', aliases=['exit'], help='Deactivate current virtual environment') subparsers.add_parser('list', aliases=['ls'], - help='List environments available in '+ - '$VIRTUALENV_HOME') + help=('List environments available in ' + '$VIRTUALENV_HOME')) remove = subparsers.add_parser('remove', aliases=['rm', 'delete', 'del'], help='Remove virtual environment') remove.add_argument('names', metavar='ENV', nargs='+', - help='The environments to remove. ENV can be '+ - 'either a name from the venvs shown by '+ - 'vox list or the path to an arbitrary venv') + help=('The environments to remove. ENV can be ' + 'either a name from the venvs shown by vox' + 'list or the path to an arbitrary venv')) subparsers.add_parser('help', help='Show this help message') return parser