diff --git a/news/ap-xonfig.rst b/news/ap-xonfig.rst new file mode 100644 index 000000000..e166ef7e4 --- /dev/null +++ b/news/ap-xonfig.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* ``xonfig`` now has colored help message when ran interactively. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/completers/test_completer_command.py b/tests/completers/test_completer_command.py index cf407110f..686b3dae1 100644 --- a/tests/completers/test_completer_command.py +++ b/tests/completers/test_completer_command.py @@ -1,15 +1,6 @@ import pytest -@pytest.fixture -def xsh_with_aliases(xession, monkeypatch): - from xonsh.aliases import Aliases, make_default_aliases - - xsh = xession - monkeypatch.setattr(xsh, "aliases", Aliases(make_default_aliases())) - return xsh - - @pytest.fixture def mock_completer(monkeypatch, xsh_with_aliases): xsh = xsh_with_aliases diff --git a/tests/completers/test_xompletions.py b/tests/completers/test_xompletions.py index cdd882edb..00b93774c 100644 --- a/tests/completers/test_xompletions.py +++ b/tests/completers/test_xompletions.py @@ -3,31 +3,30 @@ from xonsh.parsers.completion_context import ( CommandContext, CompletionContext, ) -from xonsh.completers.xompletions import complete_xonfig, complete_xontrib, xt +from xonsh.completers.xompletions import complete_xontrib +import pytest -def test_xonfig(): - assert complete_xonfig( - CompletionContext( - CommandContext(args=(CommandArg("xonfig"),), arg_index=1, prefix="-") - ) - ) == {"-h"} +@pytest.mark.parametrize( + "args, prefix, exp", + [ + ( + "xonfig", + "-", + {"-h", "--help"}, + ), + ( + "xonfig colors", + "b", + {"blue", "brown"}, + ), + ], +) +def test_xonfig(args, prefix, exp, xsh_with_aliases, monkeypatch, check_completer): + from xonsh import xonfig - -def test_xonfig_colors(monkeypatch): - monkeypatch.setattr(xt, "color_style_names", lambda: ["blue", "brown", "other"]) - assert ( - complete_xonfig( - CompletionContext( - CommandContext( - args=(CommandArg("xonfig"), CommandArg("colors")), - arg_index=2, - prefix="b", - ) - ) - ) - == {"blue", "brown"} - ) + monkeypatch.setattr(xonfig, "color_style_names", lambda: ["blue", "brown", "other"]) + assert check_completer(args, prefix=prefix) == exp def test_xontrib(): diff --git a/tests/conftest.py b/tests/conftest.py index 6e41b81d5..17e2eb700 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -141,6 +141,15 @@ def xession(xonsh_builtins) -> XonshSession: return XSH +@pytest.fixture +def xsh_with_aliases(xession, monkeypatch): + from xonsh.aliases import Aliases, make_default_aliases + + xsh = xession + monkeypatch.setattr(xsh, "aliases", Aliases(make_default_aliases())) + return xsh + + @pytest.fixture(scope="session") def completion_context_parse(): return CompletionContextParser().parse diff --git a/tests/test_xonfig.py b/tests/test_xonfig.py index 8110c2343..7b95e748c 100644 --- a/tests/test_xonfig.py +++ b/tests/test_xonfig.py @@ -13,7 +13,7 @@ import json import pytest # noqa F401 from xonsh.tools import ON_WINDOWS -from xonsh.xonfig import XONFIG_MAIN_ACTIONS, xonfig_main +from xonsh.xonfig import xonfig_main def test_xonfg_help(capsys, xonsh_builtins): @@ -25,8 +25,15 @@ def test_xonfg_help(capsys, xonsh_builtins): m = pat.match(capout) assert m[1] verbs = set(v.strip().lower() for v in m[1].split(",")) - exp = set(v.lower() for v in XONFIG_MAIN_ACTIONS) - assert verbs == exp + assert verbs == { + "jupyter-kernel", + "info", + "styles", + "wizard", + "web", + "colors", + "tutorial", + } @pytest.mark.parametrize( diff --git a/xonsh/aliases.py b/xonsh/aliases.py index 2d278b4b9..a564ce3c2 100644 --- a/xonsh/aliases.py +++ b/xonsh/aliases.py @@ -703,11 +703,12 @@ def xexec(args, stdin=None): ) -def xonfig(args, stdin=None): +@lazyobject +def xonfig(): """Runs the xonsh configuration utility.""" from xonsh.xonfig import xonfig_main # lazy import - return xonfig_main(args) + return xonfig_main @unthreadable diff --git a/xonsh/cli_utils.py b/xonsh/cli_utils.py index 624c1a4af..1d2de104e 100644 --- a/xonsh/cli_utils.py +++ b/xonsh/cli_utils.py @@ -297,7 +297,10 @@ class ArgParser(ap.ArgumentParser): doc = get_doc(func) kwargs.setdefault("description", doc) kwargs.setdefault("help", doc) - parser = self.commands.add_parser(kwargs.pop("prog", func.__name__), **kwargs) + name = kwargs.pop("prog", None) + if not name: + name = func.__name__.lstrip("_").replace("_", "-") + parser = self.commands.add_parser(name, **kwargs) add_args(parser, func, allowed_params=args) return parser diff --git a/xonsh/completers/init.py b/xonsh/completers/init.py index 7883203d8..c86f26371 100644 --- a/xonsh/completers/init.py +++ b/xonsh/completers/init.py @@ -16,7 +16,7 @@ from xonsh.completers.commands import ( complete_end_proc_tokens, complete_end_proc_keywords, ) -from xonsh.completers.xompletions import complete_xonfig, complete_xontrib +from xonsh.completers.xompletions import complete_xontrib from xonsh.completers._aliases import complete_argparser_aliases from xonsh.completers.environment import complete_environment_vars @@ -36,7 +36,6 @@ def default_completers(): ("pip", complete_pip), ("cd", complete_cd), ("rmdir", complete_rmdir), - ("xonfig", complete_xonfig), ("xontrib", complete_xontrib), ("import", complete_import), ("bash", complete_from_bash), diff --git a/xonsh/completers/xompletions.py b/xonsh/completers/xompletions.py index eb39afef3..737896f14 100644 --- a/xonsh/completers/xompletions.py +++ b/xonsh/completers/xompletions.py @@ -1,24 +1,9 @@ """Provides completions for xonsh internal utilities""" -from xonsh.parsers.completion_context import CommandContext import xonsh.xontribs as xx import xonsh.xontribs_meta as xmt -import xonsh.tools as xt -from xonsh.xonfig import XONFIG_MAIN_ACTIONS from xonsh.completers.tools import contextual_command_completer_for - - -@contextual_command_completer_for("xonfig") -def complete_xonfig(command: CommandContext): - """Completion for ``xonfig``.""" - curix = command.arg_index - if curix == 1: - possible = set(XONFIG_MAIN_ACTIONS.keys()) | {"-h"} - elif curix == 2 and command.args[1].value == "colors": - possible = set(xt.color_style_names()) - else: - raise StopIteration - return {i for i in possible if i.startswith(command.prefix)} +from xonsh.parsers.completion_context import CommandContext def _list_installed_xontribs(): diff --git a/xonsh/xonfig.py b/xonsh/xonfig.py index bbbe27df4..7da54c6ca 100644 --- a/xonsh/xonfig.py +++ b/xonsh/xonfig.py @@ -9,8 +9,6 @@ import random import pprint import tempfile import textwrap -import argparse -import functools import itertools import contextlib import collections @@ -21,6 +19,7 @@ from xonsh.ply import ply import xonsh.wizard as wiz from xonsh import __version__ as XONSH_VERSION from xonsh.built_ins import XSH +from xonsh.cli_utils import ArgParserAlias, Annotated, Arg, add_args from xonsh.prompt.base import is_template_string from xonsh.platform import ( is_readline_available, @@ -445,15 +444,25 @@ def make_xonfig_wizard(default_file=None, confirm=False, no_wizard_file=None): return w -def _wizard(ns): +def _wizard( + rcfile: Annotated[str, Arg("--file")] = None, + confirm: Annotated[bool, Arg("--confirm", action="store_true")] = False, +): + """Launch configurator in terminal + + Parameters + ------- + rcfile + config file location, default=$XONSHRC + confirm + confirm that the wizard should be run. + """ env = XSH.env shell = XSH.shell.shell xonshrcs = env.get("XONSHRC", []) - fname = xonshrcs[-1] if xonshrcs and ns.file is None else ns.file + fname = xonshrcs[-1] if xonshrcs and rcfile is None else rcfile no_wiz = os.path.join(env.get("XONSH_CONFIG_DIR"), "no-wizard") - w = make_xonfig_wizard( - default_file=fname, confirm=ns.confirm, no_wizard_file=no_wiz - ) + w = make_xonfig_wizard(default_file=fname, confirm=confirm, no_wizard_file=no_wiz) tempenv = {"PROMPT": "", "XONSH_STORE_STDOUT": False} pv = wiz.PromptVisitor(w, store_in_history=False, multiline=False) @@ -504,9 +513,18 @@ def _xonfig_format_json(data): return s -def _info(ns): +def _info( + to_json: Annotated[bool, Arg("--json", action="store_true")] = False, +) -> str: + """Displays configuration information + + Parameters + ---------- + to_json + reports results as json + """ env = XSH.env - data = [("xonsh", XONSH_VERSION)] + data: tp.List[tp.Any] = [("xonsh", XONSH_VERSION)] hash_, date_ = githash() if hash_: data.append(("Git SHA", hash_)) @@ -526,7 +544,7 @@ def _info(ns): ) if ON_LINUX: data.append(("distro", linux_distro())) - data.append(("on wsl", bool(ON_WSL))), + data.append(("on wsl", bool(ON_WSL))) if ON_WSL: data.append(("wsl version", 1 if ON_WSL1 else 2)) data.extend( @@ -554,16 +572,25 @@ def _info(ns): data.extend([("xontrib", xontribs_loaded())]) data.extend([("RC file", XSH.rc_files)]) - formatter = _xonfig_format_json if ns.json else _xonfig_format_human + formatter = _xonfig_format_json if to_json else _xonfig_format_human s = formatter(data) return s -def _styles(ns): +def _styles( + to_json: Annotated[bool, Arg("--json", action="store_true")] = False, _stdout=None +): + """Prints available xonsh color styles + + Parameters + ---------- + to_json + reports results as json + """ env = XSH.env curr = env.get("XONSH_COLOR_STYLE") styles = sorted(color_style_names()) - if ns.json: + if to_json: s = json.dumps(styles, sort_keys=True, indent=1) print(s) return @@ -574,7 +601,7 @@ def _styles(ns): else: lines.append(" " + style) s = "\n".join(lines) - print_color(s) + print_color(s, file=_stdout) def _str_colors(cmap, cols): @@ -621,16 +648,29 @@ def _tok_colors(cmap, cols): return toks -def _colors(args): +def xonfig_color_completer(*_, **__): + yield from color_style_names() + + +def _colors( + style: Annotated[str, Arg(nargs="?", completer=xonfig_color_completer)] = None +): + """Preview color style + + Parameters + ---------- + style + name of the style to preview. If not given, current style name is used. + """ columns, _ = shutil.get_terminal_size() - columns -= int(ON_WINDOWS) + columns -= int(bool(ON_WINDOWS)) style_stash = XSH.env["XONSH_COLOR_STYLE"] - if args.style is not None: - if args.style not in color_style_names(): - print("Invalid style: {}".format(args.style)) + if style is not None: + if style not in color_style_names(): + print("Invalid style: {}".format(style)) return - XSH.env["XONSH_COLOR_STYLE"] = args.style + XSH.env["XONSH_COLOR_STYLE"] = style color_map = color_style() akey = next(iter(color_map)) @@ -642,20 +682,46 @@ def _colors(args): XSH.env["XONSH_COLOR_STYLE"] = style_stash -def _tutorial(args): +def _tutorial(): + """Launch tutorial in browser.""" import webbrowser webbrowser.open("http://xon.sh/tutorial.html") -def _web(args): +def _web( + _args, + browser: Annotated[bool, Arg("--no-browser", action="store_false")] = True, +): + """Launch configurator in browser. + + Parameters + ---------- + browser + don't open browser + """ + import subprocess - subprocess.run([sys.executable, "-m", "xonsh.webconfig"] + args.orig_args[1:]) + subprocess.run([sys.executable, "-m", "xonsh.webconfig"] + _args[1:]) -def _jupyter_kernel(args): - """Make xonsh available as a Jupyter kernel.""" +def _jupyter_kernel( + user: Annotated[bool, Arg("--user", action="store_true")] = False, + prefix: Annotated[str, Arg("--prefix")] = None, + root: Annotated[str, Arg("--root")] = None, +): + """Generate xonsh kernel for jupyter. + + Parameters + ---------- + user + Install kernel spec in user config directory. + prefix + Installation prefix for bin, lib, etc. + root + Install relative to this alternate root directory. + """ try: from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel except ImportError as e: @@ -663,9 +729,7 @@ def _jupyter_kernel(args): ksm = KernelSpecManager() - root = args.root - prefix = args.prefix if args.prefix else sys.prefix - user = args.user + prefix = prefix or sys.prefix spec = { "argv": [ sys.executable, @@ -719,87 +783,25 @@ def _jupyter_kernel(args): return 0 -@functools.lru_cache(1) -def _xonfig_create_parser(): - p = argparse.ArgumentParser( - prog="xonfig", description="Manages xonsh configuration." - ) - subp = p.add_subparsers(title="action", dest="action") - info = subp.add_parser( - "info", help=("displays configuration information, " "default action") - ) - info.add_argument( - "--json", action="store_true", default=False, help="reports results as json" - ) - web = subp.add_parser("web", help="Launch configurator in browser.") - web.add_argument( - "--no-browser", - action="store_false", - dest="browser", - default=True, - help="don't open browser", - ) - wiz = subp.add_parser("wizard", help="Launch configurator in terminal") - wiz.add_argument( - "--file", default=None, help="config file location, default=$XONSHRC" - ) - wiz.add_argument( - "--confirm", - action="store_true", - default=False, - help="confirm that the wizard should be run.", - ) - sty = subp.add_parser("styles", help="prints available xonsh color styles") - sty.add_argument( - "--json", action="store_true", default=False, help="reports results as json" - ) - colors = subp.add_parser("colors", help="preview color style") - colors.add_argument( - "style", nargs="?", default=None, help="style to preview, default: " - ) - subp.add_parser("tutorial", help="Launch tutorial in browser.") - kern = subp.add_parser("jupyter-kernel", help="Generate xonsh kernel for jupyter.") - kern.add_argument( - "--user", - action="store_true", - default=False, - help="Install kernel spec in user config directory.", - ) - kern.add_argument( - "--root", - default=None, - help="Install relative to this alternate root directory.", - ) - kern.add_argument( - "--prefix", default=None, help="Installation prefix for bin, lib, etc." - ) +class XonfigAlias(ArgParserAlias): + """Manage xonsh configuration.""" - return p + def build(self): + parser = self.create_parser(prog="xonfig") + # register as default action + add_args(parser, _info, allowed_params=()) + parser.add_command(_info) + parser.add_command(_web) + parser.add_command(_wizard) + parser.add_command(_styles) + parser.add_command(_colors) + parser.add_command(_tutorial) + parser.add_command(_jupyter_kernel) + + return parser -XONFIG_MAIN_ACTIONS = { - "info": _info, - "web": _web, - "wizard": _wizard, - "styles": _styles, - "colors": _colors, - "tutorial": _tutorial, - "jupyter-kernel": _jupyter_kernel, -} - - -def xonfig_main(args=None): - """Main xonfig entry point.""" - if not args or ( - args[0] not in XONFIG_MAIN_ACTIONS and args[0] not in {"-h", "--help"} - ): - args.insert(0, "info") - parser = _xonfig_create_parser() - ns = parser.parse_args(args) - ns.orig_args = args - if ns.action is None: # apply default action - ns = parser.parse_args(["info"] + args) - return XONFIG_MAIN_ACTIONS[ns.action](ns) +xonfig_main = XonfigAlias() @lazyobject