diff --git a/tests/conftest.py b/tests/conftest.py index e69de29bb..06486e729 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,2 @@ +# when xonsh is not installed in the current env +# pytest_plugins = ("xonsh.pytest.plugin",) diff --git a/tests/test_aliases.py b/tests/test_aliases.py index 911b2f047..be7b6474e 100644 --- a/tests/test_aliases.py +++ b/tests/test_aliases.py @@ -6,7 +6,6 @@ import os import pytest from xonsh.aliases import Aliases, ExecAlias -from xonsh.pytest.tools import skip_if_on_windows def cd(args, stdin=None, **kwargs): @@ -52,7 +51,6 @@ def test_eval_recursive(xession): assert ales.get("color_ls") == ["ls", "- -", "--color=true"] -@skip_if_on_windows def test_eval_recursive_callable_partial(xonsh_execer, xession): ales = make_aliases() xession.env["HOME"] = os.path.expanduser("~") diff --git a/tests/test_vox.py b/tests/test_vox.py index 06514496e..72c15a533 100644 --- a/tests/test_vox.py +++ b/tests/test_vox.py @@ -1,4 +1,5 @@ """Vox tests""" +import io import os import pathlib import stat @@ -8,21 +9,75 @@ import types from typing import TYPE_CHECKING import pytest +from py.path import local from xonsh.platform import ON_WINDOWS from xonsh.pytest.tools import skip_if_on_conda, skip_if_on_msys -from xontrib.voxapi import Vox +from xontrib.voxapi import Vox, _get_vox_default_interpreter if TYPE_CHECKING: + from pytest_subprocess import FakeProcess + from xontrib.vox import VoxHandler @pytest.fixture -def vox(xession, tmpdir, load_xontrib) -> "VoxHandler": - """vox Alias function""" - +def venv_home(tmpdir, xession): + """Path where VENVs are created""" + home = tmpdir / "venvs" + home.ensure_dir() # Set up an isolated venv home - xession.env["VIRTUALENV_HOME"] = str(tmpdir) + xession.env["VIRTUALENV_HOME"] = str(home) + return home + + +@pytest.fixture +def venv_proc(fake_process: "FakeProcess", venv_home): + def version_handle(process): + ver = str(sys.version).split()[0] + process.stdout.write(f"Python {ver}") + + def venv_handle(process): + env_path = local(process.args[3]) + (env_path / "lib").ensure_dir() + bin_path = env_path / ("Scripts" if ON_WINDOWS else "bin") + + (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) + + for pip_name in ["pip", "pip.exe"]: + fake_process.register( + [str(bin_path / pip_name), "freeze", fake_process.any()], stdout="" + ) + + # will be used by `vox runin` + fake_process.register( + [pip_name, "--version"], + stdout=f"pip 22.0.4 from {env_path}/lib/python3.10/site-packages/pip (python 3.10)", + ) + fake_process.keep_last_process(True) + return env_path + + def get_interpreters(): + interpreter = _get_vox_default_interpreter() + yield interpreter + if sys.executable != interpreter: + yield sys.executable + + for cmd in get_interpreters(): + fake_process.register([cmd, "--version"], callback=version_handle) + venv = (cmd, "-m", "venv") + fake_process.register([*venv, fake_process.any(min=1)], callback=venv_handle) + fake_process.keep_last_process(True) + return fake_process + + +@pytest.fixture +def vox(xession, load_xontrib, venv_proc) -> "VoxHandler": + """vox Alias function""" # Set up enough environment for xonsh to function xession.env["PWD"] = os.getcwd() @@ -31,8 +86,8 @@ def vox(xession, tmpdir, load_xontrib) -> "VoxHandler": xession.env["XONSH_SHOW_TRACEBACK"] = True load_xontrib("vox") - - yield xession.aliases["vox"] + vox = xession.aliases["vox"] + return vox @pytest.fixture @@ -55,46 +110,67 @@ def record_events(xession): yield Listener() -@skip_if_on_msys -@skip_if_on_conda -def test_vox_flow(xession, vox, record_events, tmpdir): +def test_vox_flow(xession, vox, record_events, venv_home): """ Creates a virtual environment, gets it, enumerates it, and then deletes it. """ - xession.env["VIRTUALENV_HOME"] = str(tmpdir) record_events( "vox_on_create", "vox_on_delete", "vox_on_activate", "vox_on_deactivate" ) vox(["create", "spam"]) - assert stat.S_ISDIR(tmpdir.join("spam").stat().mode) + assert stat.S_ISDIR(venv_home.join("spam").stat().mode) assert record_events.last == ("vox_on_create", "spam") ve = vox.vox["spam"] - assert ve.env == str(tmpdir.join("spam")) + assert ve.env == str(venv_home.join("spam")) assert os.path.isdir(ve.bin) assert "spam" in vox.vox assert "spam" in list(vox.vox) - # activate/deactivate + # activate vox(["activate", "spam"]) assert xession.env["VIRTUAL_ENV"] == vox.vox["spam"].env assert record_events.last == ("vox_on_activate", "spam", str(ve.env)) - vox.deactivate() + + out = io.StringIO() + # info + vox(["info"], stdout=out) + assert "spam" in out.getvalue() + out.seek(0) + + # list + vox(["list"], stdout=out) + print(out.getvalue()) + assert "spam" in out.getvalue() + out.seek(0) + + # wipe + vox(["wipe"], stdout=out) + print(out.getvalue()) + assert "Nothing to remove" in out.getvalue() + out.seek(0) + + # deactivate + vox(["deactivate"]) assert "VIRTUAL_ENV" not in xession.env assert record_events.last == ("vox_on_deactivate", "spam", str(ve.env)) + # runin + vox(["runin", "spam", "pip", "--version"], stdout=out) + print(out.getvalue()) + assert "spam" in out.getvalue() + out.seek(0) + # removal vox(["rm", "spam", "--force"]) - assert not tmpdir.join("spam").check() + assert not venv_home.join("spam").check() assert record_events.last == ("vox_on_delete", "spam") -@skip_if_on_msys -@skip_if_on_conda -def test_activate_non_vox_venv(xession, vox, record_events, tmpdir): +def test_activate_non_vox_venv(xession, vox, record_events, venv_proc, venv_home): """ Create a virtual environment using Python's built-in venv module (not in VIRTUALENV_HOME) and verify that vox can activate it correctly. @@ -103,7 +179,7 @@ def test_activate_non_vox_venv(xession, vox, record_events, tmpdir): record_events("vox_on_activate", "vox_on_deactivate") - with tmpdir.as_cwd(): + with venv_home.as_cwd(): venv_dirname = "venv" sp.run([sys.executable, "-m", "venv", venv_dirname]) vox(["activate", venv_dirname]) @@ -117,7 +193,7 @@ def test_activate_non_vox_venv(xession, vox, record_events, tmpdir): assert record_events.last == ( "vox_on_activate", venv_dirname, - str(pathlib.Path(str(tmpdir)) / venv_dirname), + str(pathlib.Path(str(venv_home)) / venv_dirname), ) vox(["deactivate"]) @@ -126,13 +202,13 @@ def test_activate_non_vox_venv(xession, vox, record_events, tmpdir): assert record_events.last == ( "vox_on_deactivate", venv_dirname, - str(pathlib.Path(str(tmpdir)) / venv_dirname), + str(pathlib.Path(str(venv_home)) / venv_dirname), ) @skip_if_on_msys @skip_if_on_conda -def test_path(xession, vox, tmpdir, a_venv): +def test_path(xession, vox, a_venv): """ Test to make sure Vox properly activates and deactivates by examining $PATH """ @@ -146,20 +222,17 @@ def test_path(xession, vox, tmpdir, a_venv): assert oldpath == xession.env["PATH"] -@skip_if_on_msys -@skip_if_on_conda -def test_crud_subdir(xession, tmpdir): +def test_crud_subdir(xession, venv_home, venv_proc): """ Creates a virtual environment, gets it, enumerates it, and then deletes it. """ - xession.env["VIRTUALENV_HOME"] = str(tmpdir) vox = Vox(force_removals=True) vox.create("spam/eggs") - assert stat.S_ISDIR(tmpdir.join("spam", "eggs").stat().mode) + assert stat.S_ISDIR(venv_home.join("spam", "eggs").stat().mode) ve = vox["spam/eggs"] - assert ve.env == str(tmpdir.join("spam", "eggs")) + assert ve.env == str(venv_home.join("spam", "eggs")) assert os.path.isdir(ve.bin) assert "spam/eggs" in vox @@ -170,16 +243,14 @@ def test_crud_subdir(xession, tmpdir): del vox["spam/eggs"] - assert not tmpdir.join("spam", "eggs").check() + assert not venv_home.join("spam", "eggs").check() -@skip_if_on_msys -@skip_if_on_conda -def test_crud_path(xession, tmpdir): +def test_crud_path(xession, tmpdir, venv_proc): """ Creates a virtual environment, gets it, enumerates it, and then deletes it. """ - tmp = pathlib.Path(str(tmpdir)) + tmp = str(tmpdir) vox = Vox(force_removals=True) vox.create(tmp) @@ -217,9 +288,7 @@ def test_reserved_names(xession, tmpdir): @pytest.mark.parametrize("registered", [False, True]) -@skip_if_on_msys -@skip_if_on_conda -def test_autovox(xession, tmpdir, vox, a_venv, load_xontrib, registered): +def test_autovox(xession, vox, a_venv, load_xontrib, registered): """ Tests that autovox works """ @@ -248,39 +317,33 @@ def test_autovox(xession, tmpdir, vox, a_venv, load_xontrib, registered): @pytest.fixture -def venv_home(tmpdir): - """Path where VENVs are created""" - return tmpdir +def create_venv(venv_proc): + vox = Vox(force_removals=True) + + def wrapped(name): + vox.create(name) + return local(vox[name].env) + + return wrapped @pytest.fixture -def venvs(venv_home): +def venvs(venv_home, create_venv): """Create virtualenv with names venv0, venv1""" from xonsh.dirstack import popd, pushd pushd([str(venv_home)]) - paths = [] - for idx in range(2): - env_path = venv_home / f"venv{idx}" - bin_path = env_path / ("Scripts" if ON_WINDOWS else "bin") - paths.append(env_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 + yield [create_venv(f"venv{idx}") for idx in range(2)] popd([]) @pytest.fixture -def a_venv(venvs): - return venvs[0] +def a_venv(create_venv): + return create_venv("venv0") @pytest.fixture -def patched_cmd_cache(xession, vox, venvs, monkeypatch): +def patched_cmd_cache(xession, vox, monkeypatch): cc = xession.commands_cache def no_change(self, *_): @@ -289,7 +352,7 @@ def patched_cmd_cache(xession, vox, venvs, monkeypatch): monkeypatch.setattr(cc, "_check_changes", types.MethodType(no_change, cc)) monkeypatch.setattr(cc, "_update_cmds_cache", types.MethodType(no_change, cc)) bins = {path: (path, False) for path in _PY_BINS} - cc._cmds_cache.update(bins) + cc._cmds_cache = bins yield cc @@ -308,7 +371,6 @@ _VOX_NEW_OPTS = { "--system-site-packages", "--without-pip", }.union(_HELP_OPTS) -_VOX_NEW_EXP = _PY_BINS.union(_VOX_NEW_OPTS) if ON_WINDOWS: _VOX_NEW_OPTS.add("--symlinks") @@ -318,77 +380,92 @@ else: _VOX_RM_OPTS = {"-f", "--force"}.union(_HELP_OPTS) -@pytest.mark.parametrize( - "args, positionals, opts", - [ - ( - "vox", - { - "delete", - "new", - "remove", - "del", - "workon", - "list", - "exit", - "info", - "ls", - "rm", - "deactivate", - "activate", - "enter", - "create", - "project-get", - "project-set", - "runin", - "runin-all", - "toggle-ssp", - "wipe", - "upgrade", - }, - _HELP_OPTS, - ), - ( - "vox create", - set(), - _VOX_NEW_OPTS.union( +class TestVoxCompletions: + @pytest.fixture + def check(self, check_completer, xession, vox): + def wrapped(cmd, positionals, options=None): + for k in list(xession.completers): + if k != "alias": + xession.completers.pop(k) + assert check_completer(cmd) == positionals + xession.env["ALIAS_COMPLETIONS_OPTIONS_BY_DEFAULT"] = True + if options: + assert check_completer(cmd) == positionals.union(options) + + return wrapped + + @pytest.mark.parametrize( + "args, positionals, opts", + [ + ( + "vox", { - "-a", - "--activate", - "--wp", - "--without-pip", - "-p", - "--interpreter", - "-i", - "--install", - "-l", - "--link", - "--link-project", - "-r", - "--requirements", - "-t", - "--temp", - "--prompt", - } + "delete", + "new", + "remove", + "del", + "workon", + "list", + "exit", + "info", + "ls", + "rm", + "deactivate", + "activate", + "enter", + "create", + "project-get", + "project-set", + "runin", + "runin-all", + "toggle-ssp", + "wipe", + "upgrade", + }, + _HELP_OPTS, ), - ), - ("vox activate", _VENV_NAMES, _HELP_OPTS.union({"-n", "--no-cd"})), - ("vox rm", _VENV_NAMES, _VOX_RM_OPTS), - ("vox rm venv1", _VENV_NAMES, _VOX_RM_OPTS), # pos nargs: one or more - ("vox rm venv1 venv2", _VENV_NAMES, _VOX_RM_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) + ( + "vox create", + set(), + _VOX_NEW_OPTS.union( + { + "-a", + "--activate", + "--wp", + "--without-pip", + "-p", + "--interpreter", + "-i", + "--install", + "-l", + "--link", + "--link-project", + "-r", + "--requirements", + "-t", + "--temp", + "--prompt", + } + ), + ), + ("vox activate", _VENV_NAMES, _HELP_OPTS.union({"-n", "--no-cd"})), + ("vox rm", _VENV_NAMES, _VOX_RM_OPTS), + ("vox rm venv1", _VENV_NAMES, _VOX_RM_OPTS), # pos nargs: one or more + ("vox rm venv1 venv2", _VENV_NAMES, _VOX_RM_OPTS), # pos nargs: two or more + ], + ) + def test_vox_commands(self, args, positionals, opts, check, venvs): + check(args, positionals, opts) + + @pytest.mark.parametrize( + "args", + [ + "vox new --activate --interpreter", # option after option + "vox new --interpreter", # "option: first + "vox new --activate env1 --interpreter", # option after pos + "vox new env1 --interpreter", # "option: at end" + "vox new env1 --interpreter=", # "option: at end with + ], + ) + def test_interpreter(self, check, args, patched_cmd_cache): + check(args, _PY_BINS) diff --git a/xonsh/cli_utils.py b/xonsh/cli_utils.py index e4e891cb1..5b80be1ae 100644 --- a/xonsh/cli_utils.py +++ b/xonsh/cli_utils.py @@ -582,6 +582,14 @@ class ArgParserAlias: For usage please check ``xonsh.completers.completer.py`` module. """ + class Error(Exception): + """Special case, when raised, the traceback will not be shown. + Instead the process with exit with error code and message""" + + def __init__(self, message: str, errno=1): + super().__init__(message) + self.errno = errno + def __init__(self, threadable=True, **kwargs) -> None: if not threadable: from xonsh.tools import unthreadable @@ -646,11 +654,11 @@ class ArgParserAlias: def err(self, *args, **kwargs): """Write text to error stream""" - return self.write_to("stderr", *args, **kwargs) + self.write_to("stderr", *args, **kwargs) def out(self, *args, **kwargs): """Write text to output stream""" - return self.write_to("stdout", *args, **kwargs) + self.write_to("stdout", *args, **kwargs) def __call__( self, @@ -664,22 +672,26 @@ class ArgParserAlias: ): self.stdout = stdout self.stderr = stderr - result = dispatch( - self.parser, - args, - _parser=self.parser, - _args=args, - _stdin=stdin, - _stdout=stdout, - _stderr=stderr, - _spec=spec, - _stack=stack, - **kwargs, - ) - - # free the reference to input/output. Otherwise it will result in errors - self.stdout = None - self.stderr = None + try: + result = dispatch( + self.parser, + args, + _parser=self.parser, + _args=args, + _stdin=stdin, + _stdout=stdout, + _stderr=stderr, + _spec=spec, + _stack=stack, + **kwargs, + ) + except self.Error as ex: + self.err(f"Error: {ex}") + sys.exit(getattr(ex, "errno", 1)) + finally: + # free the reference to input/output. Otherwise it will result in errors + self.stdout = None + self.stderr = None return result diff --git a/xontrib/vox.py b/xontrib/vox.py index 2a994f598..880bff0e8 100644 --- a/xontrib/vox.py +++ b/xontrib/vox.py @@ -1,7 +1,6 @@ """Python virtual environment manager for xonsh.""" import os.path import subprocess -import sys import tempfile import typing as tp from pathlib import Path @@ -11,6 +10,7 @@ import xontrib.voxapi as voxapi from xonsh.built_ins import XSH from xonsh.dirstack import pushd_fn from xonsh.platform import ON_WINDOWS +from xonsh.tools import XonshError __all__ = () @@ -119,7 +119,7 @@ class VoxHandler(xcli.ArgParserAlias): Provides an alternative prompt prefix for this environment. """ - print("Creating environment...") + self.out("Creating environment...") if temporary: path = tempfile.mkdtemp(prefix=f"vox-env-{name}") @@ -150,9 +150,9 @@ class VoxHandler(xcli.ArgParserAlias): if activate: self.activate(name) - print(f"Environment {name!r} created and activated.\n") + self.out(f"Environment {name!r} created and activated.\n") else: - print( + self.out( f'Environment {name!r} created. Activate it with "vox activate {name}".\n' ) @@ -179,12 +179,11 @@ class VoxHandler(xcli.ArgParserAlias): try: self.vox.activate(name) except KeyError: - self.parser.error( - f'This environment doesn\'t exist. Create it with "vox new {name}".\n', + raise self.Error( + f'This environment doesn\'t exist. Create it with "vox new {name}"', ) - return None - print(f'Activated "{name}".\n') + self.out(f'Activated "{name}".\n') if not no_cd: project_dir = self._get_project_dir(name) if project_dir: @@ -202,16 +201,16 @@ class VoxHandler(xcli.ArgParserAlias): """ if self.vox.active() is None: - self.parser.error( + raise self.Error( 'No environment currently active. Activate one with "vox activate".\n', ) env_name = self.vox.deactivate() if remove: self.vox.force_removals = force del self.vox[env_name] - print(f'Environment "{env_name}" deactivated and removed.\n') + self.out(f'Environment "{env_name}" deactivated and removed.\n') else: - print(f'Environment "{env_name}" deactivated.\n') + self.out(f'Environment "{env_name}" deactivated.\n') def list(self): """List available virtual environments.""" @@ -219,16 +218,15 @@ class VoxHandler(xcli.ArgParserAlias): try: envs = sorted(self.vox.keys()) except PermissionError: - self.parser.error("No permissions on VIRTUALENV_HOME") - return None + raise self.Error("No permissions on VIRTUALENV_HOME") if not envs: - self.parser.error( + raise self.Error( 'No environments available. Create one with "vox new".\n', ) - print("Available environments:") - print("\n".join(envs)) + self.out("Available environments:") + self.out("\n".join(envs)) def remove( self, @@ -253,28 +251,24 @@ class VoxHandler(xcli.ArgParserAlias): try: del self.vox[name] except voxapi.EnvironmentInUse: - self.parser.error( + raise self.Error( f'The "{name}" environment is currently active. ' 'In order to remove it, deactivate it first with "vox deactivate".\n', ) except KeyError: - self.parser.error(f'"{name}" environment doesn\'t exist.\n') + raise self.Error(f'"{name}" environment doesn\'t exist.\n') else: - print(f'Environment "{name}" removed.') - print() + self.out(f'Environment "{name}" removed.') + self.out() def _in_venv(self, env_dir: str, command: str, *args, **kwargs): - env = XSH.env.detype() - env["VIRTUAL_ENV"] = env_dir - env["PATH"] = os.pathsep.join( - [ - os.path.join(env_dir, self.vox.sub_dirs[0]), - env["PATH"], - ] - ) + env = {**XSH.env.detype(), "VIRTUAL_ENV": env_dir} + + bin_path = os.path.join(env_dir, self.vox.sub_dirs[0]) + env["PATH"] = os.pathsep.join([bin_path, env["PATH"]]) + for key in ("PYTHONHOME", "__PYVENV_LAUNCHER__"): - if key in env: - del env[key] + env.pop(key, None) try: return subprocess.check_call( @@ -284,7 +278,7 @@ class VoxHandler(xcli.ArgParserAlias): # won't inherit the PATH except OSError as e: if e.errno == 2: - self.parser.error(f"Unable to find {command}") + raise self.Error(f"Unable to find {command}") raise def runin( @@ -310,7 +304,7 @@ class VoxHandler(xcli.ArgParserAlias): """ env_dir = self._get_env_dir(venv) if not args: - self.parser.error("No command is passed") + raise self.Error("No command is passed") self._in_venv(env_dir, *args) def runin_all( @@ -326,19 +320,18 @@ class VoxHandler(xcli.ArgParserAlias): """ errors = False for env in self.vox: - print("\n%s:" % env) + self.out("\n%s:" % env) try: self.runin(env, *args) except subprocess.CalledProcessError as e: errors = True - print(e, file=sys.stderr) - sys.exit(errors) + self.err(e) + self.parser.exit(errors) def _sitepackages_dir(self, venv_path: str): env_python = self.vox.get_binary_path("python", venv_path) if not os.path.exists(env_python): - self.parser.error("ERROR: no virtualenv active") - return + raise self.Error("no virtualenv active") return Path( subprocess.check_output( @@ -352,17 +345,18 @@ class VoxHandler(xcli.ArgParserAlias): ) def _get_env_dir(self, venv=None): - venv = venv or "..." + venv = venv or ... try: env_dir = self.vox[venv].env except KeyError: # check whether the venv is a valid path to an environment - if os.path.exists(venv) and os.path.exists( - self.vox.get_binary_path("python", venv) + if ( + isinstance(venv, str) + and os.path.exists(venv) + and os.path.exists(self.vox.get_binary_path("python", venv)) ): return venv - self.parser.error("No virtualenv is found") - return + raise XonshError("No virtualenv is found") return env_dir def toggle_ssp(self): @@ -374,10 +368,10 @@ class VoxHandler(xcli.ArgParserAlias): ngsp_file = site.parent / "no-global-site-packages.txt" if ngsp_file.exists(): ngsp_file.unlink() - print("Enabled global site-packages") + self.out("Enabled global site-packages") else: with ngsp_file.open("w"): - print("Disabled global site-packages") + self.out("Disabled global site-packages") def project_set( self, @@ -397,9 +391,9 @@ class VoxHandler(xcli.ArgParserAlias): project = os.path.abspath(project_path or ".") if not os.path.exists(env_dir): - self.parser.error(f"Environment '{env_dir}' doesn't exist.") + raise self.Error(f"Environment '{env_dir}' doesn't exist.") if not os.path.isdir(project): - self.parser.error(f"{project} does not exist") + raise self.Error(f"{project} does not exist") project_file = self._get_project_file() project_file.write_text(project) @@ -428,10 +422,10 @@ class VoxHandler(xcli.ArgParserAlias): """ project_dir = self._get_project_dir(venv) if project_dir: - print(project_dir) + self.out(project_dir) else: project_file = self._get_project_file(venv) - self.parser.error( + raise self.Error( f"Corrupted or outdated: {project_file}\nDirectory: {project_dir} doesn't exist." ) @@ -454,11 +448,11 @@ class VoxHandler(xcli.ArgParserAlias): ignored = sorted(all_pkgs - pkgs) to_remove = {p.split("==")[0] for p in pkgs} if to_remove: - print("Ignoring:\n %s" % "\n ".join(ignored)) - print("Uninstalling packages:\n %s" % "\n ".join(to_remove)) + self.out("Ignoring:\n %s" % "\n ".join(ignored)) + self.out("Uninstalling packages:\n %s" % "\n ".join(to_remove)) return subprocess.run([pip_bin, "uninstall", "-y", *to_remove]) else: - print("Nothing to remove") + self.out("Nothing to remove") def info(self, venv: _venv_option = None): """Prints the path for the supplied env @@ -468,7 +462,7 @@ class VoxHandler(xcli.ArgParserAlias): venv name of the venv """ - print(self.vox[venv or ...]) + self.out(self.vox[venv or ...]) def upgrade( self, @@ -498,7 +492,7 @@ class VoxHandler(xcli.ArgParserAlias): venv = self.vox.upgrade( name or ..., symlinks=symlinks, with_pip=with_pip, interpreter=interpreter ) - print(venv) + self.out(venv) XSH.aliases["vox"] = VoxHandler()