"""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 JSONFileConfigLoader, PyFileConfigLoader from traitlets.log import get_logger from .application import JupyterApp from .paths import jupyter_config_dir, jupyter_data_dir from .utils import ensure_dir_exists 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(f"Copying {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(f"Copying {src} -> {dst}") ensure_dir_exists(os.path.dirname(dst)) shutil.copy(src, dst) if substitutions: with open(dst, encoding="utf-8") as f: text = f.read() for pat, replacement in substitutions.items(): text = pat.sub(replacement, text) with open(dst, "w", encoding="utf-8") 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, encoding="utf-8") 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, encoding="utf-8") 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", encoding="utf-8") 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()