feat: auto-completion support for source-foreign (#4564)

* feat: auto-completion support for source-foreign

* feat: add completions for source-bash/zsh/cmd

* refactor: change the boolean flags names that defaults to True
This commit is contained in:
Noorhteen Raja NJ 2021-11-27 16:28:04 +05:30 committed by GitHub
parent 6d756ef7c9
commit 0053d55e3d
Failed to generate hash of commit
4 changed files with 269 additions and 182 deletions

View file

@ -0,0 +1,23 @@
**Added:**
* auto-completion support for commands : ``source-foreign``, ``source-bash``, ``source-zsh``, ``source-cmd``
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -4,7 +4,7 @@ import pytest
import builtins
from contextlib import contextmanager
from unittest.mock import MagicMock
from xonsh.aliases import source_alias
from xonsh.aliases import source_alias, make_default_aliases
@pytest.fixture
@ -39,3 +39,40 @@ def test_source_path(mockopen, mocked_execx_checker):
path_bar = os.path.join("tests", "bin", "bar")
assert mocked_execx_checker[0].endswith(path_foo)
assert mocked_execx_checker[1].endswith(path_bar)
@pytest.mark.parametrize(
"alias",
[
"source-bash",
"source-zsh",
],
)
def test_source_foreign_fn_parser(alias, xession):
aliases = make_default_aliases()
source_bash = aliases[alias]
positionals = [act.dest for act in source_bash.parser._get_positional_actions()]
options = [act.dest for act in source_bash.parser._get_optional_actions()]
assert positionals == ["files_or_code"]
assert options == [
"help",
"interactive",
"login",
"envcmd",
"aliascmd",
"extra_args",
"safe",
"prevcmd",
"postcmd",
"funcscmd",
"sourcer",
"use_tmpfile",
"seterrprevcmd",
"seterrpostcmd",
"overwrite_aliases",
"suppress_skip_message",
"show",
"dryrun",
]

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
"""Aliases for the xonsh shell."""
import functools
import os
import re
import sys
@ -29,7 +30,6 @@ from xonsh.tools import (
XonshError,
argvquote,
escape_windows_cmd_string,
to_bool,
swap_values,
strip_simple_quotes,
ALIAS_KWARG_NAMES,
@ -369,176 +369,116 @@ def xonsh_reset(args, stdin=None):
XSH.ctx.clear()
@lazyobject
def _SOURCE_FOREIGN_PARSER():
desc = "Sources a file written in a foreign shell language."
parser = argparse.ArgumentParser("source-foreign", description=desc)
parser.add_argument("shell", help="Name or path to the foreign shell")
parser.add_argument(
"files_or_code",
nargs="+",
help="file paths to source or code in the target " "language.",
)
parser.add_argument(
"-i",
"--interactive",
type=to_bool,
default=True,
help="whether the sourced shell should be interactive",
dest="interactive",
)
parser.add_argument(
"-l",
"--login",
type=to_bool,
default=False,
help="whether the sourced shell should be login",
dest="login",
)
parser.add_argument(
"--envcmd", default=None, dest="envcmd", help="command to print environment"
)
parser.add_argument(
"--aliascmd", default=None, dest="aliascmd", help="command to print aliases"
)
parser.add_argument(
"--extra-args",
default=(),
dest="extra_args",
type=(lambda s: tuple(s.split())),
help="extra arguments needed to run the shell",
)
parser.add_argument(
"-s",
"--safe",
type=to_bool,
default=True,
help="whether the source shell should be run safely, "
"and not raise any errors, even if they occur.",
dest="safe",
)
parser.add_argument(
"-p",
"--prevcmd",
default=None,
dest="prevcmd",
help="command(s) to run before any other commands, "
"replaces traditional source.",
)
parser.add_argument(
"--postcmd",
default="",
dest="postcmd",
help="command(s) to run after all other commands",
)
parser.add_argument(
"--funcscmd",
default=None,
dest="funcscmd",
help="code to find locations of all native functions " "in the shell language.",
)
parser.add_argument(
"--sourcer",
default=None,
dest="sourcer",
help="the source command in the target shell " "language, default: source.",
)
parser.add_argument(
"--use-tmpfile",
type=to_bool,
default=False,
help="whether the commands for source shell should be "
"written to a temporary file.",
dest="use_tmpfile",
)
parser.add_argument(
"--seterrprevcmd",
default=None,
dest="seterrprevcmd",
help="command(s) to set exit-on-error before any" "other commands.",
)
parser.add_argument(
"--seterrpostcmd",
default=None,
dest="seterrpostcmd",
help="command(s) to set exit-on-error after all" "other commands.",
)
parser.add_argument(
"--overwrite-aliases",
default=False,
action="store_true",
dest="overwrite_aliases",
help="flag for whether or not sourced aliases should "
"replace the current xonsh aliases.",
)
parser.add_argument(
"--suppress-skip-message",
default=None,
action="store_true",
dest="suppress_skip_message",
help="flag for whether or not skip messages should be suppressed.",
)
parser.add_argument(
"--show",
default=False,
action="store_true",
dest="show",
help="Will show the script output.",
)
parser.add_argument(
"-d",
"--dry-run",
default=False,
action="store_true",
dest="dryrun",
help="Will not actually source the file.",
)
return parser
def source_foreign_fn(
shell: str,
files_or_code: Annotated[tp.List[str], Arg(nargs="+")],
interactive=True,
login=False,
envcmd=None,
aliascmd=None,
extra_args="",
safe=True,
prevcmd="",
postcmd="",
funcscmd="",
sourcer=None,
use_tmpfile=False,
seterrprevcmd=None,
seterrpostcmd=None,
overwrite_aliases=False,
suppress_skip_message=False,
show=False,
dryrun=False,
_stderr=None,
):
"""Sources a file written in a foreign shell language.
def source_foreign(args, stdin=None, stdout=None, stderr=None):
"""Sources a file written in a foreign shell language."""
Parameters
----------
shell
Name or path to the foreign shell
files_or_code
file paths to source or code in the target language.
interactive : -n, --non-interactive
whether the sourced shell should be interactive
login : -l, --login
whether the sourced shell should be login
envcmd : --envcmd
command to print environment
aliascmd : --aliascmd
command to print aliases
extra_args : --extra-args
extra arguments needed to run the shell
safe : -u, --unsafe
whether the source shell should be run safely, and not raise any errors, even if they occur.
prevcmd : -p, --prevcmd
command(s) to run before any other commands, replaces traditional source.
postcmd : --postcmd
command(s) to run after all other commands
funcscmd : --funcscmd
code to find locations of all native functions in the shell language.
sourcer : --sourcer
the source command in the target shell language.
If this is not set, a default value will attempt to be
looked up based on the shell name.
use_tmpfile : --use-tmpfile
whether the commands for source shell should be written to a temporary file.
seterrprevcmd : --seterrprevcmd
command(s) to set exit-on-error before any other commands.
seterrpostcmd : --seterrpostcmd
command(s) to set exit-on-error after all other commands.
overwrite_aliases : --overwrite-aliases
flag for whether or not sourced aliases should replace the current xonsh aliases.
suppress_skip_message : --suppress-skip-message
flag for whether or not skip messages should be suppressed.
show : --show
show the script output.
dryrun : -d, --dry-run
Will not actually source the file.
"""
extra_args = tuple(extra_args.split())
env = XSH.env
ns = _SOURCE_FOREIGN_PARSER.parse_args(args)
ns.suppress_skip_message = (
suppress_skip_message = (
env.get("FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE")
if ns.suppress_skip_message is None
else ns.suppress_skip_message
if not suppress_skip_message
else suppress_skip_message
)
files = ()
if ns.prevcmd is not None:
files: tp.Tuple[str, ...] = ()
if prevcmd:
pass # don't change prevcmd if given explicitly
elif os.path.isfile(ns.files_or_code[0]):
elif os.path.isfile(files_or_code[0]):
if not sourcer:
return (None, "xonsh: error: `sourcer` command is not mentioned.\n", 1)
# we have filenames to source
ns.prevcmd = "".join([f"{ns.sourcer} {f}\n" for f in ns.files_or_code])
files = tuple(ns.files_or_code)
elif ns.prevcmd is None:
ns.prevcmd = " ".join(ns.files_or_code) # code to run, no files
prevcmd = "".join([f"{sourcer} {f}\n" for f in files_or_code])
files = tuple(files_or_code)
elif not prevcmd:
prevcmd = " ".join(files_or_code) # code to run, no files
foreign_shell_data.cache_clear() # make sure that we don't get prev src
fsenv, fsaliases = foreign_shell_data(
shell=ns.shell,
login=ns.login,
interactive=ns.interactive,
envcmd=ns.envcmd,
aliascmd=ns.aliascmd,
extra_args=ns.extra_args,
safe=ns.safe,
prevcmd=ns.prevcmd,
postcmd=ns.postcmd,
funcscmd=ns.funcscmd,
sourcer=ns.sourcer,
use_tmpfile=ns.use_tmpfile,
seterrprevcmd=ns.seterrprevcmd,
seterrpostcmd=ns.seterrpostcmd,
show=ns.show,
dryrun=ns.dryrun,
shell=shell,
login=login,
interactive=interactive,
envcmd=envcmd,
aliascmd=aliascmd,
extra_args=extra_args,
safe=safe,
prevcmd=prevcmd,
postcmd=postcmd,
funcscmd=funcscmd or None, # the default is None in the called function
sourcer=sourcer,
use_tmpfile=use_tmpfile,
seterrprevcmd=seterrprevcmd,
seterrpostcmd=seterrpostcmd,
show=show,
dryrun=dryrun,
files=files,
)
if fsenv is None:
if ns.dryrun:
if dryrun:
return
else:
msg = "xonsh: error: Source failed: {0!r}\n".format(ns.prevcmd)
msg = "xonsh: error: Source failed: {0!r}\n".format(prevcmd)
msg += "xonsh: error: Possible reasons: File not found or syntax error\n"
return (None, msg, 1)
# apply results
@ -556,9 +496,9 @@ def source_foreign(args, stdin=None, stdout=None, stderr=None):
for k, v in fsaliases.items():
if k in baliases and v == baliases[k]:
continue # no change from original
elif ns.overwrite_aliases or k not in baliases:
elif overwrite_aliases or k not in baliases:
baliases[k] = v
elif ns.suppress_skip_message:
elif suppress_skip_message:
pass
else:
msg = (
@ -568,7 +508,12 @@ def source_foreign(args, stdin=None, stdout=None, stderr=None):
'You may prevent this message with "--suppress-skip-message" or '
'"$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE = True".'
)
print(msg.format(k, ns.shell), file=stderr)
print(msg.format(k, shell), file=_stderr)
source_foreign = ArgParserAlias(
func=source_foreign_fn, has_args=True, prog="source-foreign"
)
@unthreadable
@ -621,9 +566,54 @@ def source_alias(args, stdin=None):
raise
def source_cmd(args, stdin=None):
"""Simple cmd.exe-specific wrapper around source-foreign."""
args = list(args)
def source_cmd_fn(
files: Annotated[tp.List[str], Arg(nargs="+")],
login=False,
aliascmd=None,
extra_args="",
safe=True,
postcmd="",
funcscmd="",
seterrprevcmd=None,
overwrite_aliases=False,
suppress_skip_message=False,
show=False,
dryrun=False,
_stderr=None,
):
"""
Source cmd.exe files
Parameters
----------
files
paths to source files.
login : -l, --login
whether the sourced shell should be login
envcmd : --envcmd
command to print environment
aliascmd : --aliascmd
command to print aliases
extra_args : --extra-args
extra arguments needed to run the shell
safe : -s, --safe
whether the source shell should be run safely, and not raise any errors, even if they occur.
postcmd : --postcmd
command(s) to run after all other commands
funcscmd : --funcscmd
code to find locations of all native functions in the shell language.
seterrprevcmd : --seterrprevcmd
command(s) to set exit-on-error before any other commands.
overwrite_aliases : --overwrite-aliases
flag for whether or not sourced aliases should replace the current xonsh aliases.
suppress_skip_message : --suppress-skip-message
flag for whether or not skip messages should be suppressed.
show : --show
show the script output.
dryrun : -d, --dry-run
Will not actually source the file.
"""
args = list(files)
fpath = locate_binary(args[0])
args[0] = fpath if fpath else args[0]
if not os.path.isfile(args[0]):
@ -631,15 +621,32 @@ def source_cmd(args, stdin=None):
prevcmd = "call "
prevcmd += " ".join([argvquote(arg, force=True) for arg in args])
prevcmd = escape_windows_cmd_string(prevcmd)
args.append("--prevcmd={}".format(prevcmd))
args.insert(0, "cmd")
args.append("--interactive=0")
args.append("--sourcer=call")
args.append("--envcmd=set")
args.append("--seterrpostcmd=if errorlevel 1 exit 1")
args.append("--use-tmpfile=1")
with XSH.env.swap(PROMPT="$P$G"):
return source_foreign(args, stdin=stdin)
return source_foreign_fn(
shell="cmd",
files_or_code=args,
interactive=True,
sourcer="call",
envcmd="set",
seterrpostcmd="if errorlevel 1 exit 1",
use_tmpfile=True,
prevcmd=prevcmd,
# from this function
login=login,
aliascmd=aliascmd,
extra_args=extra_args,
safe=safe,
postcmd=postcmd,
funcscmd=funcscmd,
seterrprevcmd=seterrprevcmd,
overwrite_aliases=overwrite_aliases,
suppress_skip_message=suppress_skip_message,
show=show,
dryrun=dryrun,
)
source_cmd = ArgParserAlias(func=source_cmd_fn, has_args=True, prog="source-cmd")
def xexec_fn(
@ -789,8 +796,16 @@ def make_default_aliases():
"exec": xexec,
"xexec": xexec,
"source": source_alias,
"source-zsh": ["source-foreign", "zsh", "--sourcer=source"],
"source-bash": ["source-foreign", "bash", "--sourcer=source"],
"source-zsh": ArgParserAlias(
func=functools.partial(source_foreign_fn, "zsh", sourcer="source"),
has_args=True,
prog="source-zsh",
),
"source-bash": ArgParserAlias(
func=functools.partial(source_foreign_fn, "bash", sourcer="source"),
has_args=True,
prog="source-bash",
),
"source-cmd": source_cmd,
"source-foreign": source_foreign,
"history": xhm.history_main,

View file

@ -6,6 +6,7 @@ Examples
"""
import argparse as ap
import functools
import inspect
import os
import sys
@ -64,7 +65,7 @@ def Arg(
class NumpyDoc:
"""Represent parsed function docstring"""
def __init__(self, func):
def __init__(self, func, prefix_chars="-", follow_wraps=True):
"""Parse the function docstring and return its help content
Parameters
@ -73,6 +74,9 @@ class NumpyDoc:
a callable/object that holds docstring
"""
if follow_wraps and isinstance(func, functools.partial):
func = func.func
doc: str = inspect.getdoc(func) or ""
self.description, rest = self.get_func_doc(doc)
@ -83,7 +87,7 @@ class NumpyDoc:
parts = [st.strip() for st in head.split(":")]
if len(parts) == 2:
name, flag = parts
if flag and flag.startswith("-"):
if flag and any(map(flag.startswith, prefix_chars)):
self.flags[name] = [st.strip() for st in flag.split(",")]
else:
name = parts[0]
@ -154,15 +158,20 @@ def _get_args_kwargs(annot: tp.Any) -> tp.Tuple[tp.Sequence[str], tp.Dict[str, t
def add_args(
parser: ap.ArgumentParser, func: tp.Callable, allowed_params=None, doc=None
parser: ap.ArgumentParser,
func: tp.Callable,
allowed_params=None,
doc=None,
) -> None:
"""Using the function's annotation add arguments to the parser
param:Arg(*args, **kw) -> parser.add_argument(*args, *kw)
basically converts ``def fn(param : Arg(*args, **kw), ...): ...``
-> into equivalent ``parser.add_argument(*args, *kw)`` call.
"""
# call this function when this sub-command is selected
parser.set_defaults(**{_FUNC_NAME: func})
doc = doc or NumpyDoc(func)
doc = doc or NumpyDoc(func, parser.prefix_chars)
sign = inspect.signature(func)
for name, param in sign.parameters.items():
if name.startswith("_") or (
@ -209,8 +218,10 @@ def add_args(
if completer:
action.completer = completer # type: ignore
action.help = action.help or ""
if action.default and "%(default)s" not in action.help:
action.help += os.linesep + " (default: %(default)s)"
if (action.default or action.default is False) and (
"%(default)s" not in action.help
):
action.help += os.linesep + " (default: '%(default)s')"
if action.type and "%(type)s" not in action.help:
action.help += " (type: %(type)s)"
@ -575,6 +586,7 @@ class ArgParserAlias:
has_args = has_args or bool(allowed_params)
if has_args:
kwargs.setdefault("empty_help", False)
parser = make_parser(func, **kwargs)
if has_args:
add_args(parser, func, allowed_params=allowed_params)