diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..197c252 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# store every text file with Unix line endings +# to avoid whitespace diffs between windows and linux +* text=auto eol=lf + +# mark uv.lock as machine generated to hide in diff +uv.lock linguist-generated=true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0592180..a477bde 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,21 @@ ## Summary -- + ## Validation -- [ ] `scripts/quality-check.sh` -- [ ] Manual testing, if applicable: +- [ ] `scripts/quality-check.sh` passes locally +- [ ] Appropriate `/test` commands were run and are passing (e.g. `/test gpu`) +- [ ] Unit tests were added / e2e tests were added where applicable +- [ ] Manual testing, if applicable (describe further): ## Checklist - [ ] The change is focused and easy to review. -- [ ] Tests were added or updated for behavior changes. -- [ ] Documentation was updated for user-facing or setup changes. -- [ ] No secrets, generated artifacts, or local-only files are included. +- [ ] Tests were added or updated for behaviour changes. +- [ ] No secrets, generated files, or files that only make sense on my machine are included. +- [ ] Relevant documentation is updated. -## Notes for Reviewers +## Additional Comments -- + diff --git a/.github/scripts/check-commenter.sh b/.github/scripts/check-commenter.sh new file mode 100755 index 0000000..eb415ea --- /dev/null +++ b/.github/scripts/check-commenter.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# +# check if user has permission to run test command +# needs to be, owner, member, or collaborator + +set -euo pipefail + +case "$ASSOCIATION" in + OWNER | MEMBER | COLLABORATOR) + echo "$COMMENTER is allowed to run commands ($ASSOCIATION)." + ;; + *) + gh api --method POST \ + "/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \ + -f body="Sorry @$COMMENTER, only people with access to this repository can run \`/test\`." + + echo "$COMMENTER is not allowed to run commands ($ASSOCIATION)." >&2 + exit 1 + ;; +esac diff --git a/.github/scripts/find-commit.sh b/.github/scripts/find-commit.sh new file mode 100755 index 0000000..666951c --- /dev/null +++ b/.github/scripts/find-commit.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# +# find the latest commit of the branch a PR comment is made on + +set -euo pipefail + +sha="$(gh pr view "$ISSUE_NUMBER" \ + --repo "$GITHUB_REPOSITORY" \ + --json headRefOid \ + --jq .headRefOid)" + +echo "Testing commit $sha" + +{ + echo "sha=$sha" + echo "short_sha=${sha:0:7}" +} >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/parse-command.sh b/.github/scripts/parse-command.sh new file mode 100755 index 0000000..f64d84c --- /dev/null +++ b/.github/scripts/parse-command.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# Reads the comment and works out what the workflow should execute +# +# /test run all tests +# /test all run all tests +# /test run specific tests +# /test help the list of commands +# +# Extra args are passed to pytest +# +# It writes four values for the later jobs to read: +# +# mode none, help or tests +# matrix the groups to run, as the JSON a job matrix expects +# groups the same list in words, for the reply comment +# pytest_args extra arguments to hand to pytest + +set -euo pipefail + +UBUNTU='"ubuntu-latest"' +# TODO: set up real gpu machine +GPU_MACHINE='["self-hosted","gpu"]' + +# only read the first line +first_line="$(printf '%s' "$BODY" | head -n 1 | tr -d '\r')" +command="$(printf '%s' "$first_line" | awk '{print $1}')" +rest="$(printf '%s' "$first_line" | sed -E 's|^[[:space:]]*/test[[:space:]]*||; s|[[:space:]]*$||')" + +mode=none +groups=() +pytest_args="" + +if [[ "$command" != "/test" ]]; then + echo "Not a /test command, stopping here." +else + first_word="$(printf '%s' "$rest" | awk '{print $1}')" + case "$first_word" in + help | all | python | slow | gpu | web) + pytest_args="$(printf '%s' "$rest" | sed -E 's|^[^[:space:]]+[[:space:]]*||')" + ;; + *) + first_word=all + pytest_args="$rest" + ;; + esac + + case "$first_word" in + help) mode=help ;; + # TODO: add gpu back to this list once the machine from the TODO above is + # registered, otherwise will hang for 24h + all) mode=tests; groups=(python slow web) ;; + *) mode=tests; groups=("$first_word") ;; + esac + + # pass rest of line as pytest args + if [[ -n "$pytest_args" ]] && ! printf '%s' "$pytest_args" | grep -Eq '^[A-Za-z0-9 _./=:@-]+$'; then + echo "These arguments contain characters that are not allowed: $pytest_args" >&2 + exit 1 + fi +fi + +# build a strategy.matrix for github actions to consume +entries=() +for group in ${groups[@]+"${groups[@]}"}; do + case "$group" in + gpu) runs_on="$GPU_MACHINE" ;; + *) runs_on="$UBUNTU" ;; + esac + escaped="${runs_on//\"/\\\"}" + entries+=("{\"group\":\"$group\",\"runs_on\":\"$escaped\"}") +done + +matrix="{\"include\":[$( + IFS=, + echo "${entries[*]-}" +)]}" +group_list="$( + IFS=, + echo "${groups[*]-}" +)" +group_list="${group_list//,/, }" + +echo "Mode '$mode', groups: ${group_list:-none}, pytest arguments: ${pytest_args:-none}" + +{ + echo "mode=$mode" + echo "matrix=$matrix" + echo "groups=$group_list" + echo "pytest_args=$pytest_args" +} >> "$GITHUB_OUTPUT" diff --git a/.github/scripts/post-help.sh b/.github/scripts/post-help.sh new file mode 100755 index 0000000..83c233c --- /dev/null +++ b/.github/scripts/post-help.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Replies to `/test help` with the list of commands. + +set -euo pipefail + +body="$( + cat << 'EOF' +**Commands available on this pull request** + +| Comment | Description | +| --- | --- | +| `/test` or `/test all` | Every group below except `gpu`, at the same time | +| `/test python` | The quick tests. | +| `/test slow` | The tests marked `slow` | +| `/test gpu` | The tests marked `gpu`, on the project GPU machine | +| `/test web` | The 3D visualizer | +| `/test help` | Shows this message | + +Anything after the group name is passed to pytest, so `/test python -k sampler` +runs the quick tests whose names contain "sampler". + +These commands are for tests that are too slow or need hardware to run every time. +EOF +)" + +gh api --method POST \ + "/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \ + -f body="$body" diff --git a/.github/scripts/post-result.sh b/.github/scripts/post-result.sh new file mode 100755 index 0000000..515c600 --- /dev/null +++ b/.github/scripts/post-result.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Replies with the result of the tests once they have finished + +set -euo pipefail + +if [[ "$RESULT" == "success" ]]; then + heading="Tests passed" +else + heading="Tests did not pass ($RESULT)" +fi + +body="$( + cat << EOF +**$heading** + +| | | +| --- | --- | +| Groups run | \`$TEST_GROUPS\` | +| Commit tested | \`$SHORT_SHA\` (\`$SHA\`) | +| Full log | [Actions run]($RUN_URL) | + +EOF +)" + +gh api --method POST \ + "/repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \ + -f body="$body" diff --git a/.github/scripts/react-to-comment.sh b/.github/scripts/react-to-comment.sh new file mode 100755 index 0000000..aefca14 --- /dev/null +++ b/.github/scripts/react-to-comment.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# +# Puts a 👀 reaction on the comment that asked for the tests + +set -euo pipefail + +gh api --method POST \ + "/repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID/reactions" \ + -f content=eyes > /dev/null diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..aa7f41b --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,97 @@ +name: CI + +# Runs automatically on every pull request and on every push to main. +# The `format` and `test` jobs are the ones a pull request must pass before it +# is allowed to merge +# +# See docs/branch-protection.md for more info +# +# Jobs are split into web and python checks + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # check for code formatting + format: + name: format + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install Python dependencies + run: scripts/install-python-dependencies.sh + + - name: Check formatting and code style + run: scripts/format-check.sh + + # check quick test are passing + # gpu / other slow tests run with pr command + test: + name: test + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install Python dependencies + run: scripts/install-python-dependencies.sh + + - name: Run the quick tests + run: scripts/test-check.sh python + + # TODO: update script for web side when added + web: + name: web + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Look for a web/ folder + id: detect + run: | + if [[ -f web/package.json ]]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "No web/ folder yet, so there is nothing to check." + fi + + - name: Set up Node.js + if: steps.detect.outputs.present == 'true' + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install web dependencies + if: steps.detect.outputs.present == 'true' + run: scripts/install-web-dependencies.sh + + - name: Run the web checks + if: steps.detect.outputs.present == 'true' + run: scripts/web-check.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ffe3cab..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: read - -concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - quality: - name: quality - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js from .nvmrc - if: ${{ hashFiles('package.json') != '' && hashFiles('.nvmrc') != '' }} - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - - - name: Set up default Node.js - if: ${{ hashFiles('package.json') != '' && hashFiles('.nvmrc') == '' }} - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install Node dependencies - if: ${{ hashFiles('package.json') != '' }} - run: scripts/install-dependencies.sh - - - name: Run quality checks - run: scripts/quality-check.sh diff --git a/.github/workflows/pr-command.yaml b/.github/workflows/pr-command.yaml new file mode 100644 index 0000000..cccc84b --- /dev/null +++ b/.github/workflows/pr-command.yaml @@ -0,0 +1,167 @@ +name: PR command + +# TODO: update when gpu is added +# Lets you run tests via PR comment: +# /test all every test: python, slow and web (not gpu yet) +# /test python the quick tests +# /test slow the tests marked `slow` +# /test gpu the tests marked `gpu`, on the project's GPU machine +# /test web the 3D visualizer +# /test help explains the commands +# +# Changes to this file do not show on the PR that adds them + +on: + issue_comment: + types: [created] + +permissions: + contents: read + issues: write + pull-requests: write + +# There is deliberately no concurrency setting for the whole workflow. One here +# would put every comment on a pull request into a single queue, so `/test web` +# would sit waiting for `/test python` to finish. The cancelling is done per +# test group on the run-tests job below instead. + +jobs: + # Decide whether to do anything at all, and if so, what. + prepare: + name: prepare + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/test') }} + runs-on: ubuntu-latest + + outputs: + mode: ${{ steps.parse.outputs.mode }} + matrix: ${{ steps.parse.outputs.matrix }} + groups: ${{ steps.parse.outputs.groups }} + pytest_args: ${{ steps.parse.outputs.pytest_args }} + sha: ${{ steps.commit.outputs.sha }} + short_sha: ${{ steps.commit.outputs.short_sha }} + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Check the commenter is allowed to run commands + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + COMMENTER: ${{ github.event.comment.user.login }} + ASSOCIATION: ${{ github.event.comment.author_association }} + run: .github/scripts/check-commenter.sh + + - name: React to the comment + env: + GH_TOKEN: ${{ github.token }} + COMMENT_ID: ${{ github.event.comment.id }} + run: .github/scripts/react-to-comment.sh + + - name: Work out what to run + id: parse + env: + BODY: ${{ github.event.comment.body }} + run: .github/scripts/parse-command.sh + + - name: Find the commit to test + id: commit + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: .github/scripts/find-commit.sh + + # `/test help`: reply with the list of commands and run nothing. + help: + name: help + needs: prepare + if: needs.prepare.outputs.mode == 'help' + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Post the list of commands + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: .github/scripts/post-help.sh + + # pne copy of this job per group that was asked for, so + # `/test all` runs four of them at once. + run-tests: + name: ${{ matrix.group }} + needs: prepare + if: needs.prepare.outputs.mode == 'tests' + runs-on: ${{ fromJSON(matrix.runs_on) }} + timeout-minutes: 60 + + strategy: + # turning on would fail jobs across different groups + fail-fast: false + matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} + + concurrency: + group: pr-test-${{ github.event.issue.number }}-${{ matrix.group }} + cancel-in-progress: true + + steps: + # check out latest commit at time of comment + - name: Check out the pull request + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.sha }} + + - name: Install uv + if: matrix.group != 'web' + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install Python dependencies + if: matrix.group != 'web' + run: scripts/install-python-dependencies.sh + + - name: Run tests + if: matrix.group != 'web' + env: + GROUP: ${{ matrix.group }} + PYTEST_ARGS: ${{ needs.prepare.outputs.pytest_args }} + run: scripts/test-check.sh "$GROUP" $PYTEST_ARGS + + - name: Set up Node.js + if: matrix.group == 'web' + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install web dependencies + if: matrix.group == 'web' + run: scripts/install-web-dependencies.sh + + - name: Run the web checks + if: matrix.group == 'web' + run: scripts/web-check.sh + + # return one comment with the results of all tests + report: + name: report + needs: [prepare, run-tests] + if: always() && needs.run-tests.result != 'skipped' && needs.run-tests.result != 'cancelled' + runs-on: ubuntu-latest + + steps: + - name: Check out repo + uses: actions/checkout@v4 + + - name: Post the result as a comment + env: + GH_TOKEN: ${{ github.token }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + RESULT: ${{ needs.run-tests.result }} + TEST_GROUPS: ${{ needs.prepare.outputs.groups }} + SHA: ${{ needs.prepare.outputs.sha }} + SHORT_SHA: ${{ needs.prepare.outputs.short_sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: .github/scripts/post-result.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..804ba82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +__pycache__/ +*.py[cod] + +.venv/ +venv/ + +# packaging output +build/ +dist/ +*.egg-info/ + +# tool caches +.pytest_cache/ +.ruff_cache/ + +node_modules/ + +# editor specific / os +# auto generated and should be ignored +.vscode/ +.idea/ +.DS_Store +Thumbs.db diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..958267e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,127 @@ +# Contributing + +How to check your work, how tests are organised, and how changes get reviewed and merged. See the [README](README.md) first for how to install the project. + +## Running local checks + +Committing and pushing are never +blocked. Pre-merge checks run on each PR via GitHub actions which may be slow. + +You can run the same checks localy using: + +```bash +scripts/quality-check.sh +``` + + +Most formatting check failures can be enforced with: + +```bash +scripts/format-check.sh --fix +``` + +To run more specific tests, refer to the following commands. + +| Command | What it runs | +| --- | --- | +| `scripts/quality-check.sh` | All three of the below. The same as GitHub. | +| `scripts/format-check.sh` | Formatting and code style. | +| `scripts/format-check.sh --fix` | Corrects the formatting and style problems that can be corrected. | +| `scripts/test-check.sh` | The quick tests. | +| `scripts/test-check.sh slow` | The tests marked `slow`. | +| `scripts/test-check.sh gpu` | The tests marked `gpu`. | +| `scripts/test-check.sh all` | Every test. | +| `scripts/web-check.sh` | The visualizer, once `web/` exists. | + + +Testing in this project is mostly done with pytest (web test framework is undecided). We use pytest markers to categorize tests into three groups: `quick`, `slow`, and `gpu`. +`slow` and `gpu` tests are explicitly marked using `@pytest.mark.`, a test is categorized as `quick` otherwise. +You can use `test-check.sh` to run tests locally. Refer to the table above on how to invoke specific groups. + +Anything after the group are treated as pytest args, so +`scripts/test-check.sh python -k sampler` passes the `-k sample` argument to pytest. + +You can also run pytest directly, as long as you go through uv so that the +project's environment is used: + +```bash +uv run pytest +uv run pytest -k sampler +``` + +## Adding a package + +To add a package, put it in `pyproject.toml`, run `uv sync`, and commit the change to `uv.lock`. Skipping the lockfile means the package works for you and breaks for everyone else. + +## Opening a pull request + +1. Make a branch. Do not commit to `main` directly. +2. Push your branch and open a pull request into `main`. +3. Fill in the pull request description. +4. Wait for the checks to pass. GitHub will not let the pull request merge until they do. +5. Run appropriate tests using a PR comment (see below) +6. Wait for approval and ping relevant people. +7. Merge. + + +One thing to expect: pushing a new commit also removes any approval the pull +request already had, so it needs approving again. That is deliberate, so that +what gets approved is what gets merged. + +The GitHub settings that make these checks compulsory are not stored in the repository. They are listed in [docs/branch-protection.md](docs/branch-protection.md). + +## Testing + +Every PR is expected to show some proof of testing for their work. This can be with unit tests or with new pytests added to one of the three groups below. + +**Quick tests** have no marker on them. They run on every push and +decide whether a pull request is allowed to merge. Most tests should be in this +group. Write them so they stay fast. + +**Slow tests** are marked `@pytest.mark.slow`. They are left out when you push, +so they never hold up ordinary work. Run them on request, as described below. + +**GPU tests** are marked `@pytest.mark.gpu`. These will only run on the project's own GPU machine, on request. + +A GPU test needs two lines on it, not one: + +```python +@pytest.mark.gpu +@pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a GPU") +def test_core_model_runs(): + ... +``` + +The marker is what lets the GPU machine pick the test out and run it. The +`skipif` is what lets the test sit harmlessly in the suite everywhere else: on +a machine with no GPU it is reported as skipped instead of failing. Without +`skipif`, running the whole suite would fail for a reason that is not a real +problem with the code. + +## Running extra tests on a pull request + +Pushing only runs the quick tests. To run more, leave a comment on the pull +request: + +| Comment | What it runs | +| --- | --- | +| `/test all` or `/test` | Every group below except `gpu`, at the same time. | +| `/test python` | The quick tests. | +| `/test slow` | The tests marked `slow`. | +| `/test gpu` | The tests marked `gpu`, on the project's GPU machine. | +| `/test web` | The 3D visualizer. | +| `/test help` | Lists these commands. | + +Each comment runs its own job on the latest commit pushed to the branch, re-running a command while a job of the same type is running will cancel the previous run and start a new one on the latest commit. + +Only people with access to this repository can use them. A short while after +commenting you should see a 👀 reaction on your comment, and then a reply with +the result. + +The reply names the exact commit it tested. That matters: if you push more +commits afterwards, the old result no longer applies to the new code, and a +reviewer is meant to notice this and ask for the tests to be run again. + +These commands do not block merging on their own. Only the automatic checks do +that. Whether the extra tests were run is something the reviewer checks, using +the checklist in the pull request template. diff --git a/README.md b/README.md index 643d64c..25983d1 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,27 @@ -# 2026 Tech Project Template +# terrain-diffusion -Template repository for student tech projects. +A reproduction of the InfiniteDiffusion paper: generating realistic landscape +terrain that continues forever in every direction, comes out the same every +time from the same seed, and uses a fixed amount of memory no matter how far +you explore. -The CI workflow and local hook scripts are examples for a Node.js project. Update them to match the language, package manager, linter, and test runner used by the project created from this template. -This template includes pull requests, branch protection, CI checks, and optional Git hooks to give student developers practice with workflows used on real engineering teams. These checks also help keep the repository cleaner by catching formatting, linting, and test issues before code is merged. +## Getting set up -## Repository Setup - -After creating a repository from this template: - -1. Configure branch protection for `main` using [docs/branch-protection.md](docs/branch-protection.md). - -2. Optional: install local Git hooks: - - ```bash - scripts/install-git-hooks.sh - ``` - -3. Replace or adjust the example quality checks as needed. The included Node.js checks run these commands when they are defined in `package.json`: - - - `npm run lint` - - `npm run test` - -## Pull Request Flow - -- Work on a feature branch. -- Open a pull request into `main`. -- Complete the pull request checklist. -- Wait for the required quality check to pass. -- Get review approval before merging. - -## Local Quality Checks - -Run the same checks locally: +Install [uv](https://docs.astral.sh/uv/getting-started/installation/), the tool used to manage Python and Python packages: ```bash -scripts/quality-check.sh +curl -LsSf https://astral.sh/uv/install.sh | sh ``` -The pre-commit hook runs this script before each commit. - -## Scripts +Then install the project's packages: -These files are small command-line programs. They are written as `.sh` shell scripts so they can be run locally from the terminal and also reused by GitHub Actions. - -- `scripts/install-dependencies.sh` - - Installs the packages needed by a Node.js project. In most Node projects, dependencies are listed in `package.json` and installed with a package manager like `npm`, `pnpm`, or `yarn`. This script checks which lockfile the project has and uses the matching package manager. - - This is mainly used by CI before running tests, because a fresh GitHub Actions runner does not already have the project's dependencies installed. - -- `scripts/quality-check.sh` - - Runs the project's automated checks. In this template, it looks for `lint` and `test` scripts in `package.json`. - - A lint command checks code style and catches common mistakes. A test command runs the project's automated tests. If either command fails, the script fails too, which helps stop broken code from being merged. - -- `scripts/install-git-hooks.sh` - - Tells Git to use the hook files in the `githooks/` folder. You usually run this once after cloning or creating the repository. - - Git hooks are scripts that Git can run automatically at certain moments, such as right before making a commit. +```bash +scripts/install-dependencies.sh +``` -- `githooks/pre-commit` +> You do not need to install python, uv handles and maintains the correct python version. - Runs before Git creates a commit, but only after the hooks have been installed with `scripts/install-git-hooks.sh`. +## Contributing - This hook runs `scripts/quality-check.sh`, so developers get quick feedback before committing code that does not pass the project's checks. +See [CONTRIBUTING.md](CONTRIBUTING.md) for how to run the checks, how tests are organised, and how changes get reviewed and merged. diff --git a/docs/branch-protection.md b/docs/branch-protection.md index ea7b6f0..8678825 100644 --- a/docs/branch-protection.md +++ b/docs/branch-protection.md @@ -4,11 +4,17 @@ GitHub template repositories copy files, but they do not copy repository setting Use this checklist after creating a new repository from the template. -## Configure `main` +## 1. Run the checks once -Go to: +Required checks cannot be selected until they have reported at least once. -`Settings -> Branches -> Branch protection rules -> Add branch protection rule` +1. Push the repository to GitHub. +2. Open a pull request, or push a commit to `main`. +3. Wait for the CI workflow to finish. + +## 2. Configure `main` + +`Settings -> Branches -> Add branch protection rule` Set the branch name pattern to: @@ -29,47 +35,60 @@ Enable these settings: - Block force pushes. - Block branch deletion. -## Required Status Check +Select all three required status checks: -Before the status check appears in the branch protection selector, the GitHub Actions workflow must run at least once. +```text +format +test +web +``` -The included `CI` workflow is an example. If a project changes the workflow or job names, require that project's equivalent lint/test status check instead. +They may instead appear in the list as `CI / format`, `CI / test`, and +`CI / web`. Choose whichever version GitHub shows you. `web` passes until the +`web/` folder exists, and starts checking it once it does. -To make it appear: +Leave switched off: -1. Push the repository to GitHub. -2. Open a pull request, or push a commit to `main`. -3. Wait for the `CI` workflow to run. -4. Return to the branch protection rule. -5. Select the required status check. +- Require code quality results. It needs GitHub code scanning, which this + repository does not use, so every pull request would wait on an analysis that + never runs. -The check may appear as either: +## 3. Pull Request Settings -```text -quality -``` +`Settings -> General -> Pull Requests` -or: +- Enable squash merging. +- Disable merge commits. +- Disable rebase merging. +- Enable automatically delete head branches. -```text -CI / quality -``` +## 4. Actions Permissions -Choose whichever one GitHub shows for this repository. +`Settings -> Actions -> General` -## Recommended Repository Settings +- Actions permissions: allow actions created by third parties, or add + `astral-sh/setup-uv@*` to the allow list. `actions/checkout` and + `actions/setup-node` are GitHub's own, but `astral-sh/setup-uv` is not, so the + GitHub-only option is not enough and every job fails immediately. +- Workflow permissions: Read repository contents permission. Each workflow asks + for anything more at the top of its own file. -Go to: +If an organisation owns this repository, these settings also exist at +organisation level, and the stricter of the two applies. -`Settings -> General -> Pull Requests` +## 5. Add the GPU Machine -Recommended settings: +`Settings -> Actions -> Runners -> New self-hosted runner` -- Enable squash merging. -- Disable merge commits. -- Disable rebase merging unless the project specifically wants it. -- Enable automatically delete head branches. +Follow the instructions on that page. Add the label `gpu`. `self-hosted` is +applied automatically, and the workflow asks for both. -## Notes +Then add `gpu` back to the `all` group in `.github/scripts/parse-command.sh`, +where a TODO marks the line. It is left out until the machine exists so that +`/test all` cannot wait on a runner that is not there. -These settings cannot be enforced by files in this template alone. The files in `.github/` provide pull request templates and CI checks, but GitHub branch protection must be configured in each created repository. +Until this is done, do not use `/test gpu`. GitHub allows a job to sit in the +queue waiting for a self-hosted runner for 24 hours before giving up, and the +hour long timeout on the job does not apply, because that limits how long a job +may run rather than how long it may wait. No result comment is posted until the +queue time runs out. \ No newline at end of file diff --git a/githooks/pre-commit b/githooks/pre-commit deleted file mode 100755 index 4266712..0000000 --- a/githooks/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -set -eu - -exec scripts/quality-check.sh --pre-commit diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..73d430f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[project] +name = "terrain-diffusion" +version = "0.1.0" +description = "Reproduction of InfiniteDiffusion: seamless, infinite, seed-determined terrain generation." +readme = "README.md" +requires-python = ">=3.14" + +# Packages the project itself needs in order to run +# TODO: add entries here as the project starts needing them, +# then run `uv sync` to install and update uv.lock +dependencies = [] + +[dependency-groups] +dev = [ + "pytest>=8.3", + "ruff>=0.9", +] + +[tool.uv] +# TODO: When the first package is created under src/, delete this line and add a +# [build-system] section so the package can be installed. +package = false + + +# --------------------------------------------------------------------------- +# ruff: formats code and checks it for mistakes +# --------------------------------------------------------------------------- + +[tool.ruff] +line-length = 100 +target-version = "py314" +extend-exclude = ["*.md"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors: spacing and layout problems + "W", # pycodestyle warnings: trailing whitespace and similar + "F", # pyflakes: unused imports, undefined names, real mistakes + "I", # isort: keeps imports in a consistent order + "UP", # pyupgrade: flags outdated Python syntax + "B", # flake8-bugbear: common bug patterns +] +ignore = [ + "E501", # ignore line length complains since formatter decides +] + + +# --------------------------------------------------------------------------- +# pytest: finds and runs the tests +# --------------------------------------------------------------------------- + +[tool.pytest.ini_options] +# raise malformed pytest @ marks or config as errors +addopts = "--strict-markers --strict-config" + +markers = [ + "slow: use for slow tests, runs on /test slow comment", + "gpu: use for GPU tests, runs on /test gpu comment", +] diff --git a/scripts/format-check.sh b/scripts/format-check.sh new file mode 100755 index 0000000..89f6a81 --- /dev/null +++ b/scripts/format-check.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# Checks that the Python code is formatted correctly and +# has no style problems using ruff +# +# scripts/format-check.sh check, and fail if anything is wrong +# scripts/format-check.sh --fix correct what can be corrected + +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +case "${1:-}" in + --fix) + echo "==> Fixing what can be fixed automatically (ruff check --fix)" + uv run ruff check --fix . + + echo + echo "==> Formatting (ruff format)" + uv run ruff format . + + echo + echo "Done. Anything left over has to be fixed by hand." + exit 0 + ;; + "") ;; + *) + echo "Unknown option: $1" >&2 + echo "Usage: scripts/format-check.sh [--fix]" >&2 + exit 2 + ;; +esac + +check_format=() +if [[ -n "${GITHUB_ACTIONS:-}" ]]; then + check_format=(--output-format=github) +fi + +echo "==> Checking formatting (ruff format --check)" +if ! uv run ruff format --check .; then + echo + echo "Some files are not formatted. Fix them with:" >&2 + echo " scripts/format-check.sh --fix" >&2 + exit 1 +fi + +echo +echo "==> Checking code style (ruff check)" +if ! uv run ruff check "${check_format[@]}" .; then + echo + echo "Found style problems. Many can be fixed automatically with:" >&2 + echo " scripts/format-check.sh --fix" >&2 + exit 1 +fi + +echo +echo "Formatting and style are fine." diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 99e0881..fef5a8d 100755 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -1,18 +1,17 @@ #!/usr/bin/env bash +# +# Installs everything the project needs, run once after cloning +# scripts/install-python-dependencies.sh +# scripts/install-web-dependencies.sh +# set -euo pipefail -if [[ ! -f package.json ]]; then - exit 0 -fi +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -if [[ -f pnpm-lock.yaml ]]; then - corepack enable - pnpm install --frozen-lockfile -elif [[ -f yarn.lock ]]; then - corepack enable - yarn install --immutable -elif [[ -f package-lock.json || -f npm-shrinkwrap.json ]]; then - npm ci -else - npm install -fi +scripts/install-python-dependencies.sh + +echo +scripts/install-web-dependencies.sh + +echo +echo "Dependencies installed." diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh deleted file mode 100755 index 7e56b10..0000000 --- a/scripts/install-git-hooks.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(git rev-parse --show-toplevel)" -git -C "$repo_root" config core.hooksPath githooks - -echo "Git hooks installed from githooks/" diff --git a/scripts/install-python-dependencies.sh b/scripts/install-python-dependencies.sh new file mode 100755 index 0000000..9d938cb --- /dev/null +++ b/scripts/install-python-dependencies.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# Installs the Python packages the project needs. + +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if ! command -v uv > /dev/null 2>&1; then + echo "uv is not installed, and this project uses it to manage packages." >&2 + echo >&2 + echo "Install it with:" >&2 + echo " curl -LsSf https://astral.sh/uv/install.sh | sh" >&2 + echo >&2 + echo "Other options: https://docs.astral.sh/uv/getting-started/installation/" >&2 + exit 1 +fi + +# will fail when new packages are installed but not synced +echo "Installing Python packages with uv..." +uv sync --frozen diff --git a/scripts/install-web-dependencies.sh b/scripts/install-web-dependencies.sh new file mode 100755 index 0000000..dbfc093 --- /dev/null +++ b/scripts/install-web-dependencies.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Installs the JavaScript packages for the visualizer in web/ +# +# TODO: update when a web project is setup + +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if [[ ! -f web/package.json ]]; then + echo "No web/package.json yet, so there is nothing to install." + exit 0 +fi + +echo "Installing JavaScript packages in web/..." + +if [[ -f web/pnpm-lock.yaml ]]; then + corepack enable + (cd web && pnpm install --frozen-lockfile) +elif [[ -f web/yarn.lock ]]; then + corepack enable + (cd web && yarn install --immutable) +elif [[ -f web/package-lock.json ]]; then + (cd web && npm ci) +else + (cd web && npm install) +fi diff --git a/scripts/quality-check.sh b/scripts/quality-check.sh index e868426..46039fe 100755 --- a/scripts/quality-check.sh +++ b/scripts/quality-check.sh @@ -1,45 +1,24 @@ #!/usr/bin/env bash -set -euo pipefail - -has_command() { - command -v "$1" > /dev/null 2>&1 -} +# +# Runs every check, should run before pushing. +# scripts/format-check.sh formatting and code style +# scripts/test-check.sh quick tests +# scripts/web-check.sh the visualizer, once web/ exists +# +# Run any of those on their own if you only want that part. To correct +# formatting problems, run scripts/format-check.sh --fix. -run_node_script_if_present() { - local script_name="$1" +set -euo pipefail - if [[ ! -f package.json ]]; then - return 0 - fi +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - if ! node -e "const p=require('./package.json'); process.exit(p.scripts && p.scripts['$script_name'] ? 0 : 1)" 2> /dev/null; then - return 0 - fi +scripts/format-check.sh - if [[ -f pnpm-lock.yaml ]]; then - if ! has_command pnpm && has_command corepack; then - corepack enable - fi - if ! has_command pnpm; then - echo "pnpm-lock.yaml found, but pnpm is not installed." >&2 - exit 1 - fi - pnpm run "$script_name" - elif [[ -f yarn.lock ]]; then - if ! has_command yarn && has_command corepack; then - corepack enable - fi - if ! has_command yarn; then - echo "yarn.lock found, but yarn is not installed." >&2 - exit 1 - fi - yarn "$script_name" - else - npm run "$script_name" - fi -} +echo +scripts/test-check.sh -run_node_script_if_present lint -run_node_script_if_present test +echo +scripts/web-check.sh -echo "Quality checks completed." +echo +echo "All checks passed." diff --git a/scripts/test-check.sh b/scripts/test-check.sh new file mode 100755 index 0000000..3930961 --- /dev/null +++ b/scripts/test-check.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# +# Runs the tests based on groups: +# scripts/test-check.sh the quick tests, same as `python` below +# scripts/test-check.sh python the quick tests +# scripts/test-check.sh slow the tests marked `slow` +# scripts/test-check.sh gpu the tests marked `gpu` +# scripts/test-check.sh all every test +# +# Anything after the group name is passed straight to pytest: +# scripts/test-check.sh python -k sampler +# scripts/test-check.sh all -x + +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +group=python +case "${1:-}" in + --all) + group=all + shift + ;; + python | slow | gpu | all) + group="$1" + shift + ;; + web) + echo "The web tests are not pytest, so they are not run from here." >&2 + echo "Use: scripts/web-check.sh" >&2 + exit 2 + ;; + "") + # No arguments, so the default group stands. quality-check.sh calls it this way. + ;; + -* | */* | *.py) + # pytest arguments with no group in front of them. + echo "Name the test group before the pytest arguments:" >&2 + echo " scripts/test-check.sh python $*" >&2 + exit 2 + ;; + *) + echo "Unknown test group: $1" >&2 + echo "Usage: scripts/test-check.sh [python|slow|gpu|all] [pytest arguments]" >&2 + exit 2 + ;; +esac + +case "$group" in + python) markers='not slow and not gpu' ;; + slow) markers='slow' ;; + gpu) markers='gpu' ;; + all) markers='' ;; +esac + +marker_args=() +if [[ -n "$markers" ]]; then + marker_args=(-m "$markers") +fi + +echo "==> Running the '$group' tests (pytest)" + +status=0 +uv run pytest "${marker_args[@]}" "$@" || status=$? + +if [[ $status -eq 5 ]]; then + if [[ "$group" == "python" ]]; then + # Fail if no tests ran + echo >&2 + echo "No tests were found." >&2 + echo "CI Verification test should have run, check CI for errors." >&2 + exit 1 + fi + + echo + echo "No tests in the '$group' group, so there was nothing to run." + exit 0 +fi + +exit "$status" diff --git a/scripts/web-check.sh b/scripts/web-check.sh new file mode 100755 index 0000000..5c6a243 --- /dev/null +++ b/scripts/web-check.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# runs lint + correctness tests under web/. +# +# TODO: verify this works when web folder is populated + +set -euo pipefail + +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if [[ ! -f web/package.json ]]; then + echo "No web/package.json yet, so there is nothing to check." + exit 0 +fi + +runner=(npm run) +if [[ -f web/pnpm-lock.yaml ]]; then + runner=(pnpm run) +elif [[ -f web/yarn.lock ]]; then + runner=(yarn) +fi + +for script in lint test; do + if node -e "const p=require('./web/package.json'); process.exit(p.scripts && p.scripts['$script'] ? 0 : 1)" 2> /dev/null; then + echo + echo "==> Running '$script' in web/" + (cd web && "${runner[@]}" "$script") + else + echo "web/package.json has no '$script' script, skipping." + fi +done diff --git a/tests/test_environment.py b/tests/test_environment.py new file mode 100644 index 0000000..543ead3 --- /dev/null +++ b/tests/test_environment.py @@ -0,0 +1,46 @@ +"""Checks the development environment is set up correctly + +Can maybe be deleted when real tests are added? +""" + +import shutil +import sys + +MINIMUM_PYTHON_VERSION = (3, 14) + + +def test_python_version_is_supported() -> None: + """check project needs Python 3.14 or newer + + If this fails, run `uv sync`. uv reads the version from the + .python-version file and will download the right one if it is missing. + """ + assert sys.version_info[:2] >= MINIMUM_PYTHON_VERSION, ( + f"This project needs Python {MINIMUM_PYTHON_VERSION[0]}.{MINIMUM_PYTHON_VERSION[1]} " + f"or newer, but the tests are running on {sys.version_info[0]}.{sys.version_info[1]}." + ) + + +def test_development_tools_are_installed() -> None: + """ruff should be available as it's part of dev dependencies + + If this fails, run `uv sync` and `uv run pytest` so the project's environment is used + """ + assert shutil.which("ruff") is not None, ( + "ruff was not found. Run `uv sync` to install the development " + "dependencies, then run tests with `uv run pytest`." + ) + + +def test_slow_and_gpu_markers_are_registered(pytestconfig) -> None: + """`slow` and `gpu` markers are registered in pyproject.toml. + + For now, `slow` and `gpu` are the only extra pytest markers that partition our tests + """ + registered = {line.split(":", 1)[0] for line in pytestconfig.getini("markers")} + + for marker in ("slow", "gpu"): + assert marker in registered, ( + f"The '{marker}' marker is not registered. Add it back to the " + f"markers list in the [tool.pytest.ini_options] section of pyproject.toml." + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bdf91b4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,108 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, +] + +[[package]] +name = "terrain-diffusion" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3" }, + { name = "ruff", specifier = ">=0.9" }, +]