| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- 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
- 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
- # List of files recommended to be overridden
- 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( # noqa
- 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
- """
- # Input is a directory with a package.json or the current directory
- # Use the extension template as the source
- # Pull in the relevant config
- # Pull in the Python parts if possible
- # Pull in the scripts if possible
- 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)
- # Infer the options from the current directory
- 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( # noqa: S603
- [sys.executable, "setup.py", "--name"],
- cwd=target,
- )
- .decode("utf8")
- .strip()
- )
- else:
- python_name = data["name"]
- if "@" in python_name:
- python_name = python_name[1:]
- # Clean up the name to be valid package module name
- python_name = python_name.replace("/", "_").replace("-", "_")
- output_dir = target / "_temp_extension"
- if output_dir.exists():
- shutil.rmtree(output_dir)
- # Build up the template answers and run the template engine
- 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
- )
- # From the created package.json grab the devDependencies
- 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()
- # Ask the user whether to upgrade the scripts automatically
- 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")
- # Set the output directory
- data["jupyterlab"]["outputDir"] = temp_data["jupyterlab"]["outputDir"]
- # Set linters
- ## Map package.json key to previous config file
- 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}")
- # Look for resolutions in JupyterLab metadata and upgrade those as well
- 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("~", "^")
- # Sort the entries
- for key in ["scripts", "dependencies", "devDependencies"]:
- if data[key]:
- data[key] = dict(sorted(data[key].items()))
- else:
- del data[key]
- # Update style settings
- 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")
- # Update the root package.json file
- package_file.write_text(json.dumps(data, indent=2))
- override_pyproject = False
- # For the other files, ask about whether to override (when it exists)
- # At the end, list the files that were: added, overridden, skipped
- 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())
- # Backport requirements
- 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
- )
- # Backport extras
- 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")
- # Print out all warnings
- 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)
|