argparserAlias - vox (#4437)

* feat: use argparser-alias for vox

* feat: vox.create --interpreter completions

* refactor: use func based completer

* fix: implement interpreter completion

* docs:

* refactor: update import path of cli_utils

* style: convert to f-strings

* fix: failing tests

* fix: failing tests

completer from base_completer appears
This commit is contained in:
Noorhteen Raja NJ 2021-10-13 19:32:06 +05:30 committed by GitHub
parent 1780ba63e3
commit a3c1b2429e
Failed to generate hash of commit
4 changed files with 290 additions and 161 deletions

23
news/ap-vox.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* <news item>
**Changed:**
* improve ``vox`` CLI completions
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -111,6 +111,7 @@ def xonsh_builtins(monkeypatch, xonsh_events, session_vars):
cc = XSH.commands_cache
monkeypatch.setattr(cc, "locate_binary", types.MethodType(locate_binary, cc))
monkeypatch.setattr(cc, "_cmds_cache", {})
for attr, val in [
("evalx", eval),

View file

@ -3,6 +3,8 @@ import pathlib
import stat
import os
import subprocess as sp
import types
import pytest
import sys
from xontrib.voxapi import Vox
@ -282,3 +284,126 @@ def test_autovox(xession, tmpdir, load_vox):
assert vox.active() == "myenv"
xonsh.dirstack.popd([])
print(xession.env["PWD"])
@pytest.fixture
def venv_home(tmpdir):
"""Path where VENVs are created"""
return tmpdir
@pytest.fixture
def venvs(venv_home):
"""create bin paths in the tmpdir"""
from xonsh.dirstack import pushd, popd
pushd([str(venv_home)])
paths = []
for idx in range(2):
bin_path = venv_home / f"venv{idx}" / "bin"
paths.append(bin_path)
(bin_path / "python").write("", ensure=True)
(bin_path / "python.exe").write("", ensure=True)
for file in bin_path.listdir():
st = os.stat(str(file))
os.chmod(str(file), st.st_mode | stat.S_IEXEC)
yield paths
popd([])
@pytest.fixture
def patched_cmd_cache(xession, load_vox, venvs, monkeypatch):
cc = xession.commands_cache
def no_change(self, *_):
return False, False, False
monkeypatch.setattr(cc, "_update_if_changed", types.MethodType(no_change, cc))
monkeypatch.setattr(cc, "_update_cmds_cache", types.MethodType(no_change, cc))
monkeypatch.setattr(cc, "cache_file", None)
bins = {path: (path, False) for path in _PY_BINS}
cc._cmds_cache.update(bins)
yield cc
_VENV_NAMES = {"venv1", "venv1/", "venv0/", "venv0"}
if ON_WINDOWS:
_VENV_NAMES = {"venv1\\", "venv0\\"}
_HELP_OPTS = {
"-h",
"--help",
}
_PY_BINS = {"/bin/python2", "/bin/python3"}
_VOX_NEW_OPTS = {
"--copies",
"--help",
"-h",
"--ssp",
"--symlinks",
"--system-site-packages",
"--without-pip",
}
_VOX_NEW_EXP = _PY_BINS.union(_VOX_NEW_OPTS)
@pytest.mark.parametrize(
"args, positionals, opts",
[
(
"vox",
{
"delete",
"new",
"remove",
"del",
"workon",
"list",
"exit",
"ls",
"rm",
"deactivate",
"activate",
"enter",
"create",
},
_HELP_OPTS,
),
(
"vox create",
set(),
{
"--copies",
"--symlinks",
"--ssp",
"--system-site-packages",
"--activate",
"--without-pip",
"--interpreter",
"-p",
"-a",
"--help",
"-h",
},
),
("vox activate", _VENV_NAMES, _HELP_OPTS),
("vox rm", _VENV_NAMES, _HELP_OPTS),
("vox rm venv1", _VENV_NAMES, _HELP_OPTS), # pos nargs: one or more
("vox rm venv1 venv2", _VENV_NAMES, _HELP_OPTS), # pos nargs: two or more
("vox new --activate --interpreter", _PY_BINS, set()), # option after option
("vox new --interpreter", _PY_BINS, set()), # "option: first
("vox new --activate env1 --interpreter", _PY_BINS, set()), # option after pos
("vox new env1 --interpreter", _PY_BINS, set()), # "option: at end"
("vox new env1 --interpreter=", _PY_BINS, set()), # "option: at end with
],
)
def test_vox_completer(
args, check_completer, positionals, opts, xession, patched_cmd_cache, venv_home
):
xession.env["XONSH_DATA_DIR"] = venv_home
if positionals:
assert check_completer(args) == positionals
xession.env["ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT"] = True
if opts:
assert check_completer(args) == positionals.union(opts)

View file

@ -1,48 +1,41 @@
"""Python virtual environment manager for xonsh."""
import sys
import textwrap
import xonsh.cli_utils as xcli
import xontrib.voxapi as voxapi
import xonsh.lazyasd as lazyasd
from xonsh.built_ins import XSH
__all__ = ()
class VoxHandler:
def venv_names_completer(command, alias: "VoxHandler", **_):
envs = alias.vox.keys()
from xonsh.completers.path import complete_dir
yield from envs
paths, _ = complete_dir(command)
yield from paths
def py_interpreter_path_completer(xsh, **_):
for _, (path, is_alias) in xsh.commands_cache.all_commands.items():
if not is_alias and ("/python" in path or "/pypy" in path):
yield path
class VoxHandler(xcli.ArgParserAlias):
"""Vox is a virtual environment manager for xonsh."""
def parser():
from argparse import ArgumentParser
def build(self):
"""lazily called during dispatch"""
self.vox = voxapi.Vox()
parser = self.create_parser(prog="vox")
parser = ArgumentParser(prog="vox", description=__doc__)
subparsers = parser.add_subparsers(dest="command")
create = subparsers.add_parser(
"new",
# todo: completer for interpreter
create = parser.add_command(
self.new,
aliases=["create"],
help="Create a new virtual environment in $VIRTUALENV_HOME",
)
create.add_argument("name", metavar="ENV", help="The environments to create")
create.add_argument(
"--system-site-packages",
default=False,
action="store_true",
dest="system_site_packages",
help="Give the virtual environment access to the "
"system site-packages dir.",
)
create.add_argument(
"-p",
"--interpreter",
default=None,
help=textwrap.dedent(
"""
The Python interpreter used to create the virtual environment.
Can be configured via the $VOX_DEFAULT_INTERPRETER environment variable.
"""
).strip(),
args=("name", "interpreter", "system_site_packages", "activate"),
)
from xonsh.platform import ON_WINDOWS
@ -75,181 +68,168 @@ class VoxHandler:
"virtual environment (pip is bootstrapped "
"by default)",
)
create.add_argument(
"-a",
"--activate",
default=False,
action="store_true",
dest="activate",
help="Activate the newly created virtual environment.",
)
activate = subparsers.add_parser(
"activate", aliases=["workon", "enter"], 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"
),
)
deactivate = subparsers.add_parser(
"deactivate",
aliases=["exit"],
help="Deactivate current virtual environment",
)
deactivate.add_argument(
"--remove",
dest="remove",
default=False,
action="store_true",
help="Remove the virtual environment after leaving it.",
)
subparsers.add_parser(
"list",
aliases=["ls"],
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"
),
)
subparsers.add_parser("help", help="Show this help message")
parser.add_command(self.activate, aliases=["workon", "enter"])
parser.add_command(self.deactivate, aliases=["exit"])
parser.add_command(self.list, aliases=["ls"])
parser.add_command(self.remove, aliases=["rm", "delete", "del"])
return parser
parser = lazyasd.LazyObject(parser, locals(), "parser")
def new(
self,
name: xcli.Annotated[str, xcli.Arg(metavar="ENV")],
interpreter: xcli.Annotated[
str,
xcli.Arg("-p", "--interpreter", completer=py_interpreter_path_completer),
] = None,
system_site_packages: xcli.Annotated[
bool,
xcli.Arg("--system-site-packages", "--ssp", action="store_true"),
] = False,
symlinks: bool = False,
with_pip: bool = True,
activate: xcli.Annotated[
bool,
xcli.Arg("-a", "--activate", action="store_true"),
] = False,
):
"""Create a virtual environment in $VIRTUALENV_HOME with python3's ``venv``.
aliases = {
"create": "new",
"workon": "activate",
"enter": "activate",
"exit": "deactivate",
"ls": "list",
"rm": "remove",
"delete": "remove",
"del": "remove",
}
def __init__(self):
self.vox = voxapi.Vox()
def __call__(self, args, stdin=None):
"""Call the right handler method for a given command."""
args = self.parser.parse_args(args)
cmd = self.aliases.get(args.command, args.command)
if cmd is None:
self.parser.print_usage()
else:
getattr(self, "cmd_" + cmd)(args, stdin)
def cmd_new(self, args, stdin=None):
"""Create a virtual environment in $VIRTUALENV_HOME with python3's ``venv``."""
Parameters
----------
name : str
Virtual environment name
interpreter: str
Python interpreter used to create the virtual environment.
Can be configured via the $VOX_DEFAULT_INTERPRETER environment variable.
system_site_packages : bool
If True, the system (global) site-packages dir is available to
created environments.
symlinks : bool
If True, attempt to symlink rather than copy files into virtual
environment.
with_pip : bool
If True, ensure pip is installed in the virtual environment. (Default is True)
activate
Activate the newly created virtual environment.
"""
print("Creating environment...")
self.vox.create(
args.name,
system_site_packages=args.system_site_packages,
symlinks=args.symlinks,
with_pip=args.with_pip,
interpreter=args.interpreter,
name,
system_site_packages=system_site_packages,
symlinks=symlinks,
with_pip=with_pip,
interpreter=interpreter,
)
if args.activate:
self.vox.activate(args.name)
print(f"Environment {args.name!r} created and activated.\n")
if activate:
self.vox.activate(name)
print(f"Environment {name!r} created and activated.\n")
else:
print(
f'Environment {args.name!r} created. Activate it with "vox activate {args.name}".\n'
f'Environment {name!r} created. Activate it with "vox activate {name}".\n'
)
def cmd_activate(self, args, stdin=None):
"""Activate a virtual environment."""
def activate(
self,
name: xcli.Annotated[
str,
xcli.Arg(metavar="ENV", nargs="?", completer=venv_names_completer),
] = None,
):
"""Activate a virtual environment.
Parameters
----------
name
The environment to activate.
ENV can be either a name from the venvs shown by ``vox list``
or the path to an arbitrary venv
"""
if name is None:
return self.list()
try:
self.vox.activate(args.name)
self.vox.activate(name)
except KeyError:
print(
'This environment doesn\'t exist. Create it with "vox new %s".\n'
% args.name,
file=sys.stderr,
self.parser.error(
f'This environment doesn\'t exist. Create it with "vox new {name}".\n',
)
return None
else:
print('Activated "%s".\n' % args.name)
print(f'Activated "{name}".\n')
def cmd_deactivate(self, args, stdin=None):
"""Deactivate the active virtual environment."""
def deactivate(
self,
remove: xcli.Annotated[
bool,
xcli.Arg("--remove", action="store_true"),
] = False,
):
"""Deactivate the active virtual environment.
Parameters
----------
remove
Remove the virtual environment after leaving it.
"""
if self.vox.active() is None:
print(
self.parser.error(
'No environment currently active. Activate one with "vox activate".\n',
file=sys.stderr,
)
return None
env_name = self.vox.deactivate()
if args.remove:
if remove:
del self.vox[env_name]
print(f'Environment "{env_name}" deactivated and removed.\n')
else:
print(f'Environment "{env_name}" deactivated.\n')
def cmd_list(self, args, stdin=None):
def list(self):
"""List available virtual environments."""
try:
envs = sorted(self.vox.keys())
except PermissionError:
print("No permissions on VIRTUALENV_HOME")
self.parser.error("No permissions on VIRTUALENV_HOME")
return None
if not envs:
print(
self.parser.error(
'No environments available. Create one with "vox new".\n',
file=sys.stderr,
)
return None
print("Available environments:")
print("\n".join(envs))
def cmd_remove(self, args, stdin=None):
"""Remove virtual environments."""
for name in args.names:
def remove(
self,
names: xcli.Annotated[
list,
xcli.Arg(metavar="ENV", nargs="+", completer=venv_names_completer),
],
):
"""Remove virtual environments.
Parameters
----------
names
The environments to remove. ENV can be either a name from the venvs shown by vox
list or the path to an arbitrary venv
"""
for name in names:
try:
del self.vox[name]
except voxapi.EnvironmentInUse:
print(
'The "%s" environment is currently active. In order to remove it, deactivate it first with "vox deactivate".\n'
% name,
file=sys.stderr,
self.parser.error(
f'The "{name}" environment is currently active. '
'In order to remove it, deactivate it first with "vox deactivate".\n',
)
return
except KeyError:
print('"%s" environment doesn\'t exist.\n' % name, file=sys.stderr)
return
self.parser.error(f'"{name}" environment doesn\'t exist.\n')
else:
print('Environment "%s" removed.' % name)
print(f'Environment "{name}" removed.')
print()
def cmd_help(self, args, stdin=None):
self.parser.print_help()
@classmethod
def handle(cls, args, stdin=None):
"""Runs Vox environment manager."""
vox = cls()
return vox(args, stdin=stdin)
aliases["vox"] = VoxHandler.handle
XSH.aliases["vox"] = VoxHandler()