Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add %pip and %conda magic functions #11524

Merged
merged 3 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion IPython/core/interactiveshell.py
Original file line number Diff line number Diff line change
Expand Up @@ -2220,7 +2220,8 @@ def init_magics(self):
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)
Expand Down
1 change: 1 addition & 0 deletions IPython/core/magics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .logging import LoggingMagics
from .namespace import NamespaceMagics
from .osm import OSMagics
from .packaging import PackagingMagics
from .pylab import PylabMagics
from .script import ScriptMagics

Expand Down
21 changes: 1 addition & 20 deletions IPython/core/magics/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def alias_magic(self, line=''):
@line_magic
def lsmagic(self, parameter_s=''):
"""List currently available magic functions."""
return MagicsDisplay(self.shell.magics_manager, ignore=[self.pip])
return MagicsDisplay(self.shell.magics_manager, ignore=[])

def _magic_docs(self, brief=False, rest=False):
"""Return docstrings from magic functions."""
Expand Down Expand Up @@ -379,25 +379,6 @@ def xmode_switch_err(name):
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 """
Expand Down
103 changes: 103 additions & 0 deletions IPython/core/magics/packaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""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')
Copy link
Member

@rgbkrk rgbkrk Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dhirschfeld would this correctly detect being in a conda environment on Windows?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better way might be to check if Anaconda is in sys.version. That should work cross-platform.

Copy link
Contributor Author

@jakevdp jakevdp Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that only works if you happen to be using the Python built by Anaconda. For example:

$ conda create -n forge-python
$ conda activate forge-python
$ conda install python --yes -c conda-forge
$ python -c "import sys; print(sys.version)"
3.7.1 | packaged by conda-forge | (default, Nov 13 2018, 09:50:42) 
[Clang 9.0.0 (clang-900.0.37)]

Copy link
Contributor

@MSeal MSeal Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yes you're right

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<command>.*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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this organization around args needing --y or the --prefix

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really is gloriously nice.

}
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]))
Copy link
Member

@rgbkrk rgbkrk Dec 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works pretty well, the only hidden API surface to this is that this allows the full pip API. We likely wouldn't be able to extend this with new args, possibly for something ipython / jupyter specific (capturing metadata, accepting a flag to do so).

Needless to say, I'm cool with this implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 We likely want to remind user they might need to restart the kernel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 594d000

print("Note: you may need to restart the kernel to use updated packages.")

@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))
print("\nNote: you may need to restart the kernel to use updated packages.")