Repository: conda/conda

Issue:
conda xontrib xonsh wrapper
Make a wrapper for xonsh.


Discussion:

Resolving patch:
diff --git a/conda/activate.py b/conda/activate.py
--- a/conda/activate.py
+++ b/conda/activate.py
@@ -6,6 +6,7 @@
 from os.path import abspath, basename, dirname, expanduser, expandvars, isdir, join
 import re
 import sys
+from tempfile import NamedTemporaryFile
 
 try:
     from cytoolz.itertoolz import concatv
@@ -38,11 +39,13 @@ class Activator(object):
     def __init__(self, shell):
         from .base.context import context
         self.context = context
+        self.shell = shell
 
         if shell == 'posix':
             self.pathsep_join = ':'.join
             self.path_conversion = native_path_to_unix
             self.script_extension = '.sh'
+            self.finalizer_extension = None  # don't write to file
 
             self.unset_var_tmpl = 'unset %s'
             self.set_var_tmpl = 'export %s="%s"'
@@ -52,15 +55,17 @@ def __init__(self, shell):
             self.pathsep_join = ':'.join
             self.path_conversion = native_path_to_unix
             self.script_extension = '.csh'
+            self.finalizer_extension = None  # don't write to file
 
             self.unset_var_tmpl = 'unset %s'
             self.set_var_tmpl = 'setenv %s "%s"'
             self.run_script_tmpl = 'source "%s"'
 
         elif shell == 'xonsh':
-            self.pathsep_join = lambda x: "['%s']" % "', '".join(x)
-            self.path_conversion = lambda x: x  # not sure if you want unix paths on windows or not
+            self.pathsep_join = ':'.join
+            self.path_conversion = native_path_to_unix
             self.script_extension = '.xsh'
+            self.finalizer_extension = '.xsh'
 
             self.unset_var_tmpl = 'del $%s'
             self.set_var_tmpl = '$%s = "%s"'
@@ -69,20 +74,34 @@ def __init__(self, shell):
         else:
             raise NotImplementedError()
 
+    def _finalize(self, commands, ext):
+        if ext is None:
+            return '\n'.join(commands)
+        elif ext:
+            with NamedTemporaryFile(suffix=ext, delete=False) as tf:
+                tf.write(ensure_binary('\n'.join(commands)))
+                tf.write(ensure_binary('\n'))
+            return tf.name
+        else:
+            raise NotImplementedError()
+
     def activate(self, name_or_prefix):
-        return '\n'.join(self._make_commands(self.build_activate(name_or_prefix)))
+        return self._finalize(self._yield_commands(self.build_activate(name_or_prefix)),
+                              self.finalizer_extension)
 
     def deactivate(self):
-        return '\n'.join(self._make_commands(self.build_deactivate()))
+        return self._finalize(self._yield_commands(self.build_deactivate()),
+                              self.finalizer_extension)
 
     def reactivate(self):
-        return '\n'.join(self._make_commands(self.build_reactivate()))
+        return self._finalize(self._yield_commands(self.build_reactivate()),
+                              self.finalizer_extension)
 
-    def _make_commands(self, cmds_dict):
-        for key in cmds_dict.get('unset_vars', ()):
+    def _yield_commands(self, cmds_dict):
+        for key in sorted(cmds_dict.get('unset_vars', ())):
             yield self.unset_var_tmpl % key
 
-        for key, value in iteritems(cmds_dict.get('set_vars', {})):
+        for key, value in sorted(iteritems(cmds_dict.get('set_vars', {}))):
             yield self.set_var_tmpl % (key, value)
 
         for script in cmds_dict.get('deactivate_scripts', ()):
@@ -231,7 +250,7 @@ def _get_starting_path_list(self):
             return path.split(os.pathsep)
 
     def _get_path_dirs(self, prefix):
-        if on_win:
+        if on_win:  # pragma: unix no cover
             yield prefix.rstrip("\\")
             yield join(prefix, 'Library', 'mingw-w64', 'bin')
             yield join(prefix, 'Library', 'usr', 'bin')
@@ -256,7 +275,7 @@ def _replace_prefix_in_path(self, old_prefix, new_prefix, starting_path_dirs=Non
             path_list = self._get_starting_path_list()
         else:
             path_list = list(starting_path_dirs)
-        if on_win:
+        if on_win:  # pragma: unix no cover
             # windows has a nasty habit of adding extra Library\bin directories
             prefix_dirs = tuple(self._get_path_dirs(old_prefix))
             try:
@@ -302,17 +321,24 @@ def expand(path):
     return abspath(expanduser(expandvars(path)))
 
 
-def native_path_to_unix(*paths):
+def ensure_binary(value):
+    try:
+        return value.encode('utf-8')
+    except AttributeError:  # pragma: no cover
+        # AttributeError: '<>' object has no attribute 'encode'
+        # In this case assume already binary type and do nothing
+        return value
+
+
+def native_path_to_unix(*paths):  # pragma: unix no cover
     # on windows, uses cygpath to convert windows native paths to posix paths
     if not on_win:
         return paths[0] if len(paths) == 1 else paths
     from subprocess import PIPE, Popen
     from shlex import split
-    command = 'cygpath.exe --path -f -'
+    command = 'cygpath --path -f -'
     p = Popen(split(command), stdin=PIPE, stdout=PIPE, stderr=PIPE)
-    joined = ("%s" % os.pathsep).join(paths)
-    if hasattr(joined, 'encode'):
-        joined = joined.encode('utf-8')
+    joined = ensure_binary(("%s" % os.pathsep).join(paths))
     stdout, stderr = p.communicate(input=joined)
     rc = p.returncode
     if rc != 0 or stderr:
@@ -366,4 +392,4 @@ def main():
 
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())
diff --git a/conda/cli/main.py b/conda/cli/main.py
--- a/conda/cli/main.py
+++ b/conda/cli/main.py
@@ -161,4 +161,4 @@ def main(*args):
 
 
 if __name__ == '__main__':
