283 lines
8.2 KiB
Python
283 lines
8.2 KiB
Python
|
from __future__ import unicode_literals
|
||
|
|
||
|
"""Migrating IPython < 4.0 to Jupyter
|
||
|
|
||
|
This *copies* configuration and resources to their new locations in Jupyter
|
||
|
|
||
|
Migrations:
|
||
|
|
||
|
- .ipython/
|
||
|
- nbextensions -> JUPYTER_DATA_DIR/nbextensions
|
||
|
- kernels -> JUPYTER_DATA_DIR/kernels
|
||
|
- .ipython/profile_default/
|
||
|
- static/custom -> .jupyter/custom
|
||
|
- nbconfig -> .jupyter/nbconfig
|
||
|
- security/
|
||
|
- notebook_secret, notebook_cookie_secret, nbsignatures.db -> JUPYTER_DATA_DIR
|
||
|
- ipython_{notebook,nbconvert,qtconsole}_config.py -> .jupyter/jupyter_{name}_config.py
|
||
|
|
||
|
|
||
|
"""
|
||
|
|
||
|
# Copyright (c) Jupyter Development Team.
|
||
|
# Distributed under the terms of the Modified BSD License.
|
||
|
|
||
|
import os
|
||
|
import re
|
||
|
import shutil
|
||
|
from datetime import datetime
|
||
|
|
||
|
from traitlets.config import PyFileConfigLoader, JSONFileConfigLoader
|
||
|
from traitlets.log import get_logger
|
||
|
|
||
|
from .utils import ensure_dir_exists
|
||
|
|
||
|
from .paths import jupyter_config_dir, jupyter_data_dir
|
||
|
from .application import JupyterApp
|
||
|
|
||
|
pjoin = os.path.join
|
||
|
|
||
|
migrations = {
|
||
|
pjoin('{ipython_dir}', 'nbextensions'): pjoin('{jupyter_data}', 'nbextensions'),
|
||
|
pjoin('{ipython_dir}', 'kernels'): pjoin('{jupyter_data}', 'kernels'),
|
||
|
pjoin('{profile}', 'nbconfig'): pjoin('{jupyter_config}', 'nbconfig'),
|
||
|
}
|
||
|
|
||
|
custom_src_t = pjoin('{profile}', 'static', 'custom')
|
||
|
custom_dst_t = pjoin('{jupyter_config}', 'custom')
|
||
|
|
||
|
for security_file in ('notebook_secret', 'notebook_cookie_secret', 'nbsignatures.db'):
|
||
|
src = pjoin('{profile}', 'security', security_file)
|
||
|
dst = pjoin('{jupyter_data}', security_file)
|
||
|
migrations[src] = dst
|
||
|
|
||
|
config_migrations = ['notebook', 'nbconvert', 'qtconsole']
|
||
|
|
||
|
regex = re.compile
|
||
|
|
||
|
config_substitutions = {
|
||
|
regex(r'\bIPythonQtConsoleApp\b'): 'JupyterQtConsoleApp',
|
||
|
regex(r'\bIPythonWidget\b'): 'JupyterWidget',
|
||
|
regex(r'\bRichIPythonWidget\b'): 'RichJupyterWidget',
|
||
|
regex(r'\bIPython\.html\b'): 'notebook',
|
||
|
regex(r'\bIPython\.nbconvert\b'): 'nbconvert',
|
||
|
}
|
||
|
|
||
|
|
||
|
def get_ipython_dir():
|
||
|
"""Return the IPython directory location.
|
||
|
|
||
|
Not imported from IPython because the IPython implementation
|
||
|
ensures that a writable directory exists,
|
||
|
creating a temporary directory if not.
|
||
|
We don't want to trigger that when checking if migration should happen.
|
||
|
|
||
|
We only need to support the IPython < 4 behavior for migration,
|
||
|
so importing for forward-compatibility and edge cases is not important.
|
||
|
"""
|
||
|
return os.environ.get('IPYTHONDIR', os.path.expanduser('~/.ipython'))
|
||
|
|
||
|
|
||
|
def migrate_dir(src, dst):
|
||
|
"""Migrate a directory from src to dst"""
|
||
|
log = get_logger()
|
||
|
if not os.listdir(src):
|
||
|
log.debug("No files in %s" % src)
|
||
|
return False
|
||
|
if os.path.exists(dst):
|
||
|
if os.listdir(dst):
|
||
|
# already exists, non-empty
|
||
|
log.debug("%s already exists" % dst)
|
||
|
return False
|
||
|
else:
|
||
|
os.rmdir(dst)
|
||
|
log.info("Copying %s -> %s" % (src, dst))
|
||
|
ensure_dir_exists(os.path.dirname(dst))
|
||
|
shutil.copytree(src, dst, symlinks=True)
|
||
|
return True
|
||
|
|
||
|
|
||
|
def migrate_file(src, dst, substitutions=None):
|
||
|
"""Migrate a single file from src to dst
|
||
|
|
||
|
substitutions is an optional dict of {regex: replacement} for performing replacements on the file.
|
||
|
"""
|
||
|
log = get_logger()
|
||
|
if os.path.exists(dst):
|
||
|
# already exists
|
||
|
log.debug("%s already exists" % dst)
|
||
|
return False
|
||
|
log.info("Copying %s -> %s" % (src, dst))
|
||
|
ensure_dir_exists(os.path.dirname(dst))
|
||
|
shutil.copy(src, dst)
|
||
|
if substitutions:
|
||
|
with open(dst) as f:
|
||
|
text = f.read()
|
||
|
for pat, replacement in substitutions.items():
|
||
|
text = pat.sub(replacement, text)
|
||
|
with open(dst, 'w') as f:
|
||
|
f.write(text)
|
||
|
return True
|
||
|
|
||
|
|
||
|
def migrate_one(src, dst):
|
||
|
"""Migrate one item
|
||
|
|
||
|
dispatches to migrate_dir/_file
|
||
|
"""
|
||
|
log = get_logger()
|
||
|
if os.path.isfile(src):
|
||
|
return migrate_file(src, dst)
|
||
|
elif os.path.isdir(src):
|
||
|
return migrate_dir(src, dst)
|
||
|
else:
|
||
|
log.debug("Nothing to migrate for %s" % src)
|
||
|
return False
|
||
|
|
||
|
|
||
|
def migrate_static_custom(src, dst):
|
||
|
"""Migrate non-empty custom.js,css from src to dst
|
||
|
|
||
|
src, dst are 'custom' directories containing custom.{js,css}
|
||
|
"""
|
||
|
log = get_logger()
|
||
|
migrated = False
|
||
|
|
||
|
custom_js = pjoin(src, 'custom.js')
|
||
|
custom_css = pjoin(src, 'custom.css')
|
||
|
# check if custom_js is empty:
|
||
|
custom_js_empty = True
|
||
|
if os.path.isfile(custom_js):
|
||
|
with open(custom_js) as f:
|
||
|
js = f.read().strip()
|
||
|
for line in js.splitlines():
|
||
|
if not (
|
||
|
line.isspace()
|
||
|
or line.strip().startswith(('/*', '*', '//'))
|
||
|
):
|
||
|
custom_js_empty = False
|
||
|
break
|
||
|
|
||
|
# check if custom_css is empty:
|
||
|
custom_css_empty = True
|
||
|
if os.path.isfile(custom_css):
|
||
|
with open(custom_css) as f:
|
||
|
css = f.read().strip()
|
||
|
custom_css_empty = css.startswith('/*') and css.endswith('*/')
|
||
|
|
||
|
if custom_js_empty:
|
||
|
log.debug("Ignoring empty %s" % custom_js)
|
||
|
if custom_css_empty:
|
||
|
log.debug("Ignoring empty %s" % custom_css)
|
||
|
|
||
|
if custom_js_empty and custom_css_empty:
|
||
|
# nothing to migrate
|
||
|
return False
|
||
|
ensure_dir_exists(dst)
|
||
|
|
||
|
if not custom_js_empty or not custom_css_empty:
|
||
|
ensure_dir_exists(dst)
|
||
|
|
||
|
if not custom_js_empty:
|
||
|
if migrate_file(custom_js, pjoin(dst, 'custom.js')):
|
||
|
migrated = True
|
||
|
if not custom_css_empty:
|
||
|
if migrate_file(custom_css, pjoin(dst, 'custom.css')):
|
||
|
migrated = True
|
||
|
|
||
|
return migrated
|
||
|
|
||
|
|
||
|
def migrate_config(name, env):
|
||
|
"""Migrate a config file
|
||
|
|
||
|
Includes substitutions for updated configurable names.
|
||
|
"""
|
||
|
log = get_logger()
|
||
|
src_base = pjoin('{profile}', 'ipython_{name}_config').format(name=name, **env)
|
||
|
dst_base = pjoin('{jupyter_config}', 'jupyter_{name}_config').format(name=name, **env)
|
||
|
loaders = {
|
||
|
'.py': PyFileConfigLoader,
|
||
|
'.json': JSONFileConfigLoader,
|
||
|
}
|
||
|
migrated = []
|
||
|
for ext in ('.py', '.json'):
|
||
|
src = src_base + ext
|
||
|
dst = dst_base + ext
|
||
|
if os.path.exists(src):
|
||
|
cfg = loaders[ext](src).load_config()
|
||
|
if cfg:
|
||
|
if migrate_file(src, dst, substitutions=config_substitutions):
|
||
|
migrated.append(src)
|
||
|
else:
|
||
|
# don't migrate empty config files
|
||
|
log.debug("Not migrating empty config file: %s" % src)
|
||
|
return migrated
|
||
|
|
||
|
|
||
|
def migrate():
|
||
|
"""Migrate IPython configuration to Jupyter"""
|
||
|
env = {
|
||
|
'jupyter_data': jupyter_data_dir(),
|
||
|
'jupyter_config': jupyter_config_dir(),
|
||
|
'ipython_dir': get_ipython_dir(),
|
||
|
'profile': os.path.join(get_ipython_dir(), 'profile_default'),
|
||
|
}
|
||
|
migrated = False
|
||
|
for src_t, dst_t in migrations.items():
|
||
|
src = src_t.format(**env)
|
||
|
dst = dst_t.format(**env)
|
||
|
if os.path.exists(src):
|
||
|
if migrate_one(src, dst):
|
||
|
migrated = True
|
||
|
|
||
|
for name in config_migrations:
|
||
|
if migrate_config(name, env):
|
||
|
migrated = True
|
||
|
|
||
|
custom_src = custom_src_t.format(**env)
|
||
|
custom_dst = custom_dst_t.format(**env)
|
||
|
|
||
|
if os.path.exists(custom_src):
|
||
|
if migrate_static_custom(custom_src, custom_dst):
|
||
|
migrated = True
|
||
|
|
||
|
# write a marker to avoid re-running migration checks
|
||
|
ensure_dir_exists(env['jupyter_config'])
|
||
|
with open(os.path.join(env['jupyter_config'], 'migrated'), 'w') as f:
|
||
|
f.write(datetime.utcnow().isoformat())
|
||
|
|
||
|
return migrated
|
||
|
|
||
|
|
||
|
|
||
|
class JupyterMigrate(JupyterApp):
|
||
|
name = 'jupyter-migrate'
|
||
|
description = """
|
||
|
Migrate configuration and data from .ipython prior to 4.0 to Jupyter locations.
|
||
|
|
||
|
This migrates:
|
||
|
|
||
|
- config files in the default profile
|
||
|
- kernels in ~/.ipython/kernels
|
||
|
- notebook javascript extensions in ~/.ipython/extensions
|
||
|
- custom.js/css to .jupyter/custom
|
||
|
|
||
|
to their new Jupyter locations.
|
||
|
|
||
|
All files are copied, not moved.
|
||
|
If the destinations already exist, nothing will be done.
|
||
|
"""
|
||
|
|
||
|
def start(self):
|
||
|
if not migrate():
|
||
|
self.log.info("Found nothing to migrate.")
|
||
|
|
||
|
|
||
|
main = JupyterMigrate.launch_instance
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|
||
|
|