diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5f2980ca..9c2558b9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,15 +34,15 @@ jobs: with: python-version: "3.12" - - name: install tox - run: python -m pip install tox + - name: install nox + run: python -m pip install nox - name: install cp2k if: matrix.example-name == 'cp2k_run_batch' run: sudo apt-get install -y cp2k - name: build example - run: tox -e ${{ matrix.example-name }} + run: nox -e ${{ matrix.example-name }} - name: store example as a github artifact uses: actions/upload-artifact@v4 @@ -66,8 +66,8 @@ jobs: with: python-version: "3.12" - - name: install tox - run: python -m pip install tox + - name: install nox + run: python -m pip install nox - name: load github artifact for each example uses: actions/download-artifact@v4 @@ -77,7 +77,7 @@ jobs: merge-multiple: true - name: build documentation - run: tox -e build_docs + run: nox -e build_docs - name: store documentation as github artifact to be downloaded by users uses: actions/upload-artifact@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ad3a1fd0..68613d53 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: python-version: "3.12" - name: install dependencies - run: pip install tox + run: pip install nox - name: test lint - run: tox -e lint + run: nox -e lint diff --git a/.gitignore b/.gitignore index f6eb9338..902390c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ docs/src/examples/ *build* *egg-info/ sg_execution_times.rst + +.nox/ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000..ed7dfd65 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,163 @@ +import glob +import hashlib +import json +import os + +import nox + + +# global nox options +nox.options.reuse_venv = "yes" +nox.options.sessions = ["lint", "docs"] + + +# Files that need to be linted & formatted +LINT_FILES = [ + "ipynb-to-gallery.py", + "generate-gallery.py", + "noxfile.py", + "docs/src/conf.py", + "examples", +] + +# the current list of examples, determined from the directories on disk +EXAMPLES = [ + os.path.basename(os.path.normpath(file)) for file in glob.glob("examples/*/") +] + +# ==================================================================================== # +# helper functions # +# ==================================================================================== # + + +def should_reinstall_dependencies(session, **metadata): + """ + Returns a bool indicating whether the dependencies should be re-installed in the + venv. + + This works by hashing everything in metadata, and storing the hash in the session + virtualenv. If the hash changes, we'll have to re-install! + """ + + to_hash = {} + for key, value in metadata.items(): + if os.path.exists(value): + with open(value) as fd: + to_hash[key] = fd.read() + else: + to_hash[key] = value + + to_hash = json.dumps(to_hash).encode("utf8") + sha1 = hashlib.sha1(to_hash).hexdigest() + sha1_path = os.path.join(session.virtualenv.location, "metadata.sha1") + + if session.virtualenv._reused: + if os.path.exists(sha1_path): + with open(sha1_path) as fd: + should_reinstall = fd.read().strip() != sha1 + else: + should_reinstall = True + else: + should_reinstall = True + + with open(sha1_path, "w") as fd: + fd.write(sha1) + + if should_reinstall: + session.debug("updating environment since the dependencies changed") + + return should_reinstall + + +# ==================================================================================== # +# nox sessions definitions # +# ==================================================================================== # + + +for name in EXAMPLES: + + @nox.session(name=name, venv_backend="conda") + def example(session, name=name): + environment_yml = f"examples/{name}/environment.yml" + if should_reinstall_dependencies(session, environment_yml=environment_yml): + session.run( + "conda", + "env", + "update", + "--prune", + f"--file={environment_yml}", + f"--prefix={session.virtualenv.location}", + ) + + # install sphinx-gallery and its dependencies + session.install("sphinx-gallery", "sphinx", "pillow", "matplotlib") + + session.run("python", "generate-gallery.py", f"examples/{name}") + os.unlink(f"docs/src/examples/{name}/index.rst") + + +@nox.session(venv_backend="none") +def docs(session): + """Run all examples and build the documentation""" + + for example in EXAMPLES: + session.run("nox", "-e", example, external=True) + session.run("nox", "-e", "build_docs", external=True) + + +@nox.session +def build_docs(session): + """Assemble the documentation into a website, assuming pre-generated examples""" + + requirements = "docs/requirements.txt" + if should_reinstall_dependencies(session, requirements=requirements): + session.install("-r", requirements) + + session.run("sphinx-build", "-W", "-b", "html", "docs/src", "docs/build/html") + + +@nox.session +def lint(session): + """Run linters and type checks""" + + if not session.virtualenv._reused: + session.install("black", "blackdoc") + session.install("flake8", "flake8-bugbear", "flake8-sphinx-links") + session.install("isort") + session.install("sphinx-lint") + + # Formatting + session.run("black", "--check", "--diff", *LINT_FILES) + session.run("blackdoc", "--check", "--diff", *LINT_FILES) + session.run("isort", "--check-only", "--diff", *LINT_FILES) + + # Linting + session.run( + "flake8", + "--max-line-length=88", + "--exclude=docs/src/examples/", + *LINT_FILES, + ) + + session.run( + "sphinx-lint", + "--enable=line-too-long", + "--max-line-length=88", + "--ignore=docs/src", + "README.rst", + "CONTRIBUTING.rst", + *LINT_FILES, + ) + + +@nox.session +def format(session): + """Automatically format all files""" + + if not session.virtualenv._reused: + session.install("black", "blackdoc") + session.install("isort") + + session.run("black", *LINT_FILES) + session.run("blackdoc", *LINT_FILES) + session.run("isort", *LINT_FILES) diff --git a/setup.py b/setup.py deleted file mode 100644 index 1abbd068..00000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -import setuptools - -if __name__ == "__main__": - setuptools.setup() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 0cc70a8b..00000000 --- a/tox.ini +++ /dev/null @@ -1,116 +0,0 @@ -[tox] -min_version = 4.0 -envlist = - lint - docs - -[testenv] -package = skip -lint_folders = - "{toxinidir}/ipynb-to-gallery.py" \ - "{toxinidir}/generate-gallery.py" \ - "{toxinidir}/docs/src/conf.py" \ - "{toxinidir}/examples" - -install_conda_env = - conda env update --file examples/{envname}/environment.yml --prefix {envdir}/conda --verbose - # install sphinx-gallery and its dependencies - {envdir}/conda/bin/python -m pip install sphinx-gallery sphinx pillow - -generate_gallery = - {envdir}/conda/bin/python generate-gallery.py examples/{envname} - rm -rf docs/src/examples/{envname}/index.rst - -allowlist_externals = - rm - conda - {envdir}/conda/bin/python - -[testenv:docs] -description = Run all examples and build the documentation -allowlist_externals = tox -commands = - # transform all examples to rst - tox -e lode_linear - tox -e roy_gch - tox -e sample_selection - tox -e gaas_map - tox -e cp2k_run_batch - - # Build the documentation - tox -e build_docs - -[testenv:build_docs] -description = Build the documentation (examples must be generated before) -deps = -r docs/requirements.txt -commands = sphinx-build {posargs:-E} -W -b html docs/src docs/build/html - - -[testenv:lode_linear] -description = Build the `lode_linear` example -commands = - {[testenv]install_conda_env} - {[testenv]generate_gallery} - -[testenv:roy_gch] -description = Build the `roy_gch` example -commands = - {[testenv]install_conda_env} - {[testenv]generate_gallery} - -[testenv:gaas_map] -description = Build the `gaas_map` example -commands = - {[testenv]install_conda_env} - {[testenv]generate_gallery} - -[testenv:sample_selection] -description = Build the `sample_selection` example -commands = - {[testenv]install_conda_env} - {[testenv]generate_gallery} - -[testenv:cp2k_run_batch] -description = Build the `cp2k_run_batch` example -commands = - {[testenv]install_conda_env} - {[testenv]generate_gallery} - -[testenv:lint] -description = Run linters and type checks -deps = - black - blackdoc - flake8 - flake8-bugbear - isort - flake8-sphinx-links - sphinx-lint -commands = - flake8 {[testenv]lint_folders} - black --check --diff {[testenv]lint_folders} - blackdoc --check --diff {[testenv]lint_folders} - isort --check-only --diff {[testenv]lint_folders} - sphinx-lint --enable line-too-long --max-line-length 88 \ - -i "{toxinidir}/docs/src" \ - {[testenv]lint_folders} "{toxinidir}/README.rst" "{toxinidir}/CONTRIBUTING.rst" - - -[testenv:format] -description = Abuse tox to do actual formatting on all files. -deps = - black - blackdoc - isort -commands = - black {[testenv]lint_folders} - blackdoc {[testenv]lint_folders} - isort {[testenv]lint_folders} - -[flake8] -max_line_length = 88 -exclude = - docs/src/examples/ -per-file-ignores = - # D205 and D400 are incompatible with the requirements of sphinx-gallery - examples/**:D205, D400