Merge pull request #3277 from astronouth7303/xontrib-autovox

Add autovox xontrib
This commit is contained in:
Gil Forsyth 2019-09-09 16:32:55 -04:00 committed by GitHub
commit 9acc111d65
Failed to generate hash of commit
6 changed files with 205 additions and 4 deletions

View file

@ -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. Note that you do **not** need to load the ``vox`` xontrib for this to work.
For more details see :ref:`customprompt`. 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.)

23
news/autovox.rst Normal file
View file

@ -0,0 +1,23 @@
**Added:**
* Added ``autovox`` xontrib
**Changed:**
* <news item>
**Deprecated:**
* <news item>
**Removed:**
* <news item>
**Fixed:**
* <news item>
**Security:**
* <news item>

View file

@ -7,7 +7,7 @@ import pytest
import sys import sys
from xontrib.voxapi import Vox 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 from xonsh.platform import ON_WINDOWS
@ -209,9 +209,9 @@ else:
@skip_if_on_msys @skip_if_on_msys
@skip_if_on_conda @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) xonsh_builtins.__xonsh__.env["VIRTUALENV_HOME"] = str(tmpdir)
@ -227,3 +227,51 @@ def test_crud_subdir(xonsh_builtins, tmpdir):
vox.create("spameggs/Scripts") vox.create("spameggs/Scripts")
else: else:
vox.create("spameggs/bin") vox.create("spameggs/bin")
@skip_if_on_msys
@skip_if_on_conda
@skip_if_lt_py36
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'])

View file

@ -122,12 +122,17 @@ class DummyEnv(MutableMapping):
return {k: str(v) for k, v in self._d.items()} return {k: str(v) for k, v in self._d.items()}
def __getitem__(self, k): def __getitem__(self, k):
return self._d[k] if k is ...:
return self
else:
return self._d[k]
def __setitem__(self, k, v): def __setitem__(self, k, v):
assert k is not ...
self._d[k] = v self._d[k] = v
def __delitem__(self, k): def __delitem__(self, k):
assert k is not ...
del self._d[k] del self._d[k]
def __len__(self): def __len__(self):
@ -156,6 +161,9 @@ class DummyEnv(MutableMapping):
else: else:
self[k] = v self[k] = v
def is_manually_set(self, key):
return False
# #
# Execer tools # Execer tools

View file

@ -9,6 +9,11 @@
"url": "https://github.com/sagartewari01/autojump-xonsh", "url": "https://github.com/sagartewari01/autojump-xonsh",
"description": ["autojump support for xonsh"] "description": ["autojump support for xonsh"]
}, },
{"name": "autovox",
"package": "xonsh",
"url": "http://xon.sh",
"description": ["Manages automatic activation of virtual environments."]
},
{"name": "autoxsh", {"name": "autoxsh",
"package": "xonsh-autoxsh", "package": "xonsh-autoxsh",
"url": "https://github.com/Granitas/xonsh-autoxsh", "url": "https://github.com/Granitas/xonsh-autoxsh",

101
xontrib/autovox.py Normal file
View file

@ -0,0 +1,101 @@
"""
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):
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.env)
# 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())