From c6f68f69bc2861cd92c128531d4470de71407852 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 26 Jun 2022 19:16:37 -0500 Subject: [PATCH] Add jupyter_packaging conversion wrapper (#36) --- hatch_jupyter_builder/migration/__main__.py | 1 - hatch_jupyter_builder/migration/_migrate.py | 171 ++++++++---------- .../migration/jupyter_packaging.py | 162 +++++++++++++++++ tests/data/pyproject.toml | 2 +- 4 files changed, 234 insertions(+), 102 deletions(-) create mode 100644 hatch_jupyter_builder/migration/jupyter_packaging.py diff --git a/hatch_jupyter_builder/migration/__main__.py b/hatch_jupyter_builder/migration/__main__.py index 9eb771e..71a1aaa 100644 --- a/hatch_jupyter_builder/migration/__main__.py +++ b/hatch_jupyter_builder/migration/__main__.py @@ -29,7 +29,6 @@ runner = subprocess.check_call runner([python, "-m", "pip", "install", "build"]) runner([python, "-m", "pip", "install", "packaging"]) - runner([python, "-m", "pip", "install", "jupyter_packaging"]) runner([python, "-m", "pip", "install", "tomli_w"]) runner([python, "-m", "pip", "install", "tomli"]) runner([python, "-m", "pip", "install", "hatch"]) diff --git a/hatch_jupyter_builder/migration/_migrate.py b/hatch_jupyter_builder/migration/_migrate.py index 789e9e2..e6043a2 100644 --- a/hatch_jupyter_builder/migration/_migrate.py +++ b/hatch_jupyter_builder/migration/_migrate.py @@ -1,6 +1,5 @@ import json import os -import re import subprocess import sys from pathlib import Path @@ -21,6 +20,21 @@ warnings = [] +# Read pyproject before migration to get old build requirements. +pyproject = Path("pyproject.toml") +data = tomli.loads(pyproject.read_text("utf-8")) +requires = data["build-system"]["requires"] +# Install the old build reqs into this venv. +subprocess.run([sys.executable, "-m", "pip", "install"] + requires) +requires = [ + r + for r in requires + if not r.startswith("jupyter-packaging") + and not r.startswith("setuptools") + and not r.startswith("jupyter_packaging") + and not r.startswith("wheel") +] + # Extract the current version before beginning any migration. setup_py = Path("setup.py") if setup_py.exists(): @@ -33,27 +47,25 @@ warnings.append("Fill in '[project][version]' in 'pyproject.toml'") current_version = "!!UNKONWN!!" -# Get the original requires list. -pyproject = Path("pyproject.toml") -text = pyproject.read_text("utf-8") -data = tomli.loads(text) -requires = data["build-system"]["requires"] -requires = [ - r - for r in requires - if not r.startswith("jupyter-packaging") - and not r.startswith("setuptools") - and not r.startswith("jupyter_packaging") - and not r.startswith("wheel") -] - -# Automatic migration from hatch. +# Run the hatch migration script. +print("Running hatch migration") subprocess.run([sys.executable, "-m", "hatch", "new", "--init"]) +# Run the jupyter-packaging migration script - must be done after +# hatch migration to avoid conflicts. +print("Running jupyter-packaging migration") +here = os.path.abspath(os.path.dirname(__file__)) +prev_pythonpath = os.environ.get("PYTHONPATH", "") +if prev_pythonpath: + os.environ["PYTHONPATH"] = f"{here}{os.pathsep}{prev_pythonpath}" +else: + os.environ["PYTHONPATH"] = here +subprocess.run([sys.executable, "setup.py", "--version"], capture_output=True) +os.environ["PYTHONPATH"] = prev_pythonpath + # Handle setup.cfg # Move flake8 config to separate file, preserving comments. # Add .flake8 file to git. -# Remove file when done. setup_cfg = Path("setup.cfg") flake8 = ["[flake8]"] if setup_cfg.exists(): @@ -72,15 +84,15 @@ flake8.append(line) - Path(".flake8").write_text("\n".join(flake8) + "\n", "utf-8") + if matches: + Path(".flake8").write_text("\n".join(flake8) + "\n", "utf-8") + subprocess.run(["git", "add", ".flake"]) - subprocess.run(["git", "add", ".flake"]) - -# Handle pyproject.toml config. # Migrate and remove unused config. -text = pyproject.read_text("utf-8") -data = tomli.loads(text) +# Read in the project.toml after auto migration. +print("Migrating static data") +data = tomli.loads(pyproject.read_text("utf-8")) tool_table = data.setdefault("tool", {}) # Add the other build requirements. @@ -96,7 +108,7 @@ targets_table = build_table.setdefault("targets", {}) # Remove the dynamic version. -if current_version: +if current_version and "version" in hatch_table: del hatch_table["version"] # Remove any auto-generated sdist config. @@ -107,8 +119,7 @@ targets_table["sdist"] = dict(exclude=[".github"]) hooks_table = build_table.setdefault("hooks", {}) -hooks_table["jupyter-builder"] = {} -builder_table: dict = hooks_table["jupyter-builder"] +builder_table = hooks_table.setdefault("jupyter-builder", {}) builder_table["dependencies"] = [f"hatch-jupyter-builder>={builder_version}"] # Migrate the jupyter-packaging static data. @@ -140,85 +151,45 @@ build_table["artifacts"] = artifacts -# Handle setup.py - jupyter_packaging and pre-commit config. -# Remove the file when finished. +# Handle setup.py - pre-commit config. if setup_py.exists(): text = setup_py.read_text("utf-8") if "pre-commit" in text: builder_table["install-pre-commit"] = True - build_kwargs = builder_table.setdefault("build-kwargs", {}) - editable_build_command = None - if "build_cmd" in text: - match = re.search("build_cmd=['\"](.*?)['\"]", text, re.MULTILINE) - if match: - editable_build_command = match.groups()[0] - - if "source_dir" in text or "build_dir" in text and editable_build_command: - builder_table["editable-build-kwargs"] = {} - editable_build_kwargs: dict = builder_table["editable-build-kwargs"] - if not editable_build_command: - editable_build_command = "!!! needs manual input !!!" - warnings.append( - "Fill in [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs][build_cmd]' in 'pyproject.toml', which was the 'build_cmd' argument to 'npm_builder' in 'setup.py'" - ) - editable_build_kwargs["build_cmd"] = editable_build_command - - for name in ["source_dir", "build_dir"]: - if name not in text: - continue - match = re.search(f"{name}=['\"](.*?)['\"]", text, re.MULTILINE) - if match is not None: - editable_build_kwargs[name] = match.groups()[0] - else: - warnings.append( - f"Fill in '[tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs][{name}]' in 'pyproject.toml', which was the '{name}' argument to 'npm_builder' in 'setup.py'" - ) - editable_build_kwargs[name] = "!!! needs manual input !!!" - - elif editable_build_command or "build_cmd" in text: - if not editable_build_command: - editable_build_command = "!!! needs manual input !!!" - warnings.append( - "Fill in [tool.hatch.build.hooks.jupyter-builder.editable_build_cmd]' in 'pyproject.toml', which was the 'build_cmd' argument to 'npm_builder' in 'setup.py'" - ) - build_kwargs["editable_build_cmd"] = editable_build_command - - # Handle versioning with tbump - allows for static versioning and makes - # it easier to use jupyter_releaser. - data["project"]["version"] = current_version - data["project"].pop("dynamic", None) - - tbump_table = tool_table.setdefault("tbump", {}) - tbump_table["version"] = dict( - current=current_version, - regex=r""" - (?P\d+)\.(?P\d+)\.(?P\d+)((?Pa|b|rc|.dev)(?P\d+))? - """.strip(), - ) - tbump_table["git"] = dict( - message_template=r"Bump to {new_version}", tag_template=r"v{new_version}" - ) - tbump_table["field"] = [dict(name="channel", default=""), dict(name="release", default="")] - tbump_table["file"] = [dict(src="pyproject.toml")] - - # Add entry for _version.py if it exists. - version_py = Path(project_name) / "_version.py" - if version_py.exists(): - tbump_table["file"].append(dict(src=str(version_py))) - text = version_py.read_text(encoding="utf-8") - if current_version not in text: - warnings.append( - f'Add the static version string "{current_version}" to "{version_py}" instead of dynamic version handling' - ) - - # Add entry for package.json if it exists and has the same version. - package_json = Path("package.json") - if package_json.exists(): - text = package_json.read_text(encoding="utf-8") - npm_version = json.loads(text)["version"] - if npm_version == current_version: - tbump_table["file"].append(dict(src="package.json")) +# Handle versioning with tbump - allows for static versioning and makes +# it easier to use jupyter_releaser. +data["project"]["version"] = current_version +data["project"].pop("dynamic", None) + +tbump_table = tool_table.setdefault("tbump", {}) +tbump_table["version"] = dict( + current=current_version, + regex=r""" + (?P\d+)\.(?P\d+)\.(?P\d+)((?Pa|b|rc|.dev)(?P\d+))? + """.strip(), +) +tbump_table["git"] = dict(message_template=r"Bump to {new_version}", tag_template=r"v{new_version}") +tbump_table["field"] = [dict(name="channel", default=""), dict(name="release", default="")] +tbump_table["file"] = [dict(src="pyproject.toml")] + +# Add entry for _version.py if it exists. +version_py = Path(project_name) / "_version.py" +if version_py.exists(): + tbump_table["file"].append(dict(src=str(version_py))) + text = version_py.read_text(encoding="utf-8") + if current_version not in text: + warnings.append( + f'Add the static version string "{current_version}" to "{version_py}" instead of dynamic version handling' + ) + +# Add entry for package.json if it exists and has the same version. +package_json = Path("package.json") +if package_json.exists(): + text = package_json.read_text(encoding="utf-8") + npm_version = json.loads(text)["version"] + if npm_version == current_version: + tbump_table["file"].append(dict(src="package.json")) # Add a setup.py shim. shim_text = """# setup.py shim for use with applications that require it. diff --git a/hatch_jupyter_builder/migration/jupyter_packaging.py b/hatch_jupyter_builder/migration/jupyter_packaging.py new file mode 100644 index 0000000..f51cce0 --- /dev/null +++ b/hatch_jupyter_builder/migration/jupyter_packaging.py @@ -0,0 +1,162 @@ +import os +import sys +from pathlib import Path + +import tomli +import tomli_w + +__this_shim = sys.modules.pop("jupyter_packaging") +__current_directory = sys.path.pop(0) + +import jupyter_packaging as __real_jupyter_packaging + +sys.path.insert(0, __current_directory) +sys.modules["jupyter_packaging"] = __this_shim + + +def _write_config(path, data): + pyproject = Path("pyproject.toml") + top = tomli.loads(pyproject.read_text(encoding="utf-8")) + current = top + parts = path.split(".") + for part in parts[:-1]: + if part in current: + current = current[part] + else: + current[part] = current = {} + if parts[-1] in current: + existing = current[parts[-1]] + else: + existing = current[parts[-1]] = {} + existing.update(data) + pyproject.write_text(tomli_w.dumps(top), encoding="utf-8") + + +_npm_kwargs = ["path", "build_dir", "source_dir", "build_cmd", "npm"] + + +def _normalize_path(path): + path = str(path) + cwd = os.getcwd() + if path.startswith(cwd): + return os.path.relpath(path, cwd) + return path + + +def _get_build_kwargs(**kwargs): + build_kwargs = {} + for name in _npm_kwargs: + value = kwargs[name] + if value is not None: + if name in ["path", "build_dir", "source_dir"]: + value = _normalize_path(value) + build_kwargs[name] = value + if kwargs.get("force"): + build_kwargs["force"] = True + return build_kwargs + + +def skip_if_exists(paths, *args): + if paths: + data = {"skip-if-exists": [_normalize_path(p) for p in paths]} + _write_config("tool.hatch.build.hooks.jupyter-builder", data) + return __real_jupyter_packaging.skip_if_exists(paths, *args) + + +def ensure_targets(targets): + if targets: + data = {"ensured-targets": [_normalize_path(t) for t in targets]} + _write_config("tool.hatch.build.hooks.jupyter-builder", data) + return __real_jupyter_packaging.ensure_targets(targets) + + +def wrap_installers( + pre_develop=None, + pre_dist=None, + post_develop=None, + post_dist=None, + ensured_targets=None, + skip_if_exists=None, +): + if pre_develop or post_develop: + func = pre_develop or post_develop + build_kwargs = _get_build_kwargs(**func.__kwargs) + _write_config("tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs", build_kwargs) + + if pre_dist or post_dist: + func = pre_dist or post_dist + build_kwargs = _get_build_kwargs(**func.__kwargs) + _write_config("tool.hatch.build.hooks.jupyter-builder.build-kwargs", build_kwargs) + + if skip_if_exists: + data = {"skip-if-exists": [_normalize_path(p) for p in skip_if_exists]} + _write_config("tool.hatch.build.hooks.jupyter-builder", data) + + return __real_jupyter_packaging.wrap_installers( + pre_develop=pre_develop, + pre_dist=pre_dist, + post_develop=post_develop, + post_dist=post_dist, + ensured_targets=ensured_targets, + skip_if_exists=skip_if_exists, + ) + + +def create_cmdclass( + prerelease_cmd=None, package_data_spec=None, data_files_spec=None, exclude=None +): + shared_data = {} + if data_files_spec is not None: + for (path, dname, pattern) in data_files_spec: + shared_data[f"{dname}/{pattern}"] = path + + _write_config("tool.hatch.build.targets.wheel.shared-data", shared_data) + + return __real_jupyter_packaging.create_cmdclass( + prerelease_cmd=prerelease_cmd, + package_data_spec=package_data_spec, + data_files_spec=data_files_spec, + exclude=exclude, + ) + + +def install_npm( + path=None, build_dir=None, source_dir=None, build_cmd="build", force=False, npm=None +): + build_kwargs = _get_build_kwargs(**locals()) + if build_kwargs: + _write_config("tool.hatch.build.hooks.jupyter-builder.build-kwargs", build_kwargs) + + return __real_jupyter_packaging.install_npm( + path=path, + build_dir=build_dir, + source_dir=source_dir, + build_cmd=build_cmd, + force=force, + npm=npm, + ) + + +def npm_builder( + path=None, build_dir=None, source_dir=None, build_cmd="build", force=False, npm=None +): + func = __real_jupyter_packaging.npm_builder( + path=path, + build_dir=build_dir, + source_dir=source_dir, + build_cmd=build_cmd, + force=force, + npm=npm, + ) + func.__kwargs = {} + for name in _npm_kwargs + ["force"]: + func.__kwargs[name] = locals()[name] + return func + + +def __getattr__(name): + return getattr(__real_jupyter_packaging, name) + + +del __this_shim +del __current_directory diff --git a/tests/data/pyproject.toml b/tests/data/pyproject.toml index 63ae6b9..67f41c1 100644 --- a/tests/data/pyproject.toml +++ b/tests/data/pyproject.toml @@ -77,7 +77,7 @@ npm = [ [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] build_cmd = "install:extension" source_dir = "src" -build_dir = "!!! needs manual input !!!" +build_dir = "myextension/labextension" [tool.tbump] field = [