|
|
|
|
|
|
|
import configparser |
|
import json |
|
import re |
|
import shutil |
|
import subprocess |
|
import sys |
|
from typing import Optional |
|
|
|
try: |
|
import tomllib |
|
except ImportError: |
|
import tomli as tomllib |
|
|
|
try: |
|
from importlib.resources import files |
|
except ImportError: |
|
from importlib_resources import files |
|
from pathlib import Path |
|
|
|
try: |
|
import copier |
|
except ModuleNotFoundError: |
|
msg = "Please install copier; you can use `pip install jupyterlab[upgrade-extension]`" |
|
raise RuntimeError(msg) from None |
|
|
|
|
|
RECOMMENDED_TO_OVERRIDE = [ |
|
".github/workflows/binder-on-pr.yml", |
|
".github/workflows/build.yml", |
|
".github/workflows/check-release.yml", |
|
".github/workflows/enforce-label.yml", |
|
".github/workflows/prep-release.yml", |
|
".github/workflows/publish-release.yml", |
|
".github/workflows/update-integration-tests.yml", |
|
"binder/postBuild", |
|
".eslintignore", |
|
".eslintrc.js", |
|
".gitignore", |
|
".prettierignore", |
|
".prettierrc", |
|
".stylelintrc", |
|
"RELEASE.md", |
|
"babel.config.js", |
|
"conftest.py", |
|
"jest.config.js", |
|
"pyproject.toml", |
|
"setup.py", |
|
"tsconfig.json", |
|
"tsconfig.test.json", |
|
"ui-tests/README.md", |
|
"ui-tests/jupyter_server_test_config.py", |
|
"ui-tests/package.json", |
|
"ui-tests/playwright.config.js", |
|
] |
|
|
|
JUPYTER_SERVER_REQUIREMENT = re.compile("^jupyter_server([^\\w]|$)") |
|
|
|
|
|
def update_extension( |
|
target: str, vcs_ref: Optional[str] = None, interactive: bool = True |
|
) -> None: |
|
"""Update an extension to the current JupyterLab |
|
|
|
target: str |
|
Path to the extension directory containing the extension |
|
vcs_ref: str [default: None] |
|
Template vcs_ref to checkout |
|
interactive: bool [default: true] |
|
Whether to ask before overwriting content |
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
target = Path(target).resolve() |
|
package_file = target / "package.json" |
|
pyproject_file = target / "pyproject.toml" |
|
setup_file = target / "setup.py" |
|
if not package_file.exists(): |
|
msg = f"No package.json exists in {target!s}" |
|
raise RuntimeError(msg) |
|
|
|
|
|
with open(package_file) as fid: |
|
data = json.load(fid) |
|
|
|
python_name = None |
|
if pyproject_file.exists(): |
|
pyproject = tomllib.loads(pyproject_file.read_text()) |
|
python_name = pyproject.get("project", {}).get("name") |
|
|
|
if python_name is None: |
|
if setup_file.exists(): |
|
python_name = ( |
|
subprocess.check_output( |
|
[sys.executable, "setup.py", "--name"], |
|
cwd=target, |
|
) |
|
.decode("utf8") |
|
.strip() |
|
) |
|
else: |
|
python_name = data["name"] |
|
if "@" in python_name: |
|
python_name = python_name[1:] |
|
|
|
python_name = python_name.replace("/", "_").replace("-", "_") |
|
|
|
output_dir = target / "_temp_extension" |
|
if output_dir.exists(): |
|
shutil.rmtree(output_dir) |
|
|
|
|
|
author = data.get("author", "<author_name>") |
|
author_email = "" |
|
if isinstance(author, dict): |
|
author_name = author.get("name", "<author_name>") |
|
author_email = author.get("email", author_email) |
|
else: |
|
author_name = author |
|
|
|
kind = "frontend" |
|
if (target / "jupyter-config").exists(): |
|
kind = "server" |
|
elif data.get("jupyterlab", {}).get("themePath", ""): |
|
kind = "theme" |
|
|
|
has_test = ( |
|
(target / "conftest.py").exists() |
|
or (target / "jest.config.js").exists() |
|
or (target / "ui-tests").exists() |
|
) |
|
|
|
extra_context = { |
|
"kind": kind, |
|
"author_name": author_name, |
|
"author_email": author_email, |
|
"labextension_name": data["name"], |
|
"python_name": python_name, |
|
"project_short_description": data.get("description", "<description>"), |
|
"has_settings": bool(data.get("jupyterlab", {}).get("schemaDir", "")), |
|
"has_binder": bool((target / "binder").exists()), |
|
"test": bool(has_test), |
|
"repository": data.get("repository", {}).get("url", "<repository"), |
|
} |
|
|
|
template = "https://github.com/jupyterlab/extension-template" |
|
if tuple(copier.__version__.split(".")) < ("8", "0", "0"): |
|
copier.run_auto(template, output_dir, vcs_ref=vcs_ref, data=extra_context, defaults=True) |
|
else: |
|
copier.run_copy( |
|
template, output_dir, vcs_ref=vcs_ref, data=extra_context, defaults=True, unsafe=True |
|
) |
|
|
|
|
|
with (output_dir / "package.json").open() as fid: |
|
temp_data = json.load(fid) |
|
|
|
if data.get("devDependencies"): |
|
for key, value in temp_data["devDependencies"].items(): |
|
data["devDependencies"][key] = value |
|
else: |
|
data["devDependencies"] = temp_data["devDependencies"].copy() |
|
|
|
|
|
warnings = [] |
|
choice = input("Overwrite scripts in package.json? [n]: ") if interactive else "y" |
|
if choice.upper().startswith("Y"): |
|
warnings.append("Updated scripts in package.json") |
|
data.setdefault("scripts", {}) |
|
for key, value in temp_data["scripts"].items(): |
|
data["scripts"][key] = value |
|
if "install-ext" in data["scripts"]: |
|
del data["scripts"]["install-ext"] |
|
if "prepare" in data["scripts"]: |
|
del data["scripts"]["prepare"] |
|
else: |
|
warnings.append("package.json scripts must be updated manually") |
|
|
|
|
|
data["jupyterlab"]["outputDir"] = temp_data["jupyterlab"]["outputDir"] |
|
|
|
|
|
|
|
linters = { |
|
"eslintConfig": ".eslintrc.js", |
|
"eslintIgnore": ".eslintignore", |
|
"prettier": ".prettierrc", |
|
"stylelint": ".stylelintrc", |
|
} |
|
|
|
for key, file in linters.items(): |
|
if key in temp_data: |
|
data[key] = temp_data[key] |
|
|
|
linter_file = target / file |
|
if linter_file.exists(): |
|
linter_file.unlink() |
|
warnings.append(f"DELETED {file}") |
|
|
|
|
|
root_jlab_package = files("jupyterlab").joinpath("staging/package.json") |
|
with root_jlab_package.open() as fid: |
|
root_jlab_data = json.load(fid) |
|
|
|
data.setdefault("dependencies", {}) |
|
data.setdefault("devDependencies", {}) |
|
for key, value in root_jlab_data["resolutions"].items(): |
|
if key in data["dependencies"]: |
|
data["dependencies"][key] = value.replace("~", "^") |
|
if key in data["devDependencies"]: |
|
data["devDependencies"][key] = value.replace("~", "^") |
|
|
|
|
|
for key in ["scripts", "dependencies", "devDependencies"]: |
|
if data[key]: |
|
data[key] = dict(sorted(data[key].items())) |
|
else: |
|
del data[key] |
|
|
|
|
|
data.setdefault("styleModule", "style/index.js") |
|
if isinstance(data.get("sideEffects"), list) and "style/index.js" not in data["sideEffects"]: |
|
data["sideEffects"].append("style/index.js") |
|
if "files" in data and "style/index.js" not in data["files"]: |
|
data["files"].append("style/index.js") |
|
|
|
|
|
package_file.write_text(json.dumps(data, indent=2)) |
|
|
|
override_pyproject = False |
|
|
|
|
|
for p in output_dir.rglob("*"): |
|
relpath = p.relative_to(output_dir) |
|
if str(relpath) == "package.json": |
|
continue |
|
if p.is_dir(): |
|
continue |
|
file_target = target / relpath |
|
if not file_target.exists(): |
|
file_target.parent.mkdir(parents=True, exist_ok=True) |
|
shutil.copy(p, file_target) |
|
if file_target.name == "pyproject.toml": |
|
override_pyproject = True |
|
else: |
|
old_data = p.read_bytes() |
|
new_data = file_target.read_bytes() |
|
if old_data == new_data: |
|
continue |
|
default = "y" if relpath.as_posix() in RECOMMENDED_TO_OVERRIDE else "n" |
|
choice = ( |
|
(input(f'overwrite "{relpath!s}"? [{default}]: ') or default) |
|
if interactive |
|
else "n" |
|
) |
|
if choice.upper().startswith("Y"): |
|
shutil.copy(p, file_target) |
|
if file_target.name == "pyproject.toml": |
|
override_pyproject = True |
|
else: |
|
warnings.append(f"skipped _temp_extension/{relpath!s}") |
|
|
|
if override_pyproject: |
|
if (target / "setup.cfg").exists(): |
|
try: |
|
import tomli_w |
|
except ImportError: |
|
msg = "To update pyproject.toml, you need to install tomli-w" |
|
print(msg) |
|
else: |
|
config = configparser.ConfigParser() |
|
with (target / "setup.cfg").open() as setup_cfg_file: |
|
config.read_file(setup_cfg_file) |
|
|
|
pyproject_file = target / "pyproject.toml" |
|
pyproject = tomllib.loads(pyproject_file.read_text()) |
|
|
|
|
|
requirements_raw = config.get("options", "install_requires", fallback=None) |
|
if requirements_raw is not None: |
|
requirements = list( |
|
filter( |
|
lambda r: r and JUPYTER_SERVER_REQUIREMENT.match(r) is None, |
|
requirements_raw.splitlines(), |
|
) |
|
) |
|
else: |
|
requirements = [] |
|
|
|
pyproject["project"]["dependencies"] = ( |
|
pyproject["project"].get("dependencies", []) + requirements |
|
) |
|
|
|
|
|
if config.has_section("options.extras_require"): |
|
for extra, deps_raw in config.items("options.extras_require"): |
|
deps = list(filter(lambda r: r, deps_raw.splitlines())) |
|
if extra in pyproject["project"].get("optional-dependencies", {}): |
|
if pyproject["project"].get("optional-dependencies") is None: |
|
pyproject["project"]["optional-dependencies"] = {} |
|
deps = pyproject["project"]["optional-dependencies"][extra] + deps |
|
pyproject["project"]["optional-dependencies"][extra] = deps |
|
|
|
pyproject_file.write_text(tomli_w.dumps(pyproject)) |
|
(target / "setup.cfg").unlink() |
|
warnings.append("DELETED setup.cfg") |
|
|
|
manifest_in = target / "MANIFEST.in" |
|
if manifest_in.exists(): |
|
manifest_in.unlink() |
|
warnings.append("DELETED MANIFEST.in") |
|
|
|
|
|
for warning in warnings: |
|
print("**", warning) |
|
|
|
print("** Remove _temp_extensions directory when finished") |
|
|
|
|
|
if __name__ == "__main__": |
|
import argparse |
|
|
|
parser = argparse.ArgumentParser(description="Upgrade a JupyterLab extension") |
|
|
|
parser.add_argument("--no-input", action="store_true", help="whether to prompt for information") |
|
|
|
parser.add_argument("path", action="store", type=str, help="the target path") |
|
|
|
parser.add_argument("--vcs-ref", help="the template hash to checkout", default=None) |
|
|
|
args = parser.parse_args() |
|
|
|
answer_file = Path(args.path) / ".copier-answers.yml" |
|
|
|
if answer_file.exists(): |
|
msg = "This script won't do anything for copier template, instead execute in your extension directory:\n\n copier update" |
|
if tuple(copier.__version__.split(".")) >= ("8", "0", "0"): |
|
msg += " --trust" |
|
print(msg) |
|
else: |
|
update_extension(args.path, args.vcs_ref, args.no_input is False) |
|
|