diff --git a/src/compwa_policy/check_dev_files/__init__.py b/src/compwa_policy/check_dev_files/__init__.py index f9742d55..86ad9967 100644 --- a/src/compwa_policy/check_dev_files/__init__.py +++ b/src/compwa_policy/check_dev_files/__init__.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any from compwa_policy.check_dev_files import ( + binder, black, citation, commitlint, @@ -52,9 +53,10 @@ from compwa_policy.utilities.pyproject import PythonVersion -def main(argv: Sequence[str] | None = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: # noqa: PLR0915 parser = _create_argparse() args = parser.parse_args(argv) + doc_apt_packages = _to_list(args.doc_apt_packages) environment_variables = _get_environment_variables(args.environment_variables) is_python_repo = not args.no_python repo_name, repo_title = _determine_repo_name_and_title(args) @@ -80,7 +82,7 @@ def main(argv: Sequence[str] | None = None) -> int: github_workflows.main, precommit_config, allow_deprecated=args.allow_deprecated_workflows, - doc_apt_packages=_to_list(args.doc_apt_packages), + doc_apt_packages=doc_apt_packages, github_pages=args.github_pages, keep_pr_linting=args.keep_pr_linting, no_cd=args.no_cd, @@ -94,6 +96,7 @@ def main(argv: Sequence[str] | None = None) -> int: test_extras=_to_list(args.ci_test_extras), ) if has_notebooks: + do(binder.main, dev_python_version, doc_apt_packages) do(jupyter.main, args.no_ruff) do(nbstripout.main, precommit_config, _to_list(args.allowed_cell_metadata)) do( diff --git a/src/compwa_policy/check_dev_files/binder.py b/src/compwa_policy/check_dev_files/binder.py new file mode 100644 index 00000000..be037197 --- /dev/null +++ b/src/compwa_policy/check_dev_files/binder.py @@ -0,0 +1,99 @@ +"""Add configuration for Binder. + +See also https://mybinder.readthedocs.io/en/latest/using/config_files.html. +""" + +from __future__ import annotations + +import os +from textwrap import dedent +from typing import TYPE_CHECKING + +from compwa_policy.errors import PrecommitError +from compwa_policy.utilities import CONFIG_PATH +from compwa_policy.utilities.executor import Executor +from compwa_policy.utilities.match import git_ls_files + +if TYPE_CHECKING: + from pathlib import Path + + from compwa_policy.utilities.pyproject import PythonVersion + + +def main(python_version: PythonVersion, apt_packages: list[str]) -> None: + with Executor() as do: + do(_update_apt_txt, apt_packages) + do(_update_post_build) + do(_make_executable, CONFIG_PATH.binder / "postBuild") + do(_update_runtime_txt, python_version) + + +def _update_apt_txt(apt_packages: list[str]) -> None: + apt_txt = CONFIG_PATH.binder / "apt.txt" + if not apt_packages and apt_txt.exists(): + apt_txt.unlink() + msg = f"Removed {apt_txt}, because --doc-apt-packages does not specify any packages." + raise PrecommitError(msg) + apt_packages = sorted(set(apt_packages)) + __update_file( + expected_content="\n".join(apt_packages) + "\n", + path=apt_txt, + ) + + +def _update_post_build() -> None: + expected_content = dedent(""" + #!/bin/bash + set -ex + curl -LsSf https://astral.sh/uv/install.sh | sh + source $HOME/.cargo/env + """).strip() + if "uv.lock" in set(git_ls_files(untracked=True)): + expected_content += dedent(R""" + uv export \ + --extra jupyter \ + --extra notebooks \ + > requirements.txt + uv pip install \ + --requirement requirements.txt \ + --system + uv cache clean + """) + else: + expected_content += dedent(R""" + uv pip install \ + --editable '.[jupyter,notebooks]' \ + --no-cache \ + --system + """) + __update_file( + expected_content.strip() + "\n", + path=CONFIG_PATH.binder / "postBuild", + ) + + +def _make_executable(path: Path) -> None: + if os.access(path, os.X_OK): + return + msg = f"{path} has been made executable" + path.chmod(0o755) + raise PrecommitError(msg) + + +def _update_runtime_txt(python_version: PythonVersion) -> None: + __update_file( + expected_content=f"python-{python_version}\n", + path=CONFIG_PATH.binder / "runtime.txt", + ) + + +def __update_file(expected_content: str, path: Path) -> None: + path.parent.mkdir(exist_ok=True) + if path.exists(): + with open(path) as stream: + if stream.read() == expected_content: + return + with open(path, "w") as stream: + stream.write(expected_content) + msg = f"Updated {path}" + raise PrecommitError(msg) diff --git a/src/compwa_policy/utilities/__init__.py b/src/compwa_policy/utilities/__init__.py index f7fdbef7..f2494351 100644 --- a/src/compwa_policy/utilities/__init__.py +++ b/src/compwa_policy/utilities/__init__.py @@ -17,6 +17,7 @@ class _ConfigFilePaths(NamedTuple): + binder: Path = Path(".binder") citation: Path = Path("CITATION.cff") codecov: Path = Path("codecov.yml") conda: Path = Path("environment.yml")