From c7e5b7b0f94bd55fce125538fc8dcc1d8c955f53 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Sun, 18 Aug 2019 21:09:57 -0400 Subject: [PATCH 01/12] Add autovox xontrib to support automatic vox --- xontrib/autovox.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 xontrib/autovox.py diff --git a/xontrib/autovox.py b/xontrib/autovox.py new file mode 100644 index 000000000..d7240205f --- /dev/null +++ b/xontrib/autovox.py @@ -0,0 +1,102 @@ +""" +A framework for automatic vox. + +This coordinates multiple automatic vox policies and deals with some of the +mechanics of venv searching and chdir handling. + +This provides no interface for end users. + +Developers should look at events.autovox_policy +""" +import itertools +from pathlib import Path +import xontrib.voxapi as voxapi +import warnings + +__all__ = () + + +_policies = [] + +events.doc( + "autovox_policy", + """ +autovox_policy(path: pathlib.Path) -> Union[str, pathlib.Path, None] + +Register a policy with autovox. + +A policy is a function that takes a Path and returns the venv associated with it, +if any. + +NOTE: The policy should only return a venv for this path exactly, not for +parent paths. Parent walking is handled by autovox so that all policies can +be queried at each level. +""", +) + + +class MultipleVenvsWarning(RuntimeWarning): + pass + + +def get_venv(vox, dirpath): + # Search up the directory tree until a venv is found, or none + for path in itertools.chain((dirpath,), dirpath.parents): + print(f"Checking {path}") + venvs = [ + vox[p] + for p in events.autovox_policy.fire(path=path) + if p is not None and p in vox # Filter out venvs that don't exist + ] + if len(venvs) == 0: + continue + else: + if len(venvs) > 1: + warnings.warn( + MultipleVenvsWarning( + f"Found {len(venvs)} venvs for {path}; using the first" + ) + ) + return venvs[0] + + +def check_for_new_venv(curdir): + vox = voxapi.Vox() + try: + oldve = vox[...] + except KeyError: + oldve = None + newve = get_venv(vox, curdir) + + if oldve != newve: + if newve is None: + vox.deactivate() + else: + vox.activate(newve) + + +# Core mechanism: Check for venv when the current directory changes +@events.on_chdir +def cd_handler(newdir, **_): + check_for_new_venv(Path(newdir)) + + +# Recalculate when venvs are created or destroyed + + +@events.vox_on_create +def create_handler(**_): + check_for_new_venv(Path.cwd()) + + +@events.vox_on_destroy +def destroy_handler(**_): + check_for_new_venv(Path.cwd()) + + +# Initial activation before first prompt + + +@events.on_post_init +def load_handler(**_): + check_for_new_venv(Path.cwd()) From 1e81451ccfc4f8b5787c04efa700df3f2791bdaa Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 13:37:46 -0400 Subject: [PATCH 02/12] Add news and docs. --- docs/python_virtual_environments.rst | 16 ++++++++++++++++ news/autovox.rst | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 news/autovox.rst diff --git a/docs/python_virtual_environments.rst b/docs/python_virtual_environments.rst index b744c1101..9ff824b3f 100644 --- a/docs/python_virtual_environments.rst +++ b/docs/python_virtual_environments.rst @@ -102,3 +102,19 @@ Simply add the ``'{env_name}'`` variable to your ``$PROMPT``:: Note that you do **not** need to load the ``vox`` xontrib for this to work. For more details see :ref:`customprompt`. + + +Automatically Switching Environments +------------------------------------ + +Automatic environment switching based on the current directory is managed with the ``autovox`` xontrib (``xontrib load autovox``). Third-party xontribs may register various policies for use with autovox. Pick and choose xontribs that implement policies that match your work style. + +Implementing policies is easy! Just register with the ``autovox_policy`` event and return a ``Path`` if there is a matching venv. For example, this policy implements handling if there is a ``.venv`` directory in the project:: + + @events.autovox_policy + def dotvenv_policy(path, **_): + venv = path / '.venv' + if venv.exists(): + return venv + +Note that you should only return if there is an environment for this directory exactly. Scanning parent directories is managed by autovox. You should also make the policy check relatively cheap. (Local IO is ok, but probably shouldn't call out to network services.) diff --git a/news/autovox.rst b/news/autovox.rst new file mode 100644 index 000000000..39fb7e5cc --- /dev/null +++ b/news/autovox.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added `autovox` xontrib + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 5c184adfc5637269f3bc432ba85185bf1f832b63 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 13:40:13 -0400 Subject: [PATCH 03/12] Add autovox to xontribs.json --- xonsh/xontribs.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xonsh/xontribs.json b/xonsh/xontribs.json index 02c54da8a..46cf5b16b 100644 --- a/xonsh/xontribs.json +++ b/xonsh/xontribs.json @@ -9,6 +9,11 @@ "url": "https://github.com/sagartewari01/autojump-xonsh", "description": ["autojump support for xonsh"] }, + {"name": "autovox", + "package": "xonsh", + "url": "http://xon.sh", + "description": ["Manages automatic activation of virtual environments."] + }, {"name": "autoxsh", "package": "xonsh-autoxsh", "url": "https://github.com/Granitas/xonsh-autoxsh", From cfd59c5a83b9e21d41a2d1fc23c479a25e5d984b Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:03:17 -0400 Subject: [PATCH 04/12] Single grave strikes again! --- news/autovox.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/autovox.rst b/news/autovox.rst index 39fb7e5cc..b7db759c4 100644 --- a/news/autovox.rst +++ b/news/autovox.rst @@ -1,6 +1,6 @@ **Added:** -* Added `autovox` xontrib +* Added ``autovox`` xontrib **Changed:** From cf45a946eb29516395590c3083997e7e184fc136 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:07:27 -0400 Subject: [PATCH 05/12] Fix masked test names --- tests/test_vox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_vox.py b/tests/test_vox.py index 80ee59803..254dcc48c 100644 --- a/tests/test_vox.py +++ b/tests/test_vox.py @@ -159,9 +159,9 @@ else: @skip_if_on_msys @skip_if_on_conda -def test_crud_subdir(xonsh_builtins, tmpdir): +def test_reserved_names(xonsh_builtins, tmpdir): """ - Creates a virtual environment, gets it, enumerates it, and then deletes it. + Tests that reserved words are disallowed. """ xonsh_builtins.__xonsh__.env["VIRTUALENV_HOME"] = str(tmpdir) From f0b49f0c8178bc2bd0519012a330bb98aabc1d82 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:30:04 -0400 Subject: [PATCH 06/12] Add ... support to DummyEnv --- tests/tools.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/tools.py b/tests/tools.py index 1956862e6..08d44486d 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -122,12 +122,17 @@ class DummyEnv(MutableMapping): return {k: str(v) for k, v in self._d.items()} def __getitem__(self, k): - return self._d[k] + if k is ...: + return self + else: + return self._d[k] def __setitem__(self, k, v): + assert k is not ... self._d[k] = v def __delitem__(self, k): + assert k is not ... del self._d[k] def __len__(self): From d4ba3b578985a0422c94d3d26feadba38273827c Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:34:20 -0400 Subject: [PATCH 07/12] Remove print() --- xontrib/autovox.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xontrib/autovox.py b/xontrib/autovox.py index d7240205f..0a95befa8 100644 --- a/xontrib/autovox.py +++ b/xontrib/autovox.py @@ -42,7 +42,6 @@ class MultipleVenvsWarning(RuntimeWarning): def get_venv(vox, dirpath): # Search up the directory tree until a venv is found, or none for path in itertools.chain((dirpath,), dirpath.parents): - print(f"Checking {path}") venvs = [ vox[p] for p in events.autovox_policy.fire(path=path) From 3586da78da7b0e224d081ac41e3d8bb7675641ab Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:50:07 -0400 Subject: [PATCH 08/12] Add is_manually_set() to DummyEnv --- tests/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/tools.py b/tests/tools.py index 08d44486d..77de738e7 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -161,6 +161,9 @@ class DummyEnv(MutableMapping): else: self[k] = v + def is_manually_set(self, key): + return False + # # Execer tools From 82fbac63e35070f49023c21e982e83f7ca14a7eb Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:50:19 -0400 Subject: [PATCH 09/12] Bug in autovox --- xontrib/autovox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xontrib/autovox.py b/xontrib/autovox.py index 0a95befa8..2e7621765 100644 --- a/xontrib/autovox.py +++ b/xontrib/autovox.py @@ -71,7 +71,7 @@ def check_for_new_venv(curdir): if newve is None: vox.deactivate() else: - vox.activate(newve) + vox.activate(newve.env) # Core mechanism: Check for venv when the current directory changes From f8e5e9521bb196b8f3230b7ee9008799797d97a3 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 14:50:27 -0400 Subject: [PATCH 10/12] Write test for autovox --- tests/test_vox.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_vox.py b/tests/test_vox.py index 254dcc48c..fec02d1dd 100644 --- a/tests/test_vox.py +++ b/tests/test_vox.py @@ -177,3 +177,50 @@ def test_reserved_names(xonsh_builtins, tmpdir): vox.create("spameggs/Scripts") else: vox.create("spameggs/bin") + + +@skip_if_on_msys +@skip_if_on_conda +def test_autovox(xonsh_builtins, tmpdir): + """ + Tests that autovox works + """ + import importlib + from xonsh.lib import subprocess + import xonsh.dirstack + + # Set up an isolated venv home + xonsh_builtins.__xonsh__.env["VIRTUALENV_HOME"] = str(tmpdir) + + # Makes sure that event handlers are registered + import xontrib.autovox + importlib.reload(xontrib.autovox) + + # Set up enough environment for xonsh to function + xonsh_builtins.__xonsh__.env['PWD'] = os.getcwd() + xonsh_builtins.__xonsh__.env['DIRSTACK_SIZE'] = 10 + xonsh_builtins.__xonsh__.env['PATH'] = [] + + xonsh_builtins.__xonsh__.env['XONSH_SHOW_TRACEBACK'] = True + + @xonsh_builtins.events.autovox_policy + def policy(path, **_): + print("Checking", repr(path), vox.active()) + if str(path) == str(tmpdir): + return "myenv" + + vox = Vox() + + print(xonsh_builtins.__xonsh__.env['PWD']) + xonsh.dirstack.pushd([str(tmpdir)]) + print(xonsh_builtins.__xonsh__.env['PWD']) + assert vox.active() is None + xonsh.dirstack.popd([]) + print(xonsh_builtins.__xonsh__.env['PWD']) + + vox.create('myenv') + xonsh.dirstack.pushd([str(tmpdir)]) + print(xonsh_builtins.__xonsh__.env['PWD']) + assert vox.active() == 'myenv' + xonsh.dirstack.popd([]) + print(xonsh_builtins.__xonsh__.env['PWD']) From 0b9ab83c860e086bfd6cf370f8afdaa9b62ed458 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 15:02:41 -0400 Subject: [PATCH 11/12] Only run autovox test on Py3.6+ --- tests/test_vox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_vox.py b/tests/test_vox.py index fec02d1dd..7a1a1e057 100644 --- a/tests/test_vox.py +++ b/tests/test_vox.py @@ -6,7 +6,7 @@ import os import pytest from xontrib.voxapi import Vox -from tools import skip_if_on_conda, skip_if_on_msys +from tools import skip_if_on_conda, skip_if_on_msys, skip_if_lt_py36 from xonsh.platform import ON_WINDOWS @@ -181,6 +181,7 @@ def test_reserved_names(xonsh_builtins, tmpdir): @skip_if_on_msys @skip_if_on_conda +@skip_if_lt_py36 def test_autovox(xonsh_builtins, tmpdir): """ Tests that autovox works From 14cf2265eb9f8fad8386d7a94211f5b7937154e3 Mon Sep 17 00:00:00 2001 From: Jamie Bliss Date: Wed, 28 Aug 2019 15:06:36 -0400 Subject: [PATCH 12/12] bump