upgrade_extension.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import json
  2. import os
  3. import os.path as osp
  4. from pathlib import Path
  5. import pkg_resources
  6. import shutil
  7. import sys
  8. import subprocess
  9. try:
  10. from cookiecutter.main import cookiecutter
  11. except ImportError:
  12. raise RuntimeError("Please install cookiecutter")
  13. COOKIECUTTER_BRANCH = "3.0"
  14. def update_extension(target, interactive=True):
  15. """Update an extension to the current JupyterLab
  16. target: str
  17. Path to the extension directory containing the extension
  18. interactive: bool [default: true]
  19. Whether to ask before overwriting content
  20. """
  21. # Input is a directory with a package.json or the current directory
  22. # Use the cookiecutter as the source
  23. # Pull in the relevant config
  24. # Pull in the Python parts if possible
  25. # Pull in the scripts if possible
  26. target = osp.abspath(target)
  27. package_file = osp.join(target, 'package.json')
  28. setup_file = osp.join(target, 'setup.py')
  29. if not osp.exists(package_file):
  30. raise RuntimeError('No package.json exists in %s' % target)
  31. # Infer the options from the current directory
  32. with open(package_file) as fid:
  33. data = json.load(fid)
  34. if osp.exists(setup_file):
  35. python_name = subprocess.check_output([sys.executable, 'setup.py', '--name'], cwd=target).decode('utf8').strip()
  36. else:
  37. python_name = data['name']
  38. if '@' in python_name:
  39. python_name = python_name[1:].replace('/', '_').replace('-', '_')
  40. output_dir = osp.join(target, '_temp_extension')
  41. if osp.exists(output_dir):
  42. shutil.rmtree(output_dir)
  43. # Build up the cookiecutter args and run the cookiecutter
  44. extra_context = dict(
  45. author_name = data.get('author', '<author_name>'),
  46. labextension_name = data['name'],
  47. project_short_description = data.get('description', '<description>'),
  48. has_server_extension = 'y' if osp.exists(osp.join(target, 'jupyter-config')) else 'n',
  49. has_binder = 'y' if osp.exists(osp.join(target, 'binder')) else 'n',
  50. repository = data.get('repository', {}).get('url', '<repository'),
  51. python_name = python_name
  52. )
  53. template = 'https://github.com/jupyterlab/extension-cookiecutter-ts'
  54. cookiecutter(template=template, checkout=COOKIECUTTER_BRANCH, output_dir=output_dir,
  55. extra_context=extra_context, no_input=not interactive)
  56. python_name = os.listdir(output_dir)[0]
  57. # hoist the output up one level
  58. shutil.move(osp.join(output_dir, python_name), osp.join(output_dir, '_temp'))
  59. for filename in os.listdir(osp.join(output_dir, '_temp')):
  60. shutil.move(osp.join(output_dir, '_temp', filename), osp.join(output_dir, filename))
  61. shutil.rmtree(osp.join(output_dir, '_temp'))
  62. # Check whether there are any phosphor dependencies
  63. has_phosphor = False
  64. for name in ['devDependencies', 'dependencies']:
  65. if name not in data:
  66. continue
  67. for (key, value) in data[name].items():
  68. if key.startswith('@phosphor/'):
  69. has_phosphor = True
  70. data[name][key.replace('@phosphor/', '@lumino/')] = value
  71. for key in list(data[name]):
  72. if key.startswith('@phosphor/'):
  73. del data[name][key]
  74. # From the created package.json grab the devDependencies
  75. with open(osp.join(output_dir, 'package.json')) as fid:
  76. temp_data = json.load(fid)
  77. for (key, value) in temp_data['devDependencies'].items():
  78. data['devDependencies'][key] = value
  79. # Ask the user whether to upgrade the scripts automatically
  80. warnings = []
  81. if interactive:
  82. choice = input('overwrite scripts in package.json? [n]: ')
  83. else:
  84. choice = 'y'
  85. if choice.upper().startswith('Y'):
  86. warnings.append('Updated scripts in package.json')
  87. data.setdefault('scripts', dict())
  88. for (key, value) in temp_data['scripts'].items():
  89. data['scripts'][key] = value
  90. if 'install-ext' in data['scripts']:
  91. del data['scripts']['install-ext']
  92. else:
  93. warnings.append('package.json scripts must be updated manually')
  94. # Set the output directory
  95. data['jupyterlab']['outputDir'] = temp_data['jupyterlab']['outputDir']
  96. # Look for resolutions in JupyterLab metadata and upgrade those as well
  97. root_jlab_package = pkg_resources.resource_filename('jupyterlab', 'staging/package.json')
  98. with open(root_jlab_package) as fid:
  99. root_jlab_data = json.load(fid)
  100. data.setdefault('dependencies', dict())
  101. data.setdefault('devDependencies', dict())
  102. for (key, value) in root_jlab_data['resolutions'].items():
  103. if key in data['dependencies']:
  104. data['dependencies'][key] = value.replace('~', '^')
  105. if key in data['devDependencies']:
  106. data['devDependencies'][key] = value.replace('~', '^')
  107. # Sort the entries
  108. for key in ['scripts', 'dependencies', 'devDependencies']:
  109. if data[key]:
  110. data[key] = dict(sorted(data[key].items()))
  111. else:
  112. del data[key]
  113. # Update the root package.json file
  114. with open(package_file, 'w') as fid:
  115. json.dump(data, fid, indent=2)
  116. # For the other files, ask about whether to override (when it exists)
  117. # At the end, list the files that were: added, overridden, skipped
  118. path = Path(output_dir)
  119. for p in path.rglob("*"):
  120. relpath = osp.relpath(p, path)
  121. if relpath == "package.json":
  122. continue
  123. if p.is_dir():
  124. continue
  125. file_target = osp.join(target, relpath)
  126. if not osp.exists(file_target):
  127. os.makedirs(osp.dirname(file_target), exist_ok=True)
  128. shutil.copy(p, file_target)
  129. else:
  130. with open(p) as fid:
  131. old_data = fid.read()
  132. with open(file_target) as fid:
  133. new_data = fid.read()
  134. if old_data == new_data:
  135. continue
  136. if interactive:
  137. choice = input('overwrite "%s"? [n]: ' % relpath)
  138. else:
  139. choice = 'n'
  140. if choice.upper().startswith('Y'):
  141. shutil.copy(p, file_target)
  142. else:
  143. warnings.append('skipped _temp_extension/%s' % relpath)
  144. # Print out all warnings
  145. for warning in warnings:
  146. print('**', warning)
  147. print('** Remove _temp_extensions directory when finished')
  148. if has_phosphor:
  149. print('** Phosphor dependencies were upgraded to lumino dependencies, update imports as needed')
  150. if __name__ == "__main__":
  151. import argparse
  152. parser = argparse.ArgumentParser(description='Upgrade a JupyterLab extension')
  153. parser.add_argument('--no-input',
  154. action='store_true',
  155. help='whether to prompt for information')
  156. parser.add_argument('path',
  157. action='store',
  158. type=str,
  159. help='the target path')
  160. args = parser.parse_args()
  161. update_extension(args.path, args.no_input==False)