mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 00:14:41 +01:00
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:
parent
1780ba63e3
commit
a3c1b2429e
4 changed files with 290 additions and 161 deletions
23
news/ap-vox.rst
Normal file
23
news/ap-vox.rst
Normal 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>
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
302
xontrib/vox.py
302
xontrib/vox.py
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue