diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index ac46abe..1d7dceb 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -2220,7 +2220,8 @@ class InteractiveShell(SingletonConfigurable): self.register_magics(m.AutoMagics, m.BasicMagics, m.CodeMagics, m.ConfigMagics, m.DisplayMagics, m.ExecutionMagics, m.ExtensionMagics, m.HistoryMagics, m.LoggingMagics, - m.NamespaceMagics, m.OSMagics, m.PylabMagics, m.ScriptMagics, + m.NamespaceMagics, m.OSMagics, m.PackagingMagics, + m.PylabMagics, m.ScriptMagics, ) if sys.version_info >(3,5): self.register_magics(m.AsyncMagics) diff --git a/IPython/core/magics/__init__.py b/IPython/core/magics/__init__.py index 841f4da..a6c5f47 100644 --- a/IPython/core/magics/__init__.py +++ b/IPython/core/magics/__init__.py @@ -24,6 +24,7 @@ from .history import HistoryMagics from .logging import LoggingMagics from .namespace import NamespaceMagics from .osm import OSMagics +from .packaging import PackagingMagics from .pylab import PylabMagics from .script import ScriptMagics diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index 225c49a..c7bd006 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -379,25 +379,6 @@ Currently the magic system has the following functions:""", except: xmode_switch_err('user') - - - @line_magic - def pip(self, args=''): - """ - Intercept usage of ``pip`` in IPython and direct user to run command outside of IPython. - """ - print(textwrap.dedent(''' - The following command must be run outside of the IPython shell: - - $ pip {args} - - The Python package manager (pip) can only be used from outside of IPython. - Please reissue the `pip` command in a separate terminal or command prompt. - - See the Python documentation for more information on how to install packages: - - https://docs.python.org/3/installing/'''.format(args=args))) - @line_magic def quickref(self, arg): """ Show a quick reference sheet """ diff --git a/IPython/core/magics/packaging.py b/IPython/core/magics/packaging.py new file mode 100644 index 0000000..fffb9d7 --- /dev/null +++ b/IPython/core/magics/packaging.py @@ -0,0 +1,101 @@ +"""Implementation of packaging-related magic functions. +""" +#----------------------------------------------------------------------------- +# Copyright (c) 2018 The IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + +import os +import re +import shlex +import sys +from subprocess import Popen, PIPE + +from IPython.core.magic import Magics, magics_class, line_magic + + +def _is_conda_environment(): + """Return True if the current Python executable is in a conda env""" + # TODO: does this need to change on windows? + conda_history = os.path.join(sys.prefix, 'conda-meta', 'history') + return os.path.exists(conda_history) + + +def _get_conda_executable(): + """Find the path to the conda executable""" + # Check if there is a conda executable in the same directory as the Python executable. + # This is the case within conda's root environment. + conda = os.path.join(os.path.dirname(sys.executable), 'conda') + if os.path.isfile(conda): + return conda + + # Otherwise, attempt to extract the executable from conda history. + # This applies in any conda environment. + R = re.compile(r"^#\s*cmd:\s*(?P.*conda)\s[create|install]") + for line in open(os.path.join(sys.prefix, 'conda-meta', 'history')): + match = R.match(line) + if match: + return match.groupdict()['command'] + + # Fallback: assume conda is available on the system path. + return "conda" + + +CONDA_COMMANDS_REQUIRING_PREFIX = { + 'install', 'list', 'remove', 'uninstall', 'update', 'upgrade', +} +CONDA_COMMANDS_REQUIRING_YES = { + 'install', 'remove', 'uninstall', 'update', 'upgrade', +} +CONDA_ENV_FLAGS = {'-p', '--prefix', '-n', '--name'} +CONDA_YES_FLAGS = {'-y', '--y'} + + +@magics_class +class PackagingMagics(Magics): + """Magics related to packaging & installation""" + + @line_magic + def pip(self, line): + """Run the pip package manager within the current kernel. + + Usage: + %pip install [pkgs] + """ + self.shell.system(' '.join([sys.executable, '-m', 'pip', line])) + + @line_magic + def conda(self, line): + """Run the conda package manager within the current kernel. + + Usage: + %conda install [pkgs] + """ + if not _is_conda_environment(): + raise ValueError("The python kernel does not appear to be a conda environment. " + "Please use ``%pip install`` instead.") + + conda = _get_conda_executable() + args = shlex.split(line) + command = args[0] + args = args[1:] + extra_args = [] + + # When the subprocess does not allow us to respond "yes" during the installation, + # we need to insert --yes in the argument list for some commands + stdin_disabled = getattr(self.shell, 'kernel', None) is not None + needs_yes = command in CONDA_COMMANDS_REQUIRING_YES + has_yes = set(args).intersection(CONDA_YES_FLAGS) + if stdin_disabled and needs_yes and not has_yes: + extra_args.append("--yes") + + # Add --prefix to point conda installation to the current environment + needs_prefix = command in CONDA_COMMANDS_REQUIRING_PREFIX + has_prefix = set(args).intersection(CONDA_ENV_FLAGS) + if needs_prefix and not has_prefix: + extra_args.extend(["--prefix", sys.prefix]) + + self.shell.system(' '.join([conda, command] + extra_args + args)) \ No newline at end of file