diff --git a/IPython/config/application.py b/IPython/config/application.py index ed13a37..be907ed 100644 --- a/IPython/config/application.py +++ b/IPython/config/application.py @@ -311,7 +311,7 @@ class Application(SingletonConfigurable): lines.append('-'*len(lines[0])) lines.append('') for p in wrap_paragraphs(self.subcommand_description.format( - app=os.path.basename(self.argv[0]))): + app=self.name)): lines.append(p) lines.append('') for subc, (cls, help) in iteritems(self.subcommands): diff --git a/IPython/core/application.py b/IPython/core/application.py index 5608de1..7770013 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -131,7 +131,7 @@ class BaseIPythonApplication(Application): help=""" The name of the IPython directory. This directory is used for logging configuration (through profiles), history storage, etc. The default - is usually $HOME/.ipython. This options can also be specified through + is usually $HOME/.ipython. This option can also be specified through the environment variable IPYTHONDIR. """ ) diff --git a/IPython/core/profileapp.py b/IPython/core/profileapp.py index 2a41258..d5a6604 100644 --- a/IPython/core/profileapp.py +++ b/IPython/core/profileapp.py @@ -294,7 +294,7 @@ class ProfileCreate(BaseIPythonApplication): class ProfileApp(Application): - name = u'ipython-profile' + name = u'ipython profile' description = profile_help examples = _main_examples diff --git a/IPython/kernel/kernelspec.py b/IPython/kernel/kernelspec.py index 0db302c..a8931e0 100644 --- a/IPython/kernel/kernelspec.py +++ b/IPython/kernel/kernelspec.py @@ -1,6 +1,7 @@ import io import json import os +import shutil import sys pjoin = os.path.join @@ -12,11 +13,13 @@ from IPython.utils.traitlets import HasTraits, List, Unicode, Dict if os.name == 'nt': programdata = os.environ.get('PROGRAMDATA', None) if programdata: - SYSTEM_KERNEL_DIR = pjoin(programdata, 'ipython', 'kernels') + SYSTEM_KERNEL_DIRS = [pjoin(programdata, 'ipython', 'kernels')] else: # PROGRAMDATA is not defined by default on XP. - SYSTEM_KERNEL_DIR = None + SYSTEM_KERNEL_DIRS = [] else: - SYSTEM_KERNEL_DIR = "/usr/share/ipython/kernels" + SYSTEM_KERNEL_DIRS = ["/usr/share/ipython/kernels", + "/usr/local/share/ipython/kernels", + ] NATIVE_KERNEL_NAME = 'python3' if PY3 else 'python2' @@ -79,8 +82,7 @@ class KernelSpecManager(HasTraits): help="List of kernel directories to search. Later ones take priority over earlier." ) def _kernel_dirs_default(self): - return [ - SYSTEM_KERNEL_DIR, + return SYSTEM_KERNEL_DIRS + [ self.user_kernel_dir, ] @@ -130,6 +132,38 @@ class KernelSpecManager(HasTraits): raise NoSuchKernel(kernel_name) return KernelSpec.from_resource_dir(resource_dir) + def install_kernel_spec(self, source_dir, kernel_name=None, system=False, + replace=False): + """Install a kernel spec by copying its directory. + + If ``kernel_name`` is not given, the basename of ``source_dir`` will + be used. + + If ``system`` is True, it will attempt to install into the systemwide + kernel registry. If the process does not have appropriate permissions, + an :exc:`OSError` will be raised. + + If ``replace`` is True, this will replace an existing kernel of the same + name. Otherwise, if the destination already exists, an :exc:`OSError` + will be raised. + """ + if not kernel_name: + kernel_name = os.path.basename(source_dir) + kernel_name = kernel_name.lower() + + if system: + if SYSTEM_KERNEL_DIRS: + destination = os.path.join(SYSTEM_KERNEL_DIRS[-1], kernel_name) + else: + raise EnvironmentError("No system kernel directory is available") + else: + destination = os.path.join(self.user_kernel_dir, kernel_name) + + if replace and os.path.isdir(destination): + shutil.rmtree(destination) + + shutil.copytree(source_dir, destination) + def find_kernel_specs(): """Returns a dict mapping kernel names to resource directories.""" return KernelSpecManager().find_kernel_specs() @@ -139,4 +173,9 @@ def get_kernel_spec(kernel_name): Raises KeyError if the given kernel name is not found. """ - return KernelSpecManager().get_kernel_spec(kernel_name) \ No newline at end of file + return KernelSpecManager().get_kernel_spec(kernel_name) + +def install_kernel_spec(source_dir, kernel_name=None, system=False): + return KernelSpecManager().install_kernel_spec(source_dir, kernel_name, system) + +install_kernel_spec.__doc__ = KernelSpecManager.install_kernel_spec.__doc__ diff --git a/IPython/kernel/kernelspecapp.py b/IPython/kernel/kernelspecapp.py new file mode 100644 index 0000000..ac4b837 --- /dev/null +++ b/IPython/kernel/kernelspecapp.py @@ -0,0 +1,121 @@ + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import errno +import os.path + +from IPython.config.application import Application +from IPython.core.application import ( + BaseIPythonApplication, base_flags, base_aliases +) +from IPython.utils.traitlets import Instance, Dict, Unicode, Bool + +from .kernelspec import KernelSpecManager + +def _pythonfirst(s): + "Sort key function that will put strings starting with 'python' first." + if s.startswith('python'): + # Space is not valid in kernel names, so this should sort first + return ' ' + s + return s + +class ListKernelSpecs(BaseIPythonApplication): + description = """List installed kernel specifications.""" + kernel_spec_manager = Instance(KernelSpecManager) + + # Not all of the base aliases are meaningful (e.g. profile) + aliases = {k: base_aliases[k] for k in ['ipython-dir', 'log-level']} + flags = {'debug': base_flags['debug'],} + + def _kernel_spec_manager_default(self): + return KernelSpecManager(ipython_dir=self.ipython_dir) + + def start(self): + print("Available kernels:") + for kernelname in sorted(self.kernel_spec_manager.find_kernel_specs(), + key=_pythonfirst): + print(" %s" % kernelname) + + +class InstallKernelSpec(BaseIPythonApplication): + description = """Install a kernel specification directory.""" + kernel_spec_manager = Instance(KernelSpecManager) + + def _kernel_spec_manager_default(self): + return KernelSpecManager(ipython_dir=self.ipython_dir) + + sourcedir = Unicode() + kernel_name = Unicode("", config=True, + help="Install the kernel spec with this name" + ) + def _kernel_name_default(self): + return os.path.basename(self.sourcedir) + + system = Bool(False, config=True, + help=""" + Try to install the kernel spec to the systemwide directory instead of + the per-user directory. + """ + ) + replace = Bool(False, config=True, + help="Replace any existing kernel spec with this name." + ) + + aliases = {'name': 'InstallKernelSpec.kernel_name'} + for k in ['ipython-dir', 'log-level']: + aliases[k] = base_aliases[k] + + flags = {'system': ({'InstallKernelSpec': {'system': True}}, + "Install to the systemwide kernel registry"), + 'replace': ({'InstallKernelSpec': {'replace': True}}, + "Replace any existing kernel spec with this name."), + 'debug': base_flags['debug'], + } + + def parse_command_line(self, argv): + super(InstallKernelSpec, self).parse_command_line(argv) + # accept positional arg as profile name + if self.extra_args: + self.sourcedir = self.extra_args[0] + else: + print("No source directory specified.") + self.exit(1) + + def start(self): + try: + self.kernel_spec_manager.install_kernel_spec(self.sourcedir, + kernel_name=self.kernel_name, + system=self.system, + replace=self.replace, + ) + except OSError as e: + if e.errno == errno.EACCES: + print("Permission denied") + self.exit(1) + elif e.errno == errno.EEXIST: + print("A kernel spec is already present at %s" % e.filename) + self.exit(1) + raise + +class KernelSpecApp(Application): + name = "ipython kernelspec" + description = """Manage IPython kernel specifications.""" + + subcommands = Dict(dict( + list = (ListKernelSpecs, ListKernelSpecs.description.splitlines()[0]), + install = (InstallKernelSpec, InstallKernelSpec.description.splitlines()[0]) + )) + + aliases = {} + flags = {} + + def start(self): + if self.subapp is None: + print("No subcommand specified. Must specify one of: %s"% list(self.subcommands)) + print() + self.print_description() + self.print_subcommands() + self.exit(1) + else: + return self.subapp.start() diff --git a/IPython/kernel/tests/test_kernelspec.py b/IPython/kernel/tests/test_kernelspec.py index 2479d10..710d8f9 100644 --- a/IPython/kernel/tests/test_kernelspec.py +++ b/IPython/kernel/tests/test_kernelspec.py @@ -3,6 +3,7 @@ import os from os.path import join as pjoin import unittest +from IPython.testing.decorators import onlyif from IPython.utils.tempdir import TemporaryDirectory from IPython.kernel import kernelspec @@ -13,7 +14,8 @@ sample_kernel_json = {'argv':['cat', '{connection_file}'], class KernelSpecTests(unittest.TestCase): def setUp(self): - self.tempdir = td = TemporaryDirectory() + td = TemporaryDirectory() + self.addCleanup(td.cleanup) self.sample_kernel_dir = pjoin(td.name, 'kernels', 'Sample') os.makedirs(self.sample_kernel_dir) json_file = pjoin(self.sample_kernel_dir, 'kernel.json') @@ -21,9 +23,12 @@ class KernelSpecTests(unittest.TestCase): json.dump(sample_kernel_json, f) self.ksm = kernelspec.KernelSpecManager(ipython_dir=td.name) - - def tearDown(self): - self.tempdir.cleanup() + + td2 = TemporaryDirectory() + self.addCleanup(td2.cleanup) + self.installable_kernel = td2.name + with open(pjoin(self.installable_kernel, 'kernel.json'), 'w') as f: + json.dump(sample_kernel_json, f) def test_find_kernel_specs(self): kernels = self.ksm.find_kernel_specs() @@ -36,4 +41,25 @@ class KernelSpecTests(unittest.TestCase): self.assertEqual(ks.display_name, sample_kernel_json['display_name']) self.assertEqual(ks.language, sample_kernel_json['language']) self.assertEqual(ks.codemirror_mode, sample_kernel_json['language']) - self.assertEqual(ks.env, {}) \ No newline at end of file + self.assertEqual(ks.env, {}) + + def test_install_kernel_spec(self): + self.ksm.install_kernel_spec(self.installable_kernel, + kernel_name='tstinstalled') + self.assertIn('tstinstalled', self.ksm.find_kernel_specs()) + + with self.assertRaises(OSError): + self.ksm.install_kernel_spec(self.installable_kernel, + kernel_name='tstinstalled') + + # Smoketest that this succeeds + self.ksm.install_kernel_spec(self.installable_kernel, + kernel_name='tstinstalled', + replace=True) + + @onlyif(os.name != 'nt' and not os.access('/usr/share', os.W_OK), "needs Unix system without root privileges") + def test_cant_install_kernel_spec(self): + with self.assertRaises(OSError): + self.ksm.install_kernel_spec(self.installable_kernel, + kernel_name='tstinstalled', + system=True) diff --git a/IPython/terminal/ipapp.py b/IPython/terminal/ipapp.py index 658b2cf..7de0339 100755 --- a/IPython/terminal/ipapp.py +++ b/IPython/terminal/ipapp.py @@ -250,6 +250,9 @@ class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp): trust=('IPython.nbformat.sign.TrustNotebookApp', "Sign notebooks to trust their potentially unsafe contents at load." ), + kernelspec=('IPython.kernel.kernelspecapp.KernelSpecApp', + "Manage IPython kernel specifications." + ), ) subcommands['install-nbextension'] = ( "IPython.html.nbextensions.NBExtensionApp",