-    main()
+    sys.exit(main())


Test patch:
diff --git a/tests/test_activate.py b/tests/test_activate.py
--- a/tests/test_activate.py
+++ b/tests/test_activate.py
@@ -3,10 +3,13 @@
 
 from logging import getLogger
 import os
-from os.path import join
+from os.path import join, isdir
 import sys
+from tempfile import gettempdir
 from unittest import TestCase
+from uuid import uuid4
 
+from conda._vendor.auxlib.ish import dals
 import pytest
 
 from conda.activate import Activator
@@ -15,6 +18,7 @@
 from conda.common.io import env_var
 from conda.exceptions import EnvironmentLocationNotFound, EnvironmentNameNotFound
 from conda.gateways.disk.create import mkdir_p
+from conda.gateways.disk.delete import rm_rf
 from conda.gateways.disk.update import touch
 from tests.helpers import tempdir
 
@@ -320,6 +324,93 @@ def test_build_deactivate_shlvl_1(self):
                     assert builder['deactivate_scripts'] == [deactivate_d_1]
 
 
+class ShellWrapperUnitTests(TestCase):
+
+    def setUp(self):
+        tempdirdir = gettempdir()
+
+        prefix_dirname = str(uuid4())[:4] + ' ' + str(uuid4())[:4]
+        self.prefix = join(tempdirdir, prefix_dirname)
+        mkdir_p(join(self.prefix, 'conda-meta'))
+        assert isdir(self.prefix)
+        touch(join(self.prefix, 'conda-meta', 'history'))
+
+    def tearDown(self):
+        rm_rf(self.prefix)
+
+    def make_dot_d_files(self, extension):
+        mkdir_p(join(self.prefix, 'etc', 'conda', 'activate.d'))
+        mkdir_p(join(self.prefix, 'etc', 'conda', 'deactivate.d'))
+
+        touch(join(self.prefix, 'etc', 'conda', 'activate.d', 'ignore.txt'))
+        touch(join(self.prefix, 'etc', 'conda', 'deactivate.d', 'ignore.txt'))
+
+        touch(join(self.prefix, 'etc', 'conda', 'activate.d', 'activate1' + extension))
+        touch(join(self.prefix, 'etc', 'conda', 'deactivate.d', 'deactivate1' + extension))
+
+    @pytest.mark.xfail(on_win, strict=True, reason="native_path_to_unix is broken on appveyor; "
+                                                   "will fix in future PR")
+    def test_xonsh_basic(self):
+        activator = Activator('xonsh')
+        self.make_dot_d_files(activator.script_extension)
+
+        activate_result = activator.activate(self.prefix)
+        with open(activate_result) as fh:
+            activate_data = fh.read()
+        rm_rf(activate_result)
+
+        new_path = activator.pathsep_join(activator._add_prefix_to_path(self.prefix))
+        assert activate_data == dals("""
+        $CONDA_DEFAULT_ENV = "%(prefix)s"
+        $CONDA_PREFIX = "%(prefix)s"
+        $CONDA_PROMPT_MODIFIER = "(%(prefix)s) "
+        $CONDA_PYTHON_EXE = "%(sys_executable)s"
+        $CONDA_SHLVL = "1"
+        $PATH = "%(new_path)s"
+        source "%(activate1)s"
+        """) % {
+            'prefix': self.prefix,
+            'new_path': new_path,
+            'sys_executable': sys.executable,
+            'activate1': join(self.prefix, 'etc', 'conda', 'activate.d', 'activate1.xsh')
+        }
+
+        with env_var('CONDA_PREFIX', self.prefix):
+            with env_var('CONDA_SHLVL', '1'):
+                with env_var('PATH', new_path):
+                    reactivate_result = activator.reactivate()
+                    with open(reactivate_result) as fh:
+                        reactivate_data = fh.read()
+                    rm_rf(reactivate_result)
+
+                    assert reactivate_data == dals("""
+                    source "%(deactivate1)s"
+                    source "%(activate1)s"
+                    """) % {
+                        'activate1': join(self.prefix, 'etc', 'conda', 'activate.d', 'activate1.xsh'),
+                        'deactivate1': join(self.prefix, 'etc', 'conda', 'deactivate.d', 'deactivate1.xsh'),
+                    }
+
+                    deactivate_result = activator.deactivate()
+                    with open(deactivate_result) as fh:
+                        deactivate_data = fh.read()
+                    rm_rf(deactivate_result)
+
+                    new_path = activator.pathsep_join(activator._remove_prefix_from_path(self.prefix))
+                    assert deactivate_data == dals("""
+                    del $CONDA_DEFAULT_ENV
+                    del $CONDA_PREFIX
+                    del $CONDA_PROMPT_MODIFIER
+                    del $CONDA_PYTHON_EXE
+                    $CONDA_SHLVL = "0"
+                    $PATH = "%(new_path)s"
+                    source "%(deactivate1)s"
+                    """) % {
+                        'new_path': new_path,
+                        'deactivate1': join(self.prefix, 'etc', 'conda', 'deactivate.d', 'deactivate1.xsh'),
+                    }
+
+
 @pytest.mark.integration
 class ActivatorIntegrationTests(TestCase):