├── .gitignore ├── CHANGELOG.rst ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── environment_kernels ├── __init__.py ├── activate_helper.py ├── core.py ├── env_kernelspec.py ├── envs_common.py ├── envs_conda.py ├── envs_virtualenv.py ├── logos │ ├── python │ │ ├── logo-32x32.png │ │ └── logo-64x64.png │ └── r │ │ └── logo-64x64.png └── utils.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 1.2 (Unreleased) 2 | ================ 3 | 4 | New Features 5 | ------------ 6 | 7 | Bug Fixes 8 | --------- 9 | 10 | 11 | 1.1.1 12 | ===== 13 | 14 | New Features 15 | ------------ 16 | 17 | Bug Fixes 18 | --------- 19 | 20 | - Fix Python 2 bug in comparing dictionary keys [#31] 21 | - Fix virtualenv kernels being prefixed with 'conda' [#32] 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Stuart Mumford 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.md 3 | include environment_kernels/logos/python/* 4 | include environment_kernels/logos/r/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Automatic Environment Kernel Detection for Jupyter 2 | ================================================== 3 | 4 | A Jupyter plugin to enable the automatic detection of environments as kernels. 5 | 6 | This plugin looks in the directories you specify for installed environments 7 | which have Jupyter installed and lists them as kernels for Jupyter to find. 8 | This makes it easy to run one notebook instance and access kernels with access 9 | to different versions of Python or different modules seamlessly. 10 | 11 | 12 | ## Installation 13 | 14 | The plugin can be installed with: 15 | 16 | pip install environment_kernels 17 | 18 | To enable the plugin add the following line to your notebook [config file](https://jupyter-notebook.readthedocs.org/en/latest/config.html): 19 | 20 | c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager' 21 | 22 | To create a config file run: 23 | 24 | jupyter notebook --generate-config 25 | 26 | or run the notebook with the following argument: 27 | 28 | --NotebookApp.kernel_spec_manager_class='environment_kernels.EnvironmentKernelSpecManager' 29 | 30 | ## Search Directories for Environments 31 | 32 | The plugin works by getting a list of possible environments which might contain an 33 | ipython kernel. 34 | 35 | There are multiple ways to find possible environments: 36 | 37 | * All subdirs of the base directories (default: `~/.conda/envs` for conda 38 | based environments and `~/.virtualenvs`) for virtualenv based environments. 39 | * If the jupyter notebook is being run in the conda root environment 40 | `conda.config.envs_dirs` will be imported and all subdirs of these 41 | dirs will be added to the list of possible environments. 42 | * If the notebook server is run from inside a conda environment then the 43 | `CONDA_ENV_DIR` variable will be set and will be used to find the 44 | directory which contains the environments. 45 | * If a `conda` executeable is available, it will be queried for a list 46 | of environments. 47 | 48 | Each possible environment will be searched for an `ipython` executeable and 49 | if found, a kernel entry will be added on the fly. 50 | 51 | The ipython search pattern is on Linux and OS/X: 52 | 53 | ENV_NAME/{bin|Scripts}/ipython 54 | 55 | and on Windows: 56 | 57 | ENV_NAME\{bin|Scripts}\ipython.exe 58 | 59 | The kernels will be named after the type (conda or virtualenv) and by the 60 | name of the environment directory (example: the kernel in conda environment 61 | `C:\miniconda\envs\tests` gets the name `conda_tests`). If there are multiple 62 | envs which would result in the same kernel name (e.g. when multiple base dirs 63 | are configured, which each contain an environment with the same name), only the 64 | first kernel will be used and this ommision will be mentioned in the notebook 65 | console log. 66 | 67 | You can configure this behaviour in mutliple ways: 68 | 69 | You can override the default base directories by setting the following 70 | config values: 71 | 72 | c.EnvironmentKernelSpecManager.virtualenv_env_dirs=['/opt/virtualenv/envs/'] 73 | c.EnvironmentKernelSpecManager.conda_env_dirs=['/opt/miniconda/envs/'] 74 | 75 | You can also disable specific search paths: 76 | 77 | c.EnvironmentKernelSpecManager.find_conda_envs=False 78 | c.EnvironmentKernelSpecManager.find_virtualenv_envs=False 79 | 80 | The above disables both types of environments, so this will effectivly 81 | disable all environment kernels. 82 | 83 | You can also disable only the conda call, which is expensive but the only reliable way 84 | on windows: 85 | 86 | c.EnvironmentKernelSpecManager.use_conda_directly=False 87 | 88 | ## Limiting Environments 89 | 90 | If you want to, you can also ignore environments with certain names: 91 | 92 | c.EnvironmentKernelSpecManager.blacklist_envs=['conda_testenv'] 93 | 94 | Or you can specify a whitelist of "allowed" environments with: 95 | 96 | c.EnvironmentKernelSpecManager.whitelist_envs=['virtualenv_testenv'] 97 | 98 | ## Configuring the display name 99 | 100 | The default lists all environmental kernels as `Environment (type_name)`. This 101 | can be cumbersome, as these kernels are usually sorted higher than other kernels. 102 | 103 | You can change the display name via this config (you must include the 104 | placeholder `{}`!): 105 | 106 | c.EnvironmentKernelSpecManager.display_name_template="~Env ({})" 107 | 108 | ## Config via the commandline 109 | 110 | All config values can also be set on the commandline by using the config value as argument: 111 | 112 | As an example: 113 | 114 | c.EnvironmentKernelSpecManager.blacklist_envs=['conda_testenv'] 115 | 116 | becomes 117 | 118 | --EnvironmentKernelSpecManager.blacklist_envs="['conda_testenv']" 119 | -------------------------------------------------------------------------------- /environment_kernels/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .core import * 3 | -------------------------------------------------------------------------------- /environment_kernels/activate_helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2015, the xonsh developers. All rights reserved. 3 | """Helpers to activate an environment and prepare the environment variables for the kernel. 4 | Copied from xonsh""" 5 | 6 | # Changes from xonsh: 7 | # - replace the xonsh environment cache with os.environ 8 | # - remove aliases and func handling -> we are only interested on environment variables 9 | # - remove xonsh special ENV thingy and "detype()" 10 | # - add source_bash and source_zsh 11 | # - Changed the default for "save" in all function definitions/parser to False to get exceptions 12 | 13 | # Original license: 14 | # Copyright 2015-2016, the xonsh developers. All rights reserved. 15 | # 16 | # Redistribution and use in source and binary forms, with or without modification, are 17 | # permitted provided that the following conditions are met: 18 | # 19 | # 1. Redistributions of source code must retain the above copyright notice, this list of 20 | # conditions and the following disclaimer. 21 | # 22 | # 2. Redistributions in binary form must reproduce the above copyright notice, this list 23 | # of conditions and the following disclaimer in the documentation and/or other materials 24 | # provided with the distribution. 25 | # 26 | # THIS SOFTWARE IS PROVIDED BY THE XONSH DEVELOPERS ``AS IS'' AND ANY EXPRESS OR IMPLIED 27 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 28 | # FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE XONSH DEVELOPERS OR 29 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 30 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 33 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 34 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | # 36 | # The views and conclusions contained in the software and documentation are those of the 37 | # authors and should not be interpreted as representing official policies, either expressed 38 | # or implied, of the stakeholders of the xonsh project or the employers of xonsh developers. 39 | 40 | from __future__ import absolute_import 41 | 42 | import os 43 | from argparse import ArgumentParser 44 | import subprocess 45 | from tempfile import NamedTemporaryFile 46 | import re 47 | from itertools import chain 48 | 49 | from .utils import FileNotFoundError, ON_WINDOWS 50 | 51 | 52 | ENV_SPLIT_RE = re.compile('^([^=]+)=([^=]*|[^\n]*)$',flags=re.DOTALL|re.MULTILINE) 53 | 54 | 55 | def source_env_vars_from_command(args): 56 | if ON_WINDOWS: 57 | return source_cmd(args) 58 | else: 59 | # bash is probably installed everywhere... if not... 60 | try: 61 | return source_bash(args) 62 | except: 63 | return source_zsh(args) 64 | 65 | 66 | def source_bash(args, stdin=None): 67 | """Simply bash-specific wrapper around source-foreign 68 | 69 | Returns a dict to be used as a new environment""" 70 | args = list(args) 71 | new_args = ['bash', '--sourcer=source'] 72 | new_args.extend(args) 73 | return source_foreign(new_args, stdin=stdin) 74 | 75 | def source_zsh(args, stdin=None): 76 | """Simply zsh-specific wrapper around source-foreign 77 | 78 | Returns a dict to be used as a new environment""" 79 | args = list(args) 80 | new_args = ['zsh', '--sourcer=source'] 81 | new_args.extend(args) 82 | return source_foreign(new_args, stdin=stdin) 83 | 84 | 85 | def source_cmd(args, stdin=None): 86 | """Simple cmd.exe-specific wrapper around source-foreign. 87 | 88 | returns a dict to be used as a new environment 89 | """ 90 | args = list(args) 91 | fpath = locate_binary(args[0]) 92 | args[0] = fpath if fpath else args[0] 93 | if not os.path.isfile(args[0]): 94 | raise RuntimeError("Command not found: %s" % args[0]) 95 | prevcmd = 'call ' 96 | prevcmd += ' '.join([argvquote(arg, force=True) for arg in args]) 97 | prevcmd = escape_windows_cmd_string(prevcmd) 98 | args.append('--prevcmd={}'.format(prevcmd)) 99 | args.insert(0, 'cmd') 100 | args.append('--interactive=0') 101 | args.append('--sourcer=call') 102 | args.append('--envcmd=set') 103 | args.append('--seterrpostcmd=if errorlevel 1 exit 1') 104 | args.append('--use-tmpfile=1') 105 | return source_foreign(args, stdin=stdin) 106 | 107 | 108 | def locate_binary(name): 109 | if os.path.isfile(name) and name != os.path.basename(name): 110 | return name 111 | 112 | directories = os.environ.get('PATH').split(os.path.pathsep) 113 | 114 | # Windows users expect to be able to execute files in the same directory without `./` 115 | if ON_WINDOWS: 116 | directories = [_get_cwd()] + directories 117 | 118 | try: 119 | return next(chain.from_iterable(yield_executables(directory, name) for directory in directories if os.path.isdir(directory))) 120 | except StopIteration: 121 | return None 122 | 123 | 124 | def argvquote(arg, force=False): 125 | """ Returns an argument quoted in such a way that that CommandLineToArgvW 126 | on Windows will return the argument string unchanged. 127 | This is the same thing Popen does when supplied with an list of arguments. 128 | Arguments in a command line should be separated by spaces; this 129 | function does not add these spaces. This implementation follows the 130 | suggestions outlined here: 131 | https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ 132 | """ 133 | if not force and len(arg) != 0 and not any([c in arg for c in ' \t\n\v"']): 134 | return arg 135 | else: 136 | n_backslashes = 0 137 | cmdline = '"' 138 | for c in arg: 139 | if c == "\\": 140 | # first count the number of current backslashes 141 | n_backslashes += 1 142 | continue 143 | if c == '"': 144 | # Escape all backslashes and the following double quotation mark 145 | cmdline += (n_backslashes * 2 + 1) * '\\' 146 | else: 147 | # backslashes are not special here 148 | cmdline += n_backslashes * '\\' 149 | n_backslashes = 0 150 | cmdline += c 151 | # Escape all backslashes, but let the terminating 152 | # double quotation mark we add below be interpreted 153 | # as a metacharacter 154 | cmdline += + n_backslashes * 2 * '\\' + '"' 155 | return cmdline 156 | 157 | 158 | def escape_windows_cmd_string(s): 159 | """Returns a string that is usable by the Windows cmd.exe. 160 | The escaping is based on details here and emperical testing: 161 | http://www.robvanderwoude.com/escapechars.php 162 | """ 163 | for c in '()%!^<>&|"': 164 | s = s.replace(c, '^' + c) 165 | s = s.replace('/?', '/.') 166 | return s 167 | 168 | 169 | def source_foreign(args, stdin=None): 170 | """Sources a file written in a foreign shell language.""" 171 | parser = _ensure_source_foreign_parser() 172 | ns = parser.parse_args(args) 173 | if ns.prevcmd is not None: 174 | pass # don't change prevcmd if given explicitly 175 | elif os.path.isfile(ns.files_or_code[0]): 176 | # we have filename to source 177 | ns.prevcmd = '{} "{}"'.format(ns.sourcer, '" "'.join(ns.files_or_code)) 178 | elif ns.prevcmd is None: 179 | ns.prevcmd = ' '.join(ns.files_or_code) # code to run, no files 180 | fsenv = foreign_shell_data(shell=ns.shell, login=ns.login, 181 | interactive=ns.interactive, 182 | envcmd=ns.envcmd, 183 | aliascmd=ns.aliascmd, 184 | extra_args=ns.extra_args, 185 | safe=ns.safe, prevcmd=ns.prevcmd, 186 | postcmd=ns.postcmd, 187 | funcscmd=ns.funcscmd, 188 | sourcer=ns.sourcer, 189 | use_tmpfile=ns.use_tmpfile, 190 | seterrprevcmd=ns.seterrprevcmd, 191 | seterrpostcmd=ns.seterrpostcmd) 192 | if fsenv is None: 193 | raise RuntimeError("Source failed: {}\n".format(ns.prevcmd), 1) 194 | # apply results 195 | env = os.environ.copy() 196 | for k, v in fsenv.items(): 197 | if k in env and v == env[k]: 198 | continue # no change from original 199 | env[k] = v 200 | # Remove any env-vars that were unset by the script. 201 | for k in os.environ: # use os.environ again to prevent errors about changed size 202 | if k not in fsenv: 203 | env.pop(k, None) 204 | return env 205 | 206 | 207 | def _get_cwd(): 208 | try: 209 | return os.getcwd() 210 | except (OSError, FileNotFoundError): 211 | return None 212 | 213 | 214 | def yield_executables_windows(directory, name): 215 | normalized_name = os.path.normcase(name) 216 | extensions = os.environ.get('PATHEXT') 217 | try: 218 | names = os.listdir(directory) 219 | except PermissionError: 220 | return 221 | for a_file in names: 222 | normalized_file_name = os.path.normcase(a_file) 223 | base_name, ext = os.path.splitext(normalized_file_name) 224 | 225 | if ( 226 | normalized_name == base_name or normalized_name == normalized_file_name 227 | ) and ext.upper() in extensions: 228 | yield os.path.join(directory, a_file) 229 | 230 | 231 | def yield_executables_posix(directory, name): 232 | try: 233 | names = os.listdir(directory) 234 | except PermissionError: 235 | return 236 | if name in names: 237 | path = os.path.join(directory, name) 238 | if _is_executable_file(path): 239 | yield path 240 | 241 | 242 | yield_executables = yield_executables_windows if ON_WINDOWS else yield_executables_posix 243 | 244 | def _is_executable_file(path): 245 | """Checks that path is an executable regular file, or a symlink towards one. 246 | This is roughly ``os.path isfile(path) and os.access(path, os.X_OK)``. 247 | 248 | This function was forked from pexpect originally: 249 | 250 | Copyright (c) 2013-2014, Pexpect development team 251 | Copyright (c) 2012, Noah Spurrier 252 | 253 | PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY 254 | PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE 255 | COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. 256 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 257 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 258 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 259 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 260 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 261 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 262 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 263 | """ 264 | # follow symlinks, 265 | fpath = os.path.realpath(path) 266 | 267 | if not os.path.isfile(fpath): 268 | # non-files (directories, fifo, etc.) 269 | return False 270 | 271 | return os.access(fpath, os.X_OK) 272 | 273 | 274 | _SOURCE_FOREIGN_PARSER = None 275 | 276 | 277 | def _ensure_source_foreign_parser(): 278 | global _SOURCE_FOREIGN_PARSER 279 | if _SOURCE_FOREIGN_PARSER is not None: 280 | return _SOURCE_FOREIGN_PARSER 281 | desc = "Sources a file written in a foreign shell language." 282 | parser = ArgumentParser('source-foreign', description=desc) 283 | parser.add_argument('shell', help='Name or path to the foreign shell') 284 | parser.add_argument('files_or_code', nargs='+', 285 | help='file paths to source or code in the target ' 286 | 'language.') 287 | parser.add_argument('-i', '--interactive', type=to_bool, default=True, 288 | help='whether the sourced shell should be interactive', 289 | dest='interactive') 290 | parser.add_argument('-l', '--login', type=to_bool, default=False, 291 | help='whether the sourced shell should be login', 292 | dest='login') 293 | parser.add_argument('--envcmd', default=None, dest='envcmd', 294 | help='command to print environment') 295 | parser.add_argument('--aliascmd', default=None, dest='aliascmd', 296 | help='command to print aliases') 297 | parser.add_argument('--extra-args', default=(), dest='extra_args', 298 | type=(lambda s: tuple(s.split())), 299 | help='extra arguments needed to run the shell') 300 | parser.add_argument('-s', '--safe', type=to_bool, default=False, 301 | help='whether the source shell should be run safely, ' 302 | 'and not raise any errors, even if they occur.', 303 | dest='safe') 304 | parser.add_argument('-p', '--prevcmd', default=None, dest='prevcmd', 305 | help='command(s) to run before any other commands, ' 306 | 'replaces traditional source.') 307 | parser.add_argument('--postcmd', default='', dest='postcmd', 308 | help='command(s) to run after all other commands') 309 | parser.add_argument('--funcscmd', default=None, dest='funcscmd', 310 | help='code to find locations of all native functions ' 311 | 'in the shell language.') 312 | parser.add_argument('--sourcer', default=None, dest='sourcer', 313 | help='the source command in the target shell ' 314 | 'language, default: source.') 315 | parser.add_argument('--use-tmpfile', type=to_bool, default=False, 316 | help='whether the commands for source shell should be ' 317 | 'written to a temporary file.', 318 | dest='use_tmpfile') 319 | parser.add_argument('--seterrprevcmd', default=None, dest='seterrprevcmd', 320 | help='command(s) to set exit-on-error before any' 321 | 'other commands.') 322 | parser.add_argument('--seterrpostcmd', default=None, dest='seterrpostcmd', 323 | help='command(s) to set exit-on-error after all' 324 | 'other commands.') 325 | _SOURCE_FOREIGN_PARSER = parser 326 | return parser 327 | 328 | def foreign_shell_data(shell, interactive=True, login=False, envcmd=None, 329 | aliascmd=None, extra_args=(), currenv=None, 330 | safe=False, prevcmd='', postcmd='', funcscmd=None, 331 | sourcer=None, use_tmpfile=False, tmpfile_ext=None, 332 | runcmd=None, seterrprevcmd=None, seterrpostcmd=None): 333 | """Extracts data from a foreign (non-xonsh) shells. Currently this gets 334 | the environment, aliases, and functions but may be extended in the future. 335 | 336 | Parameters 337 | ---------- 338 | shell : str 339 | The name of the shell, such as 'bash' or '/bin/sh'. 340 | interactive : bool, optional 341 | Whether the shell should be run in interactive mode. 342 | login : bool, optional 343 | Whether the shell should be a login shell. 344 | envcmd : str or None, optional 345 | The command to generate environment output with. 346 | aliascmd : str or None, optional 347 | The command to generate alias output with. 348 | extra_args : tuple of str, optional 349 | Addtional command line options to pass into the shell. 350 | currenv : tuple of items or None, optional 351 | Manual override for the current environment. 352 | safe : bool, optional 353 | Flag for whether or not to safely handle exceptions and other errors. 354 | prevcmd : str, optional 355 | A command to run in the shell before anything else, useful for 356 | sourcing and other commands that may require environment recovery. 357 | postcmd : str, optional 358 | A command to run after everything else, useful for cleaning up any 359 | damage that the prevcmd may have caused. 360 | funcscmd : str or None, optional 361 | This is a command or script that can be used to determine the names 362 | and locations of any functions that are native to the foreign shell. 363 | This command should print *only* a JSON object that maps 364 | function names to the filenames where the functions are defined. 365 | If this is None, then a default script will attempted to be looked 366 | up based on the shell name. Callable wrappers for these functions 367 | will be returned in the aliases dictionary. 368 | sourcer : str or None, optional 369 | How to source a foreign shell file for purposes of calling functions 370 | in that shell. If this is None, a default value will attempt to be 371 | looked up based on the shell name. 372 | use_tmpfile : bool, optional 373 | This specifies if the commands are written to a tmp file or just 374 | parsed directly to the shell 375 | tmpfile_ext : str or None, optional 376 | If tmpfile is True this sets specifies the extension used. 377 | runcmd : str or None, optional 378 | Command line switches to use when running the script, such as 379 | -c for Bash and /C for cmd.exe. 380 | seterrprevcmd : str or None, optional 381 | Command that enables exit-on-error for the shell that is run at the 382 | start of the script. For example, this is "set -e" in Bash. To disable 383 | exit-on-error behavior, simply pass in an empty string. 384 | seterrpostcmd : str or None, optional 385 | Command that enables exit-on-error for the shell that is run at the end 386 | of the script. For example, this is "if errorlevel 1 exit 1" in 387 | cmd.exe. To disable exit-on-error behavior, simply pass in an 388 | empty string. 389 | 390 | Returns 391 | ------- 392 | env : dict 393 | Dictionary of shell's environment 394 | aliases : dict 395 | Dictionary of shell's alaiases, this includes foreign function 396 | wrappers. 397 | """ 398 | cmd = [shell] 399 | cmd.extend(extra_args) # needs to come here for GNU long options 400 | if interactive: 401 | cmd.append('-i') 402 | if login: 403 | cmd.append('-l') 404 | shkey = CANON_SHELL_NAMES[shell] 405 | envcmd = DEFAULT_ENVCMDS.get(shkey, 'env') if envcmd is None else envcmd 406 | tmpfile_ext = DEFAULT_TMPFILE_EXT.get(shkey, 'sh') if tmpfile_ext is None else tmpfile_ext 407 | runcmd = DEFAULT_RUNCMD.get(shkey, '-c') if runcmd is None else runcmd 408 | seterrprevcmd = DEFAULT_SETERRPREVCMD.get(shkey, '') \ 409 | if seterrprevcmd is None else seterrprevcmd 410 | seterrpostcmd = DEFAULT_SETERRPOSTCMD.get(shkey, '') \ 411 | if seterrpostcmd is None else seterrpostcmd 412 | command = COMMAND.format(envcmd=envcmd, prevcmd=prevcmd, 413 | postcmd=postcmd, 414 | seterrprevcmd=seterrprevcmd, 415 | seterrpostcmd=seterrpostcmd).strip() 416 | 417 | cmd.append(runcmd) 418 | 419 | if not use_tmpfile: 420 | cmd.append(command) 421 | else: 422 | tmpfile = NamedTemporaryFile(suffix=tmpfile_ext, delete=False) 423 | tmpfile.write(command.encode('utf8')) 424 | tmpfile.close() 425 | cmd.append(tmpfile.name) 426 | 427 | if currenv is not None: 428 | currenv = os.environ 429 | try: 430 | s = subprocess.check_output(cmd, stderr=subprocess.PIPE, env=currenv, 431 | # start new session to avoid hangs 432 | start_new_session=True, 433 | universal_newlines=True) 434 | except (subprocess.CalledProcessError, FileNotFoundError): 435 | if not safe: 436 | raise 437 | return None, None 438 | finally: 439 | if use_tmpfile: 440 | pass 441 | os.remove(tmpfile.name) 442 | env = parse_env(s) 443 | return env 444 | 445 | def to_bool(x): 446 | """"Converts to a boolean in a semantically meaningful way.""" 447 | if isinstance(x, bool): 448 | return x 449 | elif isinstance(x, str): 450 | return False if x.lower() in _FALSES else True 451 | else: 452 | return bool(x) 453 | 454 | _FALSES = frozenset(['', '0', 'n', 'f', 'no', 'none', 'false']) 455 | 456 | # mapping of shell name alises to keys in other lookup dictionaries. 457 | CANON_SHELL_NAMES = { 458 | 'bash': 'bash', 459 | '/bin/bash': 'bash', 460 | 'zsh': 'zsh', 461 | '/bin/zsh': 'zsh', 462 | '/usr/bin/zsh': 'zsh', 463 | 'cmd': 'cmd', 464 | 'cmd.exe': 'cmd', 465 | } 466 | 467 | DEFAULT_ENVCMDS = { 468 | 'bash': 'env', 469 | 'zsh': 'env', 470 | 'cmd': 'set', 471 | } 472 | DEFAULT_SOURCERS = { 473 | 'bash': 'source', 474 | 'zsh': 'source', 475 | 'cmd': 'call', 476 | } 477 | DEFAULT_TMPFILE_EXT = { 478 | 'bash': '.sh', 479 | 'zsh': '.zsh', 480 | 'cmd': '.bat', 481 | } 482 | DEFAULT_RUNCMD = { 483 | 'bash': '-c', 484 | 'zsh': '-c', 485 | 'cmd': '/C', 486 | } 487 | DEFAULT_SETERRPREVCMD = { 488 | 'bash': 'set -e', 489 | 'zsh': 'set -e', 490 | 'cmd': '@echo off', 491 | } 492 | DEFAULT_SETERRPOSTCMD = { 493 | 'bash': '', 494 | 'zsh': '', 495 | 'cmd': 'if errorlevel 1 exit 1', 496 | } 497 | 498 | COMMAND = """ 499 | {seterrprevcmd} 500 | {prevcmd} 501 | echo __XONSH_ENV_BEG__ 502 | {envcmd} 503 | echo __XONSH_ENV_END__ 504 | {postcmd} 505 | {seterrpostcmd} 506 | """.strip() 507 | 508 | ENV_RE = re.compile('__XONSH_ENV_BEG__\n(.*)__XONSH_ENV_END__', flags=re.DOTALL) 509 | 510 | 511 | def parse_env(s): 512 | """Parses the environment portion of string into a dict.""" 513 | m = ENV_RE.search(s) 514 | if m is None: 515 | return {} 516 | g1 = m.group(1) 517 | env = dict(ENV_SPLIT_RE.findall(g1)) 518 | return env 519 | 520 | 521 | def diff_dict(a, b): 522 | ret_dict = {} 523 | if ON_WINDOWS: 524 | # Windows var names are case insensitive 525 | a = {k.upper(): a[k] for k in a.keys()} 526 | b = {k.upper(): b[k] for k in b.keys()} 527 | 528 | # put in old values which got updated/removed 529 | for key, val in a.items(): 530 | if key in b: 531 | if b[key] != val: 532 | # updated 533 | ret_dict[key] = (val, "->", b[key]) 534 | else: 535 | # not changed 536 | pass 537 | else: 538 | # removed 539 | ret_dict[key] = (val, "->", "-") 540 | for key, val in b.items(): 541 | if key not in a: 542 | # new 543 | ret_dict[key] = ("-", "->", val) 544 | return ret_dict -------------------------------------------------------------------------------- /environment_kernels/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import os 5 | import os.path 6 | 7 | from jupyter_client.kernelspec import (KernelSpecManager, NoSuchKernel) 8 | from traitlets import List, Unicode, Bool, Int 9 | 10 | from .envs_conda import get_conda_env_data 11 | from .envs_virtualenv import get_virtualenv_env_data 12 | from .utils import FileNotFoundError, HAVE_CONDA 13 | 14 | ENV_SUPPLYER = [get_conda_env_data, get_virtualenv_env_data] 15 | 16 | __all__ = ['EnvironmentKernelSpecManager'] 17 | 18 | 19 | class EnvironmentKernelSpecManager(KernelSpecManager): 20 | """ 21 | A Jupyter Kernel manager which dyamically checks for Environments 22 | 23 | Given a list of base directories, this class searches for the pattern:: 24 | 25 | BASE_DIR/NAME/{bin|Skript}/ipython 26 | 27 | where NAME is taken to be the name of the environment. 28 | """ 29 | 30 | # Take the default home DIR for conda and virtualenv as the default 31 | _default_conda_dirs = ['~/.conda/envs/'] 32 | _default_virtualenv_dirs = ['~/.virtualenvs'] 33 | 34 | # Check for the CONDA_ENV_PATH variable and add it to the list if set. 35 | if os.environ.get('CONDA_ENV_PATH', False): 36 | _default_conda_dirs.append(os.environ['CONDA_ENV_PATH'].split('envs')[0]) 37 | 38 | # If we are running inside the root conda env can get all the env dirs: 39 | if HAVE_CONDA: 40 | import conda 41 | _default_conda_dirs += conda.config.envs_dirs 42 | 43 | # Remove any duplicates 44 | _default_conda_dirs = list(set(map(os.path.expanduser, 45 | _default_conda_dirs))) 46 | 47 | conda_env_dirs = List( 48 | _default_conda_dirs, 49 | config=True, 50 | help="List of directories in which are conda environments.") 51 | 52 | virtualenv_env_dirs = List( 53 | _default_virtualenv_dirs, 54 | config=True, 55 | help="List of directories in which are virtualenv environments.") 56 | 57 | blacklist_envs = List( 58 | ["conda__build"], 59 | config=True, 60 | help="Environments which should not be used even if a ipykernel exists in it.") 61 | 62 | whitelist_envs = List( 63 | [], 64 | config=True, 65 | help="Environments which should be used, all others are ignored (overwrites blacklist_envs).") 66 | 67 | display_name_template = Unicode( 68 | u"Environment ({})", 69 | config=True, 70 | help="Template for the kernel name in the UI. Needs to include {} for the name.") 71 | 72 | conda_prefix_template = Unicode( 73 | u"conda_{}", 74 | config=True, 75 | help="Template for the conda environment kernel name prefix in the UI. Needs to include {} for the name.") 76 | 77 | virtualenv_prefix_template = Unicode( 78 | u"virtualenv_{}", 79 | config=True, 80 | help="Template for the virtualenv environment kernel name prefix in the UI. Needs to include {} for the name.") 81 | 82 | find_conda_envs = Bool( 83 | True, 84 | config=True, 85 | help="Probe for conda environments, including calling conda itself.") 86 | 87 | find_r_envs = Bool( 88 | True, 89 | config=True, 90 | help="Probe environments for R kernels (currently only conda environments).") 91 | 92 | use_conda_directly = Bool( 93 | True, 94 | config=True, 95 | help="Probe for conda environments by calling conda itself. Only relevant if find_conda_envs is True.") 96 | 97 | refresh_interval = Int( 98 | 3, 99 | config=True, 100 | help="Interval (in minutes) to refresh the list of environment kernels. Setting it to '0' disables the refresh.") 101 | 102 | find_virtualenv_envs = Bool(True, 103 | config=True, 104 | help="Probe for virtualenv environments.") 105 | 106 | def __init__(self, *args, **kwargs): 107 | super(EnvironmentKernelSpecManager, self).__init__(*args, **kwargs) 108 | self.log.info("Using EnvironmentKernelSpecManager...") 109 | self._env_data_cache = {} 110 | if self.refresh_interval > 0: 111 | try: 112 | from tornado.ioloop import PeriodicCallback, IOLoop 113 | # Initial loading NOW 114 | IOLoop.current().call_later(0, callback=self._update_env_data, initial=True) 115 | # Later updates 116 | updater = PeriodicCallback(callback=self._update_env_data, 117 | callback_time=1000 * 60 * self.refresh_interval) 118 | updater.start() 119 | if not updater.is_running(): 120 | raise Exception() 121 | self._periodic_updater = updater 122 | self.log.info("Started periodic updates of the kernel list (every %s minutes).", self.refresh_interval) 123 | except: 124 | self.log.exception("Error while trying to enable periodic updates of the kernel list.") 125 | else: 126 | self.log.info("Periodical updates the kernel list are DISABLED.") 127 | 128 | def validate_env(self, envname): 129 | """ 130 | Check the name of the environment against the black list and the 131 | whitelist. If a whitelist is specified only it is checked. 132 | """ 133 | if self.whitelist_envs and envname in self.whitelist_envs: 134 | return True 135 | elif self.whitelist_envs: 136 | return False 137 | 138 | if self.blacklist_envs and envname not in self.blacklist_envs: 139 | return True 140 | elif self.blacklist_envs: 141 | # If there is just a True, all envs are blacklisted 142 | return False 143 | else: 144 | return True 145 | 146 | def _update_env_data(self, initial=False): 147 | if initial: 148 | self.log.info("Starting initial scan of virtual environments...") 149 | else: 150 | self.log.debug("Starting periodic scan of virtual environments...") 151 | self._get_env_data(reload=True) 152 | self.log.debug("done.") 153 | 154 | def _get_env_data(self, reload=False): 155 | """Get the data about the available environments. 156 | 157 | env_data is a structure {name -> (resourcedir, kernel spec)} 158 | """ 159 | 160 | # This is called much too often and finding-process is really expensive :-( 161 | if not reload and getattr(self, "_env_data_cache", {}): 162 | return getattr(self, "_env_data_cache") 163 | 164 | env_data = {} 165 | for supplyer in ENV_SUPPLYER: 166 | env_data.update(supplyer(self)) 167 | 168 | env_data = {name: env_data[name] for name in env_data if self.validate_env(name)} 169 | new_kernels = [env for env in list(env_data.keys()) if env not in list(self._env_data_cache.keys())] 170 | if new_kernels: 171 | self.log.info("Found new kernels in environments: %s", ", ".join(new_kernels)) 172 | 173 | self._env_data_cache = env_data 174 | return env_data 175 | 176 | def find_kernel_specs_for_envs(self): 177 | """Returns a dict mapping kernel names to resource directories.""" 178 | data = self._get_env_data() 179 | return {name: data[name][0] for name in data} 180 | 181 | def get_all_kernel_specs_for_envs(self): 182 | """Returns the dict of name -> kernel_spec for all environments""" 183 | 184 | data = self._get_env_data() 185 | return {name: data[name][1] for name in data} 186 | 187 | def find_kernel_specs(self): 188 | """Returns a dict mapping kernel names to resource directories.""" 189 | # let real installed kernels overwrite envs with the same name: 190 | # this is the same order as the get_kernel_spec way, which also prefers 191 | # kernels from the jupyter dir over env kernels. 192 | specs = self.find_kernel_specs_for_envs() 193 | specs.update(super(EnvironmentKernelSpecManager, 194 | self).find_kernel_specs()) 195 | return specs 196 | 197 | def get_all_specs(self): 198 | """Returns a dict mapping kernel names and resource directories. 199 | """ 200 | # This is new in 4.1 -> https://github.com/jupyter/jupyter_client/pull/93 201 | specs = self.get_all_kernel_specs_for_envs() 202 | specs.update(super(EnvironmentKernelSpecManager, self).get_all_specs()) 203 | return specs 204 | 205 | def get_kernel_spec(self, kernel_name): 206 | """Returns a :class:`KernelSpec` instance for the given kernel_name. 207 | 208 | Raises :exc:`NoSuchKernel` if the given kernel name is not found. 209 | """ 210 | try: 211 | return super(EnvironmentKernelSpecManager, 212 | self).get_kernel_spec(kernel_name) 213 | except (NoSuchKernel, FileNotFoundError): 214 | venv_kernel_name = kernel_name.lower() 215 | specs = self.get_all_kernel_specs_for_envs() 216 | if venv_kernel_name in specs: 217 | return specs[venv_kernel_name] 218 | else: 219 | raise NoSuchKernel(kernel_name) 220 | -------------------------------------------------------------------------------- /environment_kernels/env_kernelspec.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Common function to deal with virtual environments""" 3 | from __future__ import absolute_import 4 | 5 | from jupyter_client.kernelspec import KernelSpec 6 | from traitlets import default 7 | 8 | _nothing = object() 9 | 10 | class EnvironmentLoadingKernelSpec(KernelSpec): 11 | """A KernelSpec which loads `env` by activating the virtual environment""" 12 | 13 | _loader = None 14 | _env = _nothing 15 | 16 | @property 17 | def env(self): 18 | if self._env is _nothing: 19 | if self._loader: 20 | try: 21 | self._env = self._loader() 22 | except: 23 | self._env = {} 24 | return self._env 25 | 26 | def __init__(self, loader, **kwargs): 27 | self._loader = loader 28 | super(EnvironmentLoadingKernelSpec, self).__init__(**kwargs) 29 | 30 | 31 | def to_dict(self): 32 | d = dict(argv=self.argv, 33 | # Do not trigger the loading 34 | #env=self.env, 35 | display_name=self.display_name, 36 | language=self.language, 37 | metadata=self.metadata, 38 | ) 39 | 40 | return d 41 | 42 | -------------------------------------------------------------------------------- /environment_kernels/envs_common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Common function to deal with virtual environments""" 3 | from __future__ import absolute_import 4 | 5 | import platform 6 | import os 7 | import glob 8 | 9 | from .env_kernelspec import EnvironmentLoadingKernelSpec 10 | 11 | JLAB_MINVERSION_3 = None 12 | 13 | def find_env_paths_in_basedirs(base_dirs): 14 | """Returns all potential envs in a basedir""" 15 | # get potential env path in the base_dirs 16 | env_path = [] 17 | for base_dir in base_dirs: 18 | env_path.extend(glob.glob(os.path.join( 19 | os.path.expanduser(base_dir), '*', ''))) 20 | # self.log.info("Found the following kernels from config: %s", ", ".join(venvs)) 21 | 22 | return env_path 23 | 24 | 25 | def convert_to_env_data(mgr, env_paths, validator_func, activate_func, 26 | name_template, display_name_template, name_prefix): 27 | """Converts a list of paths to environments to env_data. 28 | 29 | env_data is a structure {name -> (ressourcedir, kernel spec)} 30 | """ 31 | env_data = {} 32 | for venv_dir in env_paths: 33 | venv_name = os.path.split(os.path.abspath(venv_dir))[1] 34 | kernel_name = name_template.format(name_prefix + venv_name) 35 | kernel_name = kernel_name.lower() 36 | if kernel_name in env_data: 37 | mgr.log.debug( 38 | "Found duplicate env kernel: %s, which would again point to %s. Using the first!", 39 | kernel_name, venv_dir) 40 | continue 41 | argv, language, resource_dir, metadata = validator_func(venv_dir) 42 | if not argv: 43 | # probably does not contain the kernel type (e.g. not R or python or does not contain 44 | # the kernel code itself) 45 | continue 46 | display_name = display_name_template.format(kernel_name) 47 | kspec_dict = {"argv": argv, "language": language, 48 | "display_name": display_name, 49 | "resource_dir": resource_dir, 50 | "metadata": metadata 51 | } 52 | 53 | # the default vars are needed to save the vars in the function context 54 | def loader(env_dir=venv_dir, activate_func=activate_func, mgr=mgr): 55 | mgr.log.debug("Loading env data for %s" % env_dir) 56 | res = activate_func(mgr, env_dir) 57 | # mgr.log.info("PATH: %s" % res['PATH']) 58 | return res 59 | 60 | kspec = EnvironmentLoadingKernelSpec(loader, **kspec_dict) 61 | env_data.update({kernel_name: (resource_dir, kspec)}) 62 | return env_data 63 | 64 | 65 | def validate_IPykernel(venv_dir): 66 | """Validates that this env contains an IPython kernel and returns info to start it 67 | 68 | 69 | Returns: tuple 70 | (ARGV, language, resource_dir) 71 | """ 72 | python_exe_name = find_exe(venv_dir, "python") 73 | if python_exe_name is None: 74 | python_exe_name = find_exe(venv_dir, "python2") 75 | if python_exe_name is None: 76 | python_exe_name = find_exe(venv_dir, "python3") 77 | if python_exe_name is None: 78 | return [], None, None, {} 79 | 80 | # Make some checks for ipython first, because calling the import is expensive 81 | if find_exe(venv_dir, "ipython") is None: 82 | if find_exe(venv_dir, "ipython2") is None: 83 | if find_exe(venv_dir, "ipython3") is None: 84 | return [], None, None, {} 85 | 86 | # check if this is really an ipython **kernel** 87 | import subprocess 88 | try: 89 | subprocess.check_call([python_exe_name, '-c', 'import ipykernel'], stderr=subprocess.DEVNULL) 90 | except: 91 | # not installed? -> not useable in any case... 92 | return [], None, None, {} 93 | 94 | argv = [python_exe_name, "-m", "ipykernel", "-f", "{connection_file}"] 95 | resources_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logos", "python") 96 | 97 | metadata = {} 98 | if is_jlab_minversion_3() and is_ipykernel_minversion_6(python_exe_name): 99 | metadata["debugger"] = True 100 | return argv, "python", resources_dir, metadata 101 | 102 | 103 | def validate_IRkernel(venv_dir): 104 | """Validates that this env contains an IRkernel kernel and returns info to start it 105 | 106 | 107 | Returns: tuple 108 | (ARGV, language, resource_dir, metadata) 109 | """ 110 | r_exe_name = find_exe(venv_dir, "R") 111 | if r_exe_name is None: 112 | return [], None, None, None 113 | 114 | # check if this is really an IRkernel **kernel** 115 | import subprocess 116 | ressources_dir = None 117 | try: 118 | print_resources = 'cat(as.character(system.file("kernelspec", package = "IRkernel")))' 119 | resources_dir_bytes = subprocess.check_output([r_exe_name, '--slave', '-e', print_resources]) 120 | resources_dir = resources_dir_bytes.decode(errors='ignore') 121 | except: 122 | # not installed? -> not useable in any case... 123 | return [], None, None, None 124 | argv = [r_exe_name, "--slave", "-e", "IRkernel::main()", "--args", "{connection_file}"] 125 | if not os.path.exists(resources_dir.strip()): 126 | # Fallback to our own log, but don't get the nice js goodies... 127 | resources_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logos", "r") 128 | return argv, "r", resources_dir, dict() 129 | 130 | 131 | def find_exe(env_dir, name): 132 | """Finds a exe with that name in the environment path""" 133 | 134 | if platform.system() == "Windows": 135 | name = name + ".exe" 136 | 137 | # find the binary 138 | exe_name = os.path.join(env_dir, name) 139 | if not os.path.exists(exe_name): 140 | exe_name = os.path.join(env_dir, "bin", name) 141 | if not os.path.exists(exe_name): 142 | exe_name = os.path.join(env_dir, "Scripts", name) 143 | if not os.path.exists(exe_name): 144 | return None 145 | return exe_name 146 | 147 | 148 | def is_ipykernel_minversion_6(python_exe_name): 149 | import subprocess 150 | try: 151 | subprocess.check_call([python_exe_name, '-c', ''' 152 | import sys 153 | import ipykernel 154 | if int(ipykernel.__version__.split('.', maxsplit=1)[0]) >= 6: 155 | sys.exit(0) 156 | sys.exit(-1) 157 | ''' 158 | ], stderr=subprocess.DEVNULL) 159 | return True 160 | except Exception as e: 161 | return False 162 | 163 | 164 | def is_jlab_minversion_3(): 165 | global JLAB_MINVERSION_3 166 | if JLAB_MINVERSION_3 is not None: 167 | return JLAB_MINVERSION_3 168 | 169 | try: 170 | import jupyterlab 171 | JLAB_MINVERSION_3 = int(jupyterlab.__version__.split('.', maxsplit=1)[0]) >= 3 172 | except ModuleNotFoundError: 173 | JLAB_MINVERSION_3 = False 174 | return JLAB_MINVERSION_3 175 | -------------------------------------------------------------------------------- /environment_kernels/envs_conda.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Functions related to finding conda environments (both Python and R based)""" 3 | from __future__ import absolute_import 4 | 5 | from .activate_helper import source_env_vars_from_command 6 | from .envs_common import (find_env_paths_in_basedirs, convert_to_env_data, 7 | validate_IPykernel, validate_IRkernel) 8 | from .utils import FileNotFoundError, ON_WINDOWS 9 | 10 | def get_conda_env_data(mgr): 11 | """Finds kernel specs from conda environments 12 | 13 | env_data is a structure {name -> (resourcedir, kernel spec)} 14 | """ 15 | if not mgr.find_conda_envs: 16 | return {} 17 | 18 | mgr.log.debug("Looking for conda environments in %s...", mgr.conda_env_dirs) 19 | 20 | # find all potential env paths 21 | env_paths = find_env_paths_in_basedirs(mgr.conda_env_dirs) 22 | env_paths.extend(_find_conda_env_paths_from_conda(mgr)) 23 | env_paths = list(set(env_paths)) # remove duplicates 24 | 25 | mgr.log.debug("Scanning conda environments for python kernels...") 26 | env_data = convert_to_env_data(mgr=mgr, 27 | env_paths=env_paths, 28 | validator_func=validate_IPykernel, 29 | activate_func=_get_env_vars_for_conda_env, 30 | name_template=mgr.conda_prefix_template, 31 | display_name_template=mgr.display_name_template, 32 | name_prefix="") # lets keep the py kernels without a prefix... 33 | if mgr.find_r_envs: 34 | mgr.log.debug("Scanning conda environments for R kernels...") 35 | env_data.update(convert_to_env_data(mgr=mgr, 36 | env_paths=env_paths, 37 | validator_func=validate_IRkernel, 38 | activate_func=_get_env_vars_for_conda_env, 39 | name_template=mgr.conda_prefix_template, 40 | display_name_template=mgr.display_name_template, 41 | name_prefix="r_")) 42 | return env_data 43 | 44 | 45 | def _get_env_vars_for_conda_env(mgr, env_path): 46 | if ON_WINDOWS: 47 | args = ['activate', env_path] 48 | else: 49 | args = ['source', 'activate', env_path] 50 | 51 | try: 52 | envs = source_env_vars_from_command(args) 53 | #mgr.log.debug("PATH: %s", envs['PATH']) 54 | return envs 55 | except: 56 | # as a fallback, don't activate... 57 | mgr.log.exception( 58 | "Couldn't get environment variables for commands: %s", args) 59 | return {} 60 | 61 | 62 | def _find_conda_env_paths_from_conda(mgr): 63 | """Returns a list of path as given by `conda env list --json`. 64 | 65 | Returns empty list, if conda couldn't be called. 66 | """ 67 | # this is expensive, so make it configureable... 68 | if not mgr.use_conda_directly: 69 | return [] 70 | mgr.log.debug("Looking for conda environments by calling conda directly...") 71 | import subprocess 72 | import json 73 | try: 74 | p = subprocess.Popen( 75 | ['conda', 'env', 'list', '--json'], 76 | stdin=subprocess.PIPE, 77 | stdout=subprocess.PIPE) 78 | comm = p.communicate() 79 | output = comm[0].decode() 80 | if p.returncode != 0 or len(output) == 0: 81 | mgr.log.error( 82 | "Couldn't call 'conda' to get the environments. " 83 | "Output:\n%s", str(comm)) 84 | return [] 85 | except FileNotFoundError: 86 | mgr.log.error("'conda' not found in path.") 87 | return [] 88 | output = json.loads(output) 89 | envs = output["envs"] 90 | # self.log.info("Found the following kernels from conda: %s", ", ".join(envs)) 91 | return envs 92 | -------------------------------------------------------------------------------- /environment_kernels/envs_virtualenv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Functions related to finding virtualenv environments (python only)""" 3 | from __future__ import absolute_import 4 | 5 | import os 6 | 7 | from .utils import ON_WINDOWS 8 | from .activate_helper import source_env_vars_from_command 9 | from .envs_common import find_env_paths_in_basedirs, convert_to_env_data, validate_IPykernel 10 | 11 | 12 | def get_virtualenv_env_data(mgr): 13 | """Finds kernel specs from virtualenv environments 14 | 15 | env_data is a structure {name -> (resourcedir, kernel spec)} 16 | """ 17 | 18 | if not mgr.find_virtualenv_envs: 19 | return {} 20 | 21 | mgr.log.debug("Looking for virtualenv environments in %s...", mgr.virtualenv_env_dirs) 22 | 23 | # find all potential env paths 24 | env_paths = find_env_paths_in_basedirs(mgr.virtualenv_env_dirs) 25 | 26 | mgr.log.debug("Scanning virtualenv environments for python kernels...") 27 | env_data = convert_to_env_data(mgr=mgr, 28 | env_paths=env_paths, 29 | validator_func=validate_IPykernel, 30 | activate_func=_get_env_vars_for_virtualenv_env, 31 | name_template=mgr.virtualenv_prefix_template, 32 | display_name_template=mgr.display_name_template, 33 | # virtualenv has only python, so no need for a prefix 34 | name_prefix="") 35 | return env_data 36 | 37 | 38 | def _get_env_vars_for_virtualenv_env(mgr, env_path): 39 | if ON_WINDOWS: 40 | args = [os.path.join(env_path, "Shell", "activate")] 41 | else: 42 | args = ['source', os.path.join(env_path, "bin", "activate")] 43 | try: 44 | envs = source_env_vars_from_command(args) 45 | # mgr.log.debug("Environment variables: %s", envs) 46 | return envs 47 | except: 48 | # as a fallback, don't activate... 49 | mgr.log.exception( 50 | "Couldn't get environment variables for commands: %s", args) 51 | return {} 52 | -------------------------------------------------------------------------------- /environment_kernels/logos/python/logo-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadair/jupyter_environment_kernels/8b4ae6943ac1a29e4ea98d704fd2941d48cabf8f/environment_kernels/logos/python/logo-32x32.png -------------------------------------------------------------------------------- /environment_kernels/logos/python/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadair/jupyter_environment_kernels/8b4ae6943ac1a29e4ea98d704fd2941d48cabf8f/environment_kernels/logos/python/logo-64x64.png -------------------------------------------------------------------------------- /environment_kernels/logos/r/logo-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cadair/jupyter_environment_kernels/8b4ae6943ac1a29e4ea98d704fd2941d48cabf8f/environment_kernels/logos/r/logo-64x64.png -------------------------------------------------------------------------------- /environment_kernels/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Compatibility tricks for Python 3. Mainly to do with unicode.""" 3 | 4 | # https://github.com/ipython/ipython/blob/master/IPython/utils/py3compat.py 5 | # Parts of it stolen from IPython under the 3-Clause BSD: 6 | # 7 | # ============================= 8 | # The IPython licensing terms 9 | # ============================= 10 | # 11 | # IPython is licensed under the terms of the Modified BSD License (also known as 12 | # New or Revised or 3-Clause BSD), as follows: 13 | # 14 | # - Copyright (c) 2008-2014, IPython Development Team 15 | # - Copyright (c) 2001-2007, Fernando Perez 16 | # - Copyright (c) 2001, Janko Hauser 17 | # - Copyright (c) 2001, Nathaniel Gray 18 | # 19 | # All rights reserved. 20 | # 21 | # Redistribution and use in source and binary forms, with or without 22 | # modification, are permitted provided that the following conditions are met: 23 | # 24 | # Redistributions of source code must retain the above copyright notice, this 25 | # list of conditions and the following disclaimer. 26 | # 27 | # Redistributions in binary form must reproduce the above copyright notice, this 28 | # list of conditions and the following disclaimer in the documentation and/or 29 | # other materials provided with the distribution. 30 | # 31 | # Neither the name of the IPython Development Team nor the names of its 32 | # contributors may be used to endorse or promote products derived from this 33 | # software without specific prior written permission. 34 | # 35 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 36 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 37 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 38 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 39 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 40 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 41 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 42 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 43 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 44 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | 46 | 47 | 48 | import os 49 | import sys 50 | import platform 51 | 52 | if sys.version_info[0] >= 3: 53 | PY3 = True 54 | FileNotFoundError = FileNotFoundError 55 | from collections.abc import MutableMapping as MutableMapping 56 | 57 | else: 58 | PY3 = False 59 | FileNotFoundError = IOError 60 | from collections import MutableMapping as MutableMapping 61 | 62 | PY2 = not PY3 63 | 64 | ON_DARWIN = platform.system() == 'Darwin' 65 | ON_LINUX = platform.system() == 'Linux' 66 | ON_WINDOWS = platform.system() == 'Windows' 67 | 68 | PYTHON_VERSION_INFO = sys.version_info[:3] 69 | ON_ANACONDA = any(s in sys.version for s in {'Anaconda', 'Continuum'}) 70 | 71 | ON_POSIX = (os.name == 'posix') 72 | 73 | try: 74 | import conda.config 75 | HAVE_CONDA = True 76 | except ImportError: 77 | HAVE_CONDA = False 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="environment_kernels", 5 | description="Launch Jupyter kernels installed in environments", 6 | url="https://github.com/Cadair/jupyter_environment_kernels/", 7 | author="Stuart Mumford", 8 | author_email="stuart@cadair.com", 9 | license="BSD", 10 | packages=['environment_kernels'], 11 | include_package_data=True, 12 | version="1.2.0", 13 | classifiers=[ 14 | "Topic :: Utilities", 15 | "License :: OSI Approved :: BSD License", 16 | ], 17 | ) 18 | --------------------------------------------------------------------------------