| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- # PYTHON_ARGCOMPLETE_OK
- """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.
- from __future__ import annotations
- import os
- import re
- import shutil
- from datetime import datetime, timezone
- from pathlib import Path
- from typing import Any
- from traitlets.config.loader 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
- # mypy: disable-error-code="no-untyped-call"
- migrations = {
- str(Path("{ipython_dir}", "nbextensions")): str(Path("{jupyter_data}", "nbextensions")),
- str(Path("{ipython_dir}", "kernels")): str(Path("{jupyter_data}", "kernels")),
- str(Path("{profile}", "nbconfig")): str(Path("{jupyter_config}", "nbconfig")),
- }
- custom_src_t = str(Path("{profile}", "static", "custom"))
- custom_dst_t = str(Path("{jupyter_config}", "custom"))
- for security_file in ("notebook_secret", "notebook_cookie_secret", "nbsignatures.db"):
- src = str(Path("{profile}", "security", security_file))
- dst = str(Path("{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() -> str:
- """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", str(Path("~/.ipython").expanduser()))
- def migrate_dir(src: str, dst: str) -> bool:
- """Migrate a directory from src to dst"""
- log = get_logger()
- src_path = Path(src)
- dst_path = Path(dst)
- if not any(src_path.iterdir()):
- log.debug("No files in %s", src)
- return False
- if dst_path.exists():
- if any(dst_path.iterdir()):
- # already exists, non-empty
- log.debug("%s already exists", dst)
- return False
- dst_path.rmdir()
- log.info("Copying %s -> %s", src, dst)
- ensure_dir_exists(dst_path.parent)
- shutil.copytree(src, dst, symlinks=True)
- return True
- def migrate_file(src: str | Path, dst: str | Path, substitutions: Any = None) -> bool:
- """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()
- dst_path = Path(dst)
- if dst_path.exists():
- # already exists
- log.debug("%s already exists", dst)
- return False
- log.info("Copying %s -> %s", src, dst)
- ensure_dir_exists(dst_path.parent)
- shutil.copy(src, dst)
- if substitutions:
- with dst_path.open() as f:
- text = f.read()
- for pat, replacement in substitutions.items():
- text = pat.sub(replacement, text)
- with dst_path.open("w") as f:
- f.write(text)
- return True
- def migrate_one(src: str, dst: str) -> bool:
- """Migrate one item
- dispatches to migrate_dir/_file
- """
- log = get_logger()
- if Path(src).is_file():
- return migrate_file(src, dst)
- if Path(src).is_dir():
- return migrate_dir(src, dst)
- log.debug("Nothing to migrate for %s", src)
- return False
- def migrate_static_custom(src: str, dst: str) -> bool:
- """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 = Path(src, "custom.js")
- custom_css = Path(src, "custom.css")
- # check if custom_js is empty:
- custom_js_empty = True
- if Path(custom_js).is_file():
- with Path.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 Path(custom_css).is_file():
- with Path.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 and migrate_file(custom_js, Path(dst, "custom.js")):
- migrated = True
- if not custom_css_empty and migrate_file(custom_css, Path(dst, "custom.css")):
- migrated = True
- return migrated
- def migrate_config(name: str, env: Any) -> list[Any]:
- """Migrate a config file.
- Includes substitutions for updated configurable names.
- """
- log = get_logger()
- src_base = str(Path(f"{env['profile']}", f"ipython_{name}_config"))
- dst_base = str(Path(f"{env['jupyter_config']}", f"jupyter_{name}_config"))
- loaders = {
- ".py": PyFileConfigLoader,
- ".json": JSONFileConfigLoader,
- }
- migrated = []
- for ext in (".py", ".json"):
- src = src_base + ext
- dst = dst_base + ext
- if Path(src).exists():
- 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() -> bool:
- """Migrate IPython configuration to Jupyter"""
- env = {
- "jupyter_data": jupyter_data_dir(),
- "jupyter_config": jupyter_config_dir(),
- "ipython_dir": get_ipython_dir(),
- "profile": str(Path(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 Path(src).exists() and 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 Path(custom_src).exists() and 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 Path.open(Path(env["jupyter_config"], "migrated"), "w", encoding="utf-8") as f:
- f.write(datetime.now(tz=timezone.utc).isoformat())
- return migrated
- class JupyterMigrate(JupyterApp):
- """A Jupyter Migration App."""
- 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) -> None:
- """Start the application."""
- if not migrate():
- self.log.info("Found nothing to migrate.")
- main = JupyterMigrate.launch_instance
- if __name__ == "__main__":
- main()
|