migrate.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. from __future__ import unicode_literals
  2. """Migrating IPython < 4.0 to Jupyter
  3. This *copies* configuration and resources to their new locations in Jupyter
  4. Migrations:
  5. - .ipython/
  6. - nbextensions -> JUPYTER_DATA_DIR/nbextensions
  7. - kernels -> JUPYTER_DATA_DIR/kernels
  8. - .ipython/profile_default/
  9. - static/custom -> .jupyter/custom
  10. - nbconfig -> .jupyter/nbconfig
  11. - security/
  12. - notebook_secret, notebook_cookie_secret, nbsignatures.db -> JUPYTER_DATA_DIR
  13. - ipython_{notebook,nbconvert,qtconsole}_config.py -> .jupyter/jupyter_{name}_config.py
  14. """
  15. # Copyright (c) Jupyter Development Team.
  16. # Distributed under the terms of the Modified BSD License.
  17. import os
  18. import re
  19. import shutil
  20. from datetime import datetime
  21. from traitlets.config import PyFileConfigLoader, JSONFileConfigLoader
  22. from traitlets.log import get_logger
  23. from .utils import ensure_dir_exists
  24. from .paths import jupyter_config_dir, jupyter_data_dir
  25. from .application import JupyterApp
  26. pjoin = os.path.join
  27. migrations = {
  28. pjoin('{ipython_dir}', 'nbextensions'): pjoin('{jupyter_data}', 'nbextensions'),
  29. pjoin('{ipython_dir}', 'kernels'): pjoin('{jupyter_data}', 'kernels'),
  30. pjoin('{profile}', 'nbconfig'): pjoin('{jupyter_config}', 'nbconfig'),
  31. }
  32. custom_src_t = pjoin('{profile}', 'static', 'custom')
  33. custom_dst_t = pjoin('{jupyter_config}', 'custom')
  34. for security_file in ('notebook_secret', 'notebook_cookie_secret', 'nbsignatures.db'):
  35. src = pjoin('{profile}', 'security', security_file)
  36. dst = pjoin('{jupyter_data}', security_file)
  37. migrations[src] = dst
  38. config_migrations = ['notebook', 'nbconvert', 'qtconsole']
  39. regex = re.compile
  40. config_substitutions = {
  41. regex(r'\bIPythonQtConsoleApp\b'): 'JupyterQtConsoleApp',
  42. regex(r'\bIPythonWidget\b'): 'JupyterWidget',
  43. regex(r'\bRichIPythonWidget\b'): 'RichJupyterWidget',
  44. regex(r'\bIPython\.html\b'): 'notebook',
  45. regex(r'\bIPython\.nbconvert\b'): 'nbconvert',
  46. }
  47. def get_ipython_dir():
  48. """Return the IPython directory location.
  49. Not imported from IPython because the IPython implementation
  50. ensures that a writable directory exists,
  51. creating a temporary directory if not.
  52. We don't want to trigger that when checking if migration should happen.
  53. We only need to support the IPython < 4 behavior for migration,
  54. so importing for forward-compatibility and edge cases is not important.
  55. """
  56. return os.environ.get('IPYTHONDIR', os.path.expanduser('~/.ipython'))
  57. def migrate_dir(src, dst):
  58. """Migrate a directory from src to dst"""
  59. log = get_logger()
  60. if not os.listdir(src):
  61. log.debug("No files in %s" % src)
  62. return False
  63. if os.path.exists(dst):
  64. if os.listdir(dst):
  65. # already exists, non-empty
  66. log.debug("%s already exists" % dst)
  67. return False
  68. else:
  69. os.rmdir(dst)
  70. log.info("Copying %s -> %s" % (src, dst))
  71. ensure_dir_exists(os.path.dirname(dst))
  72. shutil.copytree(src, dst, symlinks=True)
  73. return True
  74. def migrate_file(src, dst, substitutions=None):
  75. """Migrate a single file from src to dst
  76. substitutions is an optional dict of {regex: replacement} for performing replacements on the file.
  77. """
  78. log = get_logger()
  79. if os.path.exists(dst):
  80. # already exists
  81. log.debug("%s already exists" % dst)
  82. return False
  83. log.info("Copying %s -> %s" % (src, dst))
  84. ensure_dir_exists(os.path.dirname(dst))
  85. shutil.copy(src, dst)
  86. if substitutions:
  87. with open(dst) as f:
  88. text = f.read()
  89. for pat, replacement in substitutions.items():
  90. text = pat.sub(replacement, text)
  91. with open(dst, 'w') as f:
  92. f.write(text)
  93. return True
  94. def migrate_one(src, dst):
  95. """Migrate one item
  96. dispatches to migrate_dir/_file
  97. """
  98. log = get_logger()
  99. if os.path.isfile(src):
  100. return migrate_file(src, dst)
  101. elif os.path.isdir(src):
  102. return migrate_dir(src, dst)
  103. else:
  104. log.debug("Nothing to migrate for %s" % src)
  105. return False
  106. def migrate_static_custom(src, dst):
  107. """Migrate non-empty custom.js,css from src to dst
  108. src, dst are 'custom' directories containing custom.{js,css}
  109. """
  110. log = get_logger()
  111. migrated = False
  112. custom_js = pjoin(src, 'custom.js')
  113. custom_css = pjoin(src, 'custom.css')
  114. # check if custom_js is empty:
  115. custom_js_empty = True
  116. if os.path.isfile(custom_js):
  117. with open(custom_js) as f:
  118. js = f.read().strip()
  119. for line in js.splitlines():
  120. if not (
  121. line.isspace()
  122. or line.strip().startswith(('/*', '*', '//'))
  123. ):
  124. custom_js_empty = False
  125. break
  126. # check if custom_css is empty:
  127. custom_css_empty = True
  128. if os.path.isfile(custom_css):
  129. with open(custom_css) as f:
  130. css = f.read().strip()
  131. custom_css_empty = css.startswith('/*') and css.endswith('*/')
  132. if custom_js_empty:
  133. log.debug("Ignoring empty %s" % custom_js)
  134. if custom_css_empty:
  135. log.debug("Ignoring empty %s" % custom_css)
  136. if custom_js_empty and custom_css_empty:
  137. # nothing to migrate
  138. return False
  139. ensure_dir_exists(dst)
  140. if not custom_js_empty or not custom_css_empty:
  141. ensure_dir_exists(dst)
  142. if not custom_js_empty:
  143. if migrate_file(custom_js, pjoin(dst, 'custom.js')):
  144. migrated = True
  145. if not custom_css_empty:
  146. if migrate_file(custom_css, pjoin(dst, 'custom.css')):
  147. migrated = True
  148. return migrated
  149. def migrate_config(name, env):
  150. """Migrate a config file
  151. Includes substitutions for updated configurable names.
  152. """
  153. log = get_logger()
  154. src_base = pjoin('{profile}', 'ipython_{name}_config').format(name=name, **env)
  155. dst_base = pjoin('{jupyter_config}', 'jupyter_{name}_config').format(name=name, **env)
  156. loaders = {
  157. '.py': PyFileConfigLoader,
  158. '.json': JSONFileConfigLoader,
  159. }
  160. migrated = []
  161. for ext in ('.py', '.json'):
  162. src = src_base + ext
  163. dst = dst_base + ext
  164. if os.path.exists(src):
  165. cfg = loaders[ext](src).load_config()
  166. if cfg:
  167. if migrate_file(src, dst, substitutions=config_substitutions):
  168. migrated.append(src)
  169. else:
  170. # don't migrate empty config files
  171. log.debug("Not migrating empty config file: %s" % src)
  172. return migrated
  173. def migrate():
  174. """Migrate IPython configuration to Jupyter"""
  175. env = {
  176. 'jupyter_data': jupyter_data_dir(),
  177. 'jupyter_config': jupyter_config_dir(),
  178. 'ipython_dir': get_ipython_dir(),
  179. 'profile': os.path.join(get_ipython_dir(), 'profile_default'),
  180. }
  181. migrated = False
  182. for src_t, dst_t in migrations.items():
  183. src = src_t.format(**env)
  184. dst = dst_t.format(**env)
  185. if os.path.exists(src):
  186. if migrate_one(src, dst):
  187. migrated = True
  188. for name in config_migrations:
  189. if migrate_config(name, env):
  190. migrated = True
  191. custom_src = custom_src_t.format(**env)
  192. custom_dst = custom_dst_t.format(**env)
  193. if os.path.exists(custom_src):
  194. if migrate_static_custom(custom_src, custom_dst):
  195. migrated = True
  196. # write a marker to avoid re-running migration checks
  197. ensure_dir_exists(env['jupyter_config'])
  198. with open(os.path.join(env['jupyter_config'], 'migrated'), 'w') as f:
  199. f.write(datetime.utcnow().isoformat())
  200. return migrated
  201. class JupyterMigrate(JupyterApp):
  202. name = 'jupyter-migrate'
  203. description = """
  204. Migrate configuration and data from .ipython prior to 4.0 to Jupyter locations.
  205. This migrates:
  206. - config files in the default profile
  207. - kernels in ~/.ipython/kernels
  208. - notebook javascript extensions in ~/.ipython/extensions
  209. - custom.js/css to .jupyter/custom
  210. to their new Jupyter locations.
  211. All files are copied, not moved.
  212. If the destinations already exist, nothing will be done.
  213. """
  214. def start(self):
  215. if not migrate():
  216. self.log.info("Found nothing to migrate.")
  217. main = JupyterMigrate.launch_instance
  218. if __name__ == '__main__':
  219. main()