Repository: xonsh/xonsh
Pull request: Add autovox xontrib
Add a xontrib to support automatic vox xontribs (such as avox).

This deals with all the mechanics of handling events, searching up the directory tree for venvs, and dealing with multiple policies.

Example policy:

```python
@events.autovox_policy
def dotvenv_policy(path, **_):
    venv = path / '.venv'
    if venv.exists():
        return venv
```

The user does need to invoke `xontrib load autovox` to enable this functionality.

Closes #3246 

Problem statement:
Automatic Vox Policies?
One of the re-occuring feature requests for vox is automatic de/activation of venvs, based on various polices (avox is based on a concept of project directory, I think someone did it based on `.venv`, just saw one based on `pyenv-virtualenv` (which is based on the presence of a `.python-version` file).

Wondering if we should refactor `avox` into a generic framework bundled with xonsh, and have these different polices plug in to it.

Hints from issue discussion:
Yeah, that seems like a nice approach.

Commit message: Add autovox xontrib to support automatic vox

Patch to review:
diff --git a/xontrib/autovox.py b/xontrib/autovox.py
new file mode 100644
index 0000000000..d7240205fd
--- /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())


Code review comments:
diff_hunk: @@ -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}")
path: xontrib/autovox.py
text: @user1:
Are we OK with f-strings in xontribs?  I am not oppossed 

@author:
Depends entirely on what versions of Python we're supporting.

f-strings were added in 3.6.

Current PSF status:
* 3.5.7 final: March 18, 2019 (No further plans)
* 3.6.9 final: 2019-07-02
* 3.6.10 and beyond schedule: Security fixes only, as needed, until 2021-12
* 3.7.8 final: 2020-06-27 (expected)
* 3.8.0 final: Monday, 2019-10-21 (expected)

Xonsh is still running tests for 3.5, but that might be more a matter of "hasn't been updated yet" and less "this is a thing we should/need to be doing"

@author:
Oh, and this is a debugging statement that shouldn't even be left in, so the point is moot.

@author:
Ok, this is a debugging statement, but there's another f-string farther down.