├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── README.md ├── news └── TEMPLATE.rst ├── rever.xsh ├── setup.py ├── tests ├── test_import.py └── test_xonda.py └── xontrib └── xonda.xsh /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Rever 3 | rever/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - os: linux 5 | python: 3.5 6 | env: 7 | - MINICONDA_OS="Linux" 8 | - os: linux 9 | python: 3.6 10 | env: 11 | - MINICONDA_OS="Linux" 12 | 13 | before_install: 14 | - URL="https://repo.continuum.io/miniconda/Miniconda3-latest-${MINICONDA_OS}-x86_64.sh"; 15 | wget "${URL}" -O miniconda.sh; 16 | bash miniconda.sh -b -p $HOME/miniconda; 17 | export PATH="$HOME/miniconda/bin:$PATH"; 18 | hash -r; 19 | conda config --set always_yes yes --set changeps1 no; 20 | conda update -q conda; 21 | conda info -a; 22 | 23 | install: 24 | - pip install xonsh pytest 25 | - python setup.py install 26 | 27 | script: 28 | - set -e 29 | - pytest 30 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | xonda Change Log 3 | ================ 4 | 5 | .. current developments 6 | 7 | v0.9.0 8 | ==================== 9 | 10 | **Fixed:** 11 | 12 | * No longer importing deprecated ``conda.install`` 13 | Instead including a modified fork of the symlink code 14 | until upstream xonsh support is solidified 15 | 16 | 17 | 18 | v0.8.0 19 | ==================== 20 | 21 | **Changed:** 22 | 23 | * Updated to use new ``__xonsh__`` api in 0.8.0 24 | 25 | 26 | 27 | 28 | v0.2.5 29 | ==================== 30 | 31 | **Fixed:** 32 | 33 | * Updated ``xonda`` to work with Anaconda >= 4.4.7 34 | 35 | 36 | 37 | 38 | v0.2.4 39 | ==================== 40 | 41 | **Fixed:** 42 | 43 | * Added ``importlib`` import so that ``conda activate`` doesn't break 44 | everything. Stupid bug. 45 | 46 | 47 | 48 | 49 | v0.2.3 50 | ==================== 51 | 52 | **Changed:** 53 | 54 | * ``conda`` imports are now lazy using ``lazyasd`` for a 300ms speedup 55 | in startup time. Woo! 56 | 57 | 58 | 59 | 60 | v0.2.2 61 | ==================== 62 | 63 | **Added:** 64 | 65 | * `conda env create` completions 66 | 67 | 68 | **Changed:** 69 | 70 | * Restored old ``xonda activate`` behavior. Base anaconda is completely removed 71 | from ``$PATH`` on ``activate`` so there isn't accidental cross-env loading 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gil Forsyth 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Don't use this! 2 | 3 | It was great while it lasted(?) but `xonda` is no longer necessary. 4 | Starting with `conda>=4.7.*` there is built-in `xonsh` support. 5 | 6 | To enable `conda activate`, run `conda init xonsh` and it will tack 7 | on the necessary configuration code to your `xonshrc` file and then 8 | `conda activate` and `conda deactivate` should work as you expect. 9 | 10 | Please also make sure to remove any lines akin to `xontrib load xonda` 11 | -- loading two separate `conda activate` methods at once can only lead 12 | to bad things. 13 | 14 | You can remove `xonda` entirely by running `xpip uninstall xonda` or 15 | `conda unininstall xonda` depending on your initial installation method. 16 | 17 | Thanks for using `xonda`! I'll leave this up for any folks who 18 | haven't yet upgraded `conda`, but I highly recommend upgrading `conda`. 19 | 20 | # xonda 21 | 22 | This is a thin wrapper around `conda` for use with 23 | [xonsh](http://xon.sh) 24 | 25 | It provides tab completion for most features and also will tab-complete 26 | activate/select calls for environments. 27 | 28 | ## Prerequisites 29 | 30 | Xonda requires that `conda` is already installed and importable from 31 | xonsh (i.e., `import conda` works). In practice, this probably means 32 | that you need to have installed `xonsh` from `conda` (or at least within 33 | your current `conda` environment). 34 | 35 | You also should have the `conda` `bin/` directory prefixed to your 36 | `$PATH`. 37 | 38 | Recent versions of `conda` suggest to not add the base `conda` `bin/` 39 | directory to your path -- for now, please ignore this suggestion and do 40 | prefix it to your `$PATH` or `xonda` will not work as expected. 41 | 42 | ## Installation 43 | 44 | Just do a 45 | 46 | ```console 47 | pip install xonda 48 | ``` 49 | 50 | or 51 | 52 | ```console 53 | conda install xonda -c conda-forge 54 | ``` 55 | 56 | or you can clone the repo and do 57 | 58 | ```console 59 | pip install . 60 | ``` 61 | 62 | Note that `xonda` only works if it's installed in your base conda environment, so remember to `conda activate base` (in `bash` if necessary) first. 63 | 64 | ## Configuration 65 | 66 | To automatically load xonda at startup, put 67 | 68 | ```console 69 | xontrib load xonda 70 | ``` 71 | 72 | in your `.xonshrc` 73 | 74 | ## Usage 75 | 76 | Xonda will automatically alias itself as `conda`, so you should not see 77 | any differences. 78 | 79 | If `xonda` is installed and activated via 80 | `xontrib load xonda` then `which conda` should 81 | return the alias name "conda" only, instead of 82 | the path to the actual `conda` executable 83 | 84 | **Right** 85 | 86 | ```console 87 | $ which conda 88 | conda 89 | ``` 90 | 91 | **Wrong** (or at least, not activated) 92 | 93 | ```console 94 | $ which conda 95 | /home/user/miniconda3/bin/conda 96 | ``` 97 | 98 | ### Basic commands 99 | 100 | Everything should work the way `conda` always does. So just use it as 101 | you usually do. 102 | 103 | ```console 104 | conda install -c conda-forge xonsh 105 | ``` 106 | 107 | ```console 108 | conda remove python=2.7 109 | ``` 110 | 111 | ### Environment activation 112 | `xonda` provides TAB-completion for conda environments, so you don't have to 113 | keep double-checking. Also, no more `source activate` nonsense. To see a list of 114 | available environments, type 115 | 116 | ```console 117 | conda activate 118 | ``` 119 | 120 | To deactivate, simply type 121 | 122 | ```console 123 | conda deactivate 124 | ``` 125 | 126 | Isn't that simpler? 127 | 128 | If you are already within an environment and `activate` a separate environment, 129 | `xonda` will do you the favor of first deactivating the currently active 130 | environment. 131 | -------------------------------------------------------------------------------- /news/TEMPLATE.rst: -------------------------------------------------------------------------------- 1 | **Added:** None 2 | 3 | **Changed:** None 4 | 5 | **Deprecated:** None 6 | 7 | **Removed:** None 8 | 9 | **Fixed:** None 10 | 11 | **Security:** None 12 | -------------------------------------------------------------------------------- /rever.xsh: -------------------------------------------------------------------------------- 1 | $PROJECT = 'xonda' 2 | $ACTIVITIES = ['version_bump', 'changelog', 'tag', 'push_tag', 'conda_forge', 'ghrelease', 'pypi'] 3 | 4 | $VERSION_BUMP_PATTERNS = [ 5 | ('setup.py', r'version\s*=.*,', "version='$VERSION',") 6 | ] 7 | $CHANGELOG_FILENAME = 'CHANGELOG.rst' 8 | $CHANGELOG_IGNORE = ['TEMPLATE.rst'] 9 | $TAG_REMOTE = 'git@github.com:gforsyth/xonda.git' 10 | 11 | $GITHUB_ORG = 'gforsyth' 12 | $GITHUB_REPO = 'xonda' 13 | 14 | $PYPI_RC = '$HOME/.pypirc' 15 | $PYPI_BUILD_COMMANDS = ('sdist', ) 16 | $PYPI_UPLOAD = True 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | name = 'xonda' 4 | 5 | setup( 6 | name=name, 7 | version='0.9.0', 8 | license='MIT', 9 | url='https://github.com/gforsyth/xonda', 10 | description='A xonsh wrapper around conda', 11 | packages=['xontrib'], 12 | package_dir={'xontrib': 'xontrib'}, 13 | package_data={'xontrib': ['*.xsh']}, 14 | author='Gil Forsyth', 15 | author_email='gilforsyth@gmail.com', 16 | zip_safe=False, 17 | ) 18 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from xonsh.imphooks import XonshImportHook 4 | 5 | sys.meta_path.append(XonshImportHook()) 6 | sys.path.append('./xontrib') 7 | sys.path.append('../xontrib') 8 | 9 | 10 | def test_import(): 11 | import xonda 12 | assert True 13 | -------------------------------------------------------------------------------- /tests/test_xonda.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from xonsh.imphooks import XonshImportHook 4 | 5 | sys.meta_path.append(XonshImportHook()) 6 | sys.path.append('./xontrib') 7 | sys.path.append('../xontrib') 8 | 9 | import xonda 10 | 11 | 12 | def test_list_dirs(tmpdir): 13 | """Dont return hidden dirs when listing""" 14 | p = tmpdir.mkdir("sub") 15 | p = tmpdir.mkdir("another") 16 | p = tmpdir.mkdir(".dotname") 17 | 18 | env_dirs = xonda._list_dirs(tmpdir) 19 | 20 | assert len(tmpdir.listdir()) == 3 21 | 22 | assert len(list(env_dirs)) == 2 23 | -------------------------------------------------------------------------------- /xontrib/xonda.xsh: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | from collections import namedtuple 4 | from os.path import isdir, join, lexists, normcase, normpath 5 | 6 | import xonsh.platform as xp 7 | from xonsh.lazyasd import lazyobject 8 | 9 | _Env = namedtuple('Env', ['name', 'path', 'bin_dir', 'envs_dir']) 10 | 11 | 12 | @lazyobject 13 | def cdelete(): 14 | return importlib.import_module('conda.gateways.disk.delete') 15 | 16 | 17 | def symlink_conda(prefix, root_dir, shell=None): 18 | # forked from https://github.com/conda/conda/blob/78c38a62d77b481eb23411bca3a441b881e61046/conda/install.py 19 | # do not symlink root env - this clobbers activate incorrectly. 20 | # prefix should always be longer than, or outside the root dir. 21 | if normcase(normpath(prefix)) in normcase(normpath(root_dir)): 22 | return 23 | if xp.ON_WINDOWS: 24 | where = 'Scripts' 25 | else: 26 | where = 'bin' 27 | symlink_fn = os.symlink 28 | if not isdir(join(prefix, where)): 29 | os.makedirs(join(prefix, where)) 30 | symlink_conda_hlp(prefix, root_dir, where, symlink_fn) 31 | 32 | 33 | def symlink_conda_hlp(prefix, root_dir, where, symlink_fn): 34 | # forked from https://github.com/conda/conda/blob/78c38a62d77b481eb23411bca3a441b881e61046/conda/install.py 35 | scripts = ["conda", "activate", "deactivate"] 36 | prefix_where = join(prefix, where) 37 | if not isdir(prefix_where): 38 | os.makedirs(prefix_where) 39 | for f in scripts: 40 | root_file = join(root_dir, where, f) 41 | prefix_file = join(prefix_where, f) 42 | # try to kill stale links if they exist 43 | if lexists(prefix_file): 44 | cdelete.rm_rf(prefix_file) 45 | # if they're in use, they won't be killed. Skip making new symlink. 46 | if not lexists(prefix_file): 47 | symlink_fn(root_file, prefix_file) 48 | 49 | 50 | @lazyobject 51 | def config(): 52 | try: 53 | # breaking changes introduced in Anaconda 4.4.7 54 | # try to import newer library structure first 55 | context = importlib.import_module('conda.base.context') 56 | return context.context 57 | except ModuleNotFoundError: 58 | return importlib.import_module('conda.config') 59 | 60 | 61 | def _list_dirs(path): 62 | """ 63 | Generator that lists the directories in a given path. 64 | """ 65 | for entry in os.scandir(path): 66 | if not entry.name.startswith('.') and entry.is_dir(): 67 | yield entry.name 68 | 69 | 70 | def _get_envs(): 71 | """ 72 | Grab a list of all conda env dirs from conda. 73 | """ 74 | # create the list of envrionments 75 | env_list = list() 76 | for envs_dir in config.envs_dirs: 77 | # skip non-existing environments directories 78 | if not os.path.exists(envs_dir): 79 | continue 80 | # for each environment in the environments directory 81 | for env_name in _list_dirs(envs_dir): 82 | # check for duplicates names 83 | if env_name in [env.name for env in env_list]: 84 | raise ValueError('Multiple environments with the same name ' 85 | 'in the system is not supported by xonda.') 86 | # add the environment to the list 87 | env_list.append(_Env(name=env_name, 88 | path=os.path.join(envs_dir, env_name), 89 | bin_dir=os.path.join(envs_dir, env_name, 'bin'), 90 | envs_dir=envs_dir, 91 | )) 92 | 93 | return env_list 94 | 95 | 96 | def _pick_env(env_name): 97 | """ 98 | Select an environment from the detected environments or from another path. 99 | """ 100 | if env_name in [env.name for env in _get_envs()]: 101 | # get the environment 102 | env = next(e for e in _get_envs() if e.name == env_name) 103 | return env 104 | elif os.path.exists(env_name) and "bin" in _list_dirs(env_name): 105 | # if the environment name is non-standard path, i.e. found in 106 | # the envs_dir, make sure it contains a bin directory 107 | envs_dir, env_name = os.path.split(env_name.rstrip(os.path.sep)) 108 | 109 | env = _Env(name=env_name, 110 | path=os.path.join(envs_dir, env_name), 111 | bin_dir=os.path.join(envs_dir, env_name, 'bin'), 112 | envs_dir=envs_dir, 113 | ) 114 | # add the custom path to the envs_dirs so that the environment can 115 | # be deactivated again. 116 | if envs_dir not in config.envs_dirs: 117 | # extend envs_dirs tuple in config 118 | config.envs_dirs += (envs_dir,) 119 | return env 120 | else: 121 | return False 122 | 123 | 124 | def _activate(env_name): 125 | """ 126 | Activate an existing conda directory. If a non-root directory 127 | is already active, _deactivate it first. Also install a conda 128 | symlink if not present. 129 | """ 130 | env = _pick_env(env_name) 131 | if env: 132 | # disable any currently enabled env 133 | if 'CONDA_DEFAULT_ENV' in ${...}: 134 | _deactivate() 135 | # make sure `conda` points at the right env 136 | $CONDA_DEFAULT_ENV = env.name 137 | $CONDA_PREFIX = env.path 138 | # copy current $PATH to backup envvar 139 | $_DEFAULT_CONDA_PATH = $PATH[:] 140 | # delete any existing conda path 141 | _ = [$PATH.remove(p) for p in $PATH if config.root_dir in p] 142 | # add the environment's bin dir in $PATH 143 | if env.bin_dir not in $PATH: 144 | $PATH.insert(0, env.bin_dir) 145 | # ensure conda symlink exists in directory 146 | symlink_conda(env.path, config.default_prefix) 147 | else: 148 | print("No environment '{}' found".format(env_name)) 149 | 150 | 151 | def _deactivate(): 152 | """ 153 | Deactivate the current environment and return to the default 154 | """ 155 | if '_DEFAULT_CONDA_PATH' in ${...}: 156 | $PATH = $_DEFAULT_CONDA_PATH[:] 157 | del $_DEFAULT_CONDA_PATH 158 | del $CONDA_DEFAULT_ENV 159 | if 'CONDA_PREFIX' in ${...}: 160 | del $CONDA_PREFIX 161 | 162 | 163 | def _xonda(args, stdin=None): 164 | """ 165 | If command is neither _activate nor _deactivate, just shell out to conda""" 166 | if len(args) == 2 and args[0] in ['activate', 'select']: 167 | _activate(args[1]) 168 | elif len(args) == 1 and args[0] is 'deactivate': 169 | _deactivate() 170 | elif len(args) > 0: 171 | @$(which -s conda) @(args) 172 | elif len(args) == 0: 173 | @$(which -s conda) -h 174 | else: 175 | return 176 | 177 | def _xonda_completer(prefix, line, start, end, ctx): 178 | """ 179 | Completion for `xonda` 180 | """ 181 | args = line.split(' ') 182 | possible = set() 183 | if len(args) == 0 or args[0] not in ['xonda', 'conda']: 184 | return None 185 | curix = args.index(prefix) 186 | if curix == 1: 187 | possible = {'activate', 'deactivate', 'install', 'remove', 'info', 188 | 'help', 'list', 'search', 'update', 'upgrade', 'uninstall', 189 | 'config', 'init', 'clean', 'package', 'bundle', 'env', 190 | 'select', 'create'} 191 | 192 | elif curix == 2: 193 | if args[1] in ['activate', 'select']: 194 | possible = set([env.name for env in _get_envs()]) 195 | elif args[1] == 'create': 196 | possible = {'-p', '-n'} 197 | elif args[1] == 'env': 198 | possible = {'attach', 'create', 'export', 'list', 'remove', 199 | 'upload', 'update'} 200 | 201 | elif curix == 3: 202 | if args[2] == 'export': 203 | possible = {'-n', '--name'} 204 | elif args[2] == 'create': 205 | possible = {'-h', '--help', '-f', '--file', '-n', '--name', '-p', 206 | '--prefix', '-q', '--quiet', '--force', '--json', 207 | '--debug', '-v', '--verbose'} 208 | 209 | elif curix == 4: 210 | if args[2] == 'export' and args[3] in ['-n','--name']: 211 | possible = set([env.name for env in _get_envs()]) 212 | 213 | return {i for i in possible if i.startswith(prefix)} 214 | 215 | aliases['conda'] = _xonda 216 | 217 | # add _xonda_completer to list of completers 218 | __xonsh__.completers['xonda'] = _xonda_completer 219 | # bump to top of list 220 | __xonsh__.completers.move_to_end('xonda', last=False) 221 | --------------------------------------------------------------------------------