diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 2efdf63f6..f1f014cf5 100644 --- a/.config/.copier-answers.yml +++ b/.config/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v0.0.139 +_commit: v0.0.143 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: A web app that is hosted within a local intranet. Nuxt frontend, python diff --git a/.config/pyrefly.toml b/.config/pyrefly.toml index 560430746..17aa02d6a 100644 --- a/.config/pyrefly.toml +++ b/.config/pyrefly.toml @@ -22,7 +22,7 @@ project-includes = [ "../tests" ] search-path = [".."] -disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly things the src is excluded because it's an editable local install +disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly thinks the src is excluded because it's an editable local install python-platform = "linux" infer-with-first-use = false # more similar to pyright. must define the empty list type when instantiated # "strict" enables the maximalist error set plus strict-callable-subtyping and diff --git a/.config/ruff.toml b/.config/ruff.toml index 12331cc15..db5ea4bc8 100644 --- a/.config/ruff.toml +++ b/.config/ruff.toml @@ -39,6 +39,9 @@ exclude = [ line-length = 120 indent-width = 4 +# resolved relative to the cwd where ruff runs (repo root under pre-commit), so this nests the cache under .config +cache-dir = ".config/.ruff_cache" + target-version = "py312" [lint] @@ -66,8 +69,11 @@ ignore = [ "N999", # Ignoring this since we are using jinja templates in the directory path which will eventually turn into a valid module name. # TODO: make a task in downstream templates to remove this "S101", # We do not use the -o optimize flag, so it's fine to use `assert` in the main code. It's especially helpful for static typing "SIM102", # Nested if statements can be better analyzed by coverage detectors than compound statements + "SIM103", # Explicit if/else branches let the coverage checker track both paths, unlike evaluating an expression "SIM108", # Explicit if/else branches let the coverage checker track both paths, unlike a ternary + "SIM110", # Explicit if statements and loops let the coverage checker track both paths, unlike using `any` or `all` with a generator expression "SIM114", # Using `or` statements to simplify this would confuse the coverage checker + "SIM401", # using dictionary getters to simplify this would inhibit the coverage checker from tracking both paths "TD002", # Adding author names to TODOs erodes shared ownership of codebase. Git history provides information about who originally created the TODO if that information is vitally needed "TD003", # Up to the author's judgement whether a TODO requires a link to an issue or not "TID252", # Sometimes it makes sense to use relative imports, that's a judgement call for us, not ruff diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e0c53196b..069215e80 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,16 +16,16 @@ "extensions": [ // basic tooling // "eamodio.gitlens@15.5.1", - "coderabbit.coderabbit-vscode@0.20.7", + "coderabbit.coderabbit-vscode@0.21.0", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", - "anthropic.claude-code@2.1.205", + "anthropic.claude-code@2.1.211", // Python "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.1.9001", + "meta.pyrefly@1.1.9002", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.56.0", + "charliermarsh.ruff@2026.60.0", // Misc file formats "bierner.markdown-mermaid@1.29.0", @@ -60,5 +60,5 @@ "initializeCommand": "sh .devcontainer/initialize-command.sh", "onCreateCommand": "sh .devcontainer/on-create-command.sh", "postStartCommand": "sh .devcontainer/post-start-command.sh" - // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 7f763b1e # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): ce58396b # spellchecker:disable-line } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b25cdd683..035ace0c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,7 +49,7 @@ repos: # Reformatting (should generally come before any file format or other checks, because reformatting can change things) - repo: https://github.com/crate-ci/typos - rev: 359573a3ff38f71c93e160f2ace96d118f6ff42e # frozen: v1 + rev: 96d9af6217dc2855210655af7e1ff19d23d4e593 # frozen: v1 hooks: - id: typos args: [--write-changes, --force-exclude, --config, .config/_typos.toml] @@ -113,7 +113,7 @@ repos: )$ - repo: https://github.com/rbubley/mirrors-prettier - rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3 + rev: 9337a74165b178ae2c766f60bee7252a0f06f3e8 # frozen: v3.9.5 hooks: - id: prettier args: [--config, .config/.prettierrc] @@ -185,7 +185,7 @@ repos: - id: check-case-conflict - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 943377262562a12b57292fc98fabd7dbf81451fe # frozen: 0.37.2 + rev: 1a4bb160cab6417b3045e1b37b6b72449243e658 # frozen: 0.37.4 hooks: - id: check-github-workflows @@ -239,7 +239,7 @@ repos: description: Runs hadolint to lint Dockerfiles - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 6fec9b7edb08fd9989088709d864a7826dc74e80 # frozen: v0.15.12 + rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22 hooks: - id: ruff name: ruff-src @@ -270,7 +270,7 @@ repos: )$ - repo: https://github.com/pylint-dev/pylint - rev: 88e1ab7545a4af4aea15c305a154c164a95ab842 # frozen: v4.0.5 + rev: 8a396357098337ba8be714fffdf2d00947a9778c # frozen: v4.0.6 hooks: - id: pylint name: pylint diff --git a/AGENTS.md b/AGENTS.md index b518bb5b1..ce5d03b76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,13 +9,18 @@ This project is a Copier template used to generate applications that are able to - Comments should be used very rarely. Code should generally express its intent. - Never write a one-line docstring — either the name is sufficient or the behavior warrants a full explanation. - Don't sort or remove imports manually — pre-commit handles it. -- Always include type hints for pyright in Python -- Respect the pyright rule reportUnusedCallResult; assign unneeded return values to `_` - Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript. - When disabling a linting rule with an inline directive, provide a comment at the end of the line (or on the line above for tools that don't allow extra text after an inline directive) describing the reasoning for disabling the rule. - Avoid telling the type checker what a type is rather than letting it prove it. This includes type assertions (`as SomeType` in TypeScript, `cast()` in Python) and variable annotations that override inference. Prefer approaches that let the type checker verify the type itself: `isinstance`/`instanceof` narrowing, restructuring code so the correct type flows naturally, or using discriminated unions. When there is genuinely no alternative, add a comment explaining why the workaround is necessary and why it is safe. - Avoid `||` (TypeScript) or `or` (Python) in `if`/`elif` conditions, and avoid `x in ['a', 'b']`-style membership tests in implementation code — coverage tools treat these as a single branch, silently masking untested paths and producing false 100% branch coverage. Use separate `if`/`elif` branches instead so each condition is independently covered. +### Python + +- Always include type hints. +- Respect the pyrefly unused-call-result check; assign unneeded return values to `_` +- Prefer explicit `if`/`else` (or a `for` loop with `if`/`return`) over one-line forms that collapse branches: a ternary, `d.get(key, default)`, returning a boolean expression `return b > 5`/`return bool(b)`, or `any()`/`all()` over a generator in place of a loop. `coverage.py` tracks branches as line-to-line arcs, so a single-line expression hides the untaken path. +- When filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. + ## Testing - Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default. @@ -24,7 +29,6 @@ This project is a Copier template used to generate applications that are able to - Avoid magic values in comparisons in tests in all languages (like ruff rule PLR2004 specifies). Note: `1` and `0` are not magic numbers (according to PLR2004) - Prefer using random values in tests rather than arbitrary ones (e.g. the faker library, uuids, random.randint) when possible. For enums, pick randomly rather than hardcoding one value. - Avoid loops in tests — assert each item explicitly so failures pinpoint the exact element. When verifying a condition across all items in a collection, collect the violations into a list and assert it's empty (e.g., assert [x for x in items if bad_condition(x)] == []). -- When filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. - When a test's final assertion is an absence (e.g., element is `null`, list is empty, modal is closed), include a prior presence assertion confirming the expected state existed before the action that removed it. A test whose only assertion is an absence check can pass vacuously if setup silently failed. - When asserting a mock or spy was called with specific arguments, always constrain as tightly as possible. In order of preference: (1) assert called exactly once with those args (`assert_called_once_with` in Python, `toHaveBeenCalledExactlyOnceWith` in Vitest/Jest); (2) if multiple calls are expected, assert the total call count and use a positional or last-call assertion (`nthCalledWith`, `lastCalledWith` / `assert_has_calls` with `call_args_list[n]`); (3) plain "called with at any point" (`toHaveBeenCalledWith`, `assert_called_with`) is a last resort only when neither the call count nor the call order can reasonably be constrained. - When asserting an exception is raised, verify the error message includes all key constructor arguments — not just one identifying field. This ensures the error message is fully populated and catches cases where arguments are swapped or missing. In Python: use the `match` parameter in `pytest.raises`. In TypeScript: use a regex or substring in `toThrow`, or catch and assert on error properties individually. @@ -77,7 +81,7 @@ This project is a Copier template used to generate applications that are able to - ❌ Never use `uv run python -c "import ...; print(...)"` or `inspect` to introspect Python source. ✅ Read source files directly or grep for symbols — the code is on disk and can be read without running it. - Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior. - For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec , npx , etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ... -- For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyright`, `ruff`, `ruff-format`. +- For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyrefly`, `ruff`, `ruff-format`. - Never rely on IDE diagnostics for ruff warnings — the IDE may not respect the project's ruff.toml config. Run `pre-commit run ruff -a` to get accurate results. - Never use `pnpm --prefix ` or `uv --directory ` to target a different directory — these flags break the permission allow-list matcher the same way chained `cd &&` commands do. Instead, rely on the working directory already being correct (the cwd persists between Bash tool calls), or issue a plain `cd ` as a separate prior tool call to reposition before running the command. - Never use backslash line continuations in shell commands — always write the full command on a single line. Backslashes break the permission allow-list matcher. diff --git a/extensions/context.py b/extensions/context.py index 6977779fc..ee14f11be 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -113,6 +113,7 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["gha_amazon_ecr_login"] = "v2.1.5" context["gha_setup_node"] = "v6.4.0" context["gha_action_gh_release"] = "v3.0.0" + context["gha_codecov"] = "v7.0.0" context["gha_mutex"] = "1ebad517141198e08d47cf72f3c0975316620a65 # v1.0.0-alpha.10" context["gha_pypi_publish"] = "v1.14.0" context["gha_sleep"] = "v2.0.3" diff --git a/template/.config/pyrefly.toml b/template/.config/pyrefly.toml index 65ebffb80..f1d8c36ba 100644 --- a/template/.config/pyrefly.toml +++ b/template/.config/pyrefly.toml @@ -24,7 +24,7 @@ project-includes = [ # "../backend" is needed so the tests package (rooted at backend/) resolves relative imports; # without it pyrefly infers the import root from this config's dir (.config/) and can't name the test modules search-path = ["../backend/src", "../backend"] -disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly things the src is excluded because it's an editable local install +disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly thinks the src is excluded because it's an editable local install python-platform = "linux" infer-with-first-use = false # more similar to pyright. must define the empty list type when instantiated # "strict" enables the maximalist error set plus strict-callable-subtyping and diff --git a/template/.config/ruff.toml b/template/.config/ruff.toml index 33cfba8fa..37904a263 100644 --- a/template/.config/ruff.toml +++ b/template/.config/ruff.toml @@ -39,6 +39,9 @@ exclude = [ line-length = 120 indent-width = 4 +# resolved relative to the cwd where ruff runs (repo root under pre-commit), so this nests the cache under .config +cache-dir = ".config/.ruff_cache" + target-version = "py312" [lint] @@ -66,8 +69,11 @@ ignore = [ "N999", # Ignoring this since we are using jinja templates in the directory path which will eventually turn into a valid module name. # TODO: make a task in downstream templates to remove this "S101", # We do not use the -o optimize flag, so it's fine to use `assert` in the main code. It's especially helpful for static typing "SIM102", # Nested if statements can be better analyzed by coverage detectors than compound statements + "SIM103", # Explicit if/else branches let the coverage checker track both paths, unlike evaluating an expression "SIM108", # Explicit if/else branches let the coverage checker track both paths, unlike a ternary + "SIM110", # Explicit if statements and loops let the coverage checker track both paths, unlike using `any` or `all` with a generator expression "SIM114", # Using `or` statements to simplify this would confuse the coverage checker + "SIM401", # using dictionary getters to simplify this would inhibit the coverage checker from tracking both paths "TD002", # Adding author names to TODOs erodes shared ownership of codebase. Git history provides information about who originally created the TODO if that information is vitally needed "TD003", # Up to the author's judgement whether a TODO requires a link to an issue or not "TID252", # Sometimes it makes sense to use relative imports, that's a judgement call for us, not ruff diff --git a/template/.devcontainer/devcontainer.json.jinja b/template/.devcontainer/devcontainer.json.jinja index 2493a6792..8e5885bb5 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -35,16 +35,16 @@ "-AmazonWebServices.aws-toolkit-vscode", // the AWS CLI feature installs this automatically, but it's causing problems in VS Code{% endraw %}{% endif %}{% raw %} // basic tooling // "eamodio.gitlens@15.5.1", - "coderabbit.coderabbit-vscode@0.20.7", + "coderabbit.coderabbit-vscode@0.21.0", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905",{% endraw %}{% if install_claude_cli %}{% raw %} - "anthropic.claude-code@2.1.205",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if (is_child_of_copier_base_template is defined and is_child_of_copier_base_template is sameas(true)) or (is_child_of_copier_base_template is not defined and template_uses_python is defined and template_uses_python is sameas(true)) %}{% raw %} + "anthropic.claude-code@2.1.211",{% endraw %}{% endif %}{% raw %}{% endraw %}{% if (is_child_of_copier_base_template is defined and is_child_of_copier_base_template is sameas(true)) or (is_child_of_copier_base_template is not defined and template_uses_python is defined and template_uses_python is sameas(true)) %}{% raw %} // Python "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.1.9001", + "meta.pyrefly@1.1.9002", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.56.0",{% endraw %}{% endif %}{% raw %} + "charliermarsh.ruff@2026.60.0",{% endraw %}{% endif %}{% raw %} {% endraw %}{% if is_child_of_copier_base_template is not defined and template_uses_vuejs is defined and template_uses_vuejs is sameas(true) %}{% raw %} // VueJS "vue.volar@3.2.5", diff --git a/template/AGENTS.md b/template/AGENTS.md index a534bfc3e..b5514c643 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -11,13 +11,18 @@ - Comments should be used very rarely. Code should generally express its intent. - Never write a one-line docstring — either the name is sufficient or the behavior warrants a full explanation. - Don't sort or remove imports manually — pre-commit handles it. -- Always include type hints for pyright in Python -- Respect the pyright rule reportUnusedCallResult; assign unneeded return values to `_` - Prefer keyword-only parameters (unless a very clear single-argument function): use `*` in Python signatures and destructured options objects in TypeScript. - When disabling a linting rule with an inline directive, provide a comment at the end of the line (or on the line above for tools that don't allow extra text after an inline directive) describing the reasoning for disabling the rule. - Avoid telling the type checker what a type is rather than letting it prove it. This includes type assertions (`as SomeType` in TypeScript, `cast()` in Python) and variable annotations that override inference. Prefer approaches that let the type checker verify the type itself: `isinstance`/`instanceof` narrowing, restructuring code so the correct type flows naturally, or using discriminated unions. When there is genuinely no alternative, add a comment explaining why the workaround is necessary and why it is safe. - Avoid `||` (TypeScript) or `or` (Python) in `if`/`elif` conditions, and avoid `x in ['a', 'b']`-style membership tests in implementation code — coverage tools treat these as a single branch, silently masking untested paths and producing false 100% branch coverage. Use separate `if`/`elif` branches instead so each condition is independently covered. +### Python + +- Always include type hints. +- Respect the pyrefly unused-call-result check; assign unneeded return values to `_` +- Prefer explicit `if`/`else` (or a `for` loop with `if`/`return`) over one-line forms that collapse branches: a ternary, `d.get(key, default)`, returning a boolean expression `return b > 5`/`return bool(b)`, or `any()`/`all()` over a generator in place of a loop. `coverage.py` tracks branches as line-to-line arcs, so a single-line expression hides the untaken path. +- When filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. + ## Testing - Always run tests with an explicit path (e.g. uv run pytest tests/unit) — test runners discover all types (unit, integration, E2E...) by default. @@ -26,7 +31,6 @@ - Avoid magic values in comparisons in tests in all languages (like ruff rule PLR2004 specifies). Note: `1` and `0` are not magic numbers (according to PLR2004) - Prefer using random values in tests rather than arbitrary ones (e.g. the faker library, uuids, random.randint) when possible. For enums, pick randomly rather than hardcoding one value. - Avoid loops in tests — assert each item explicitly so failures pinpoint the exact element. When verifying a condition across all items in a collection, collect the violations into a list and assert it's empty (e.g., assert [x for x in items if bad_condition(x)] == []). -- When filtering logic combines multiple `and`-joined guards (e.g. a null check alongside a value check), prefer a loop with explicit `if`/`continue` branches over a single-line comprehension. A compound boolean filter on one line hides individual branches from line coverage — each guard condition should be its own statement so missing test cases are surfaced. - When a test's final assertion is an absence (e.g., element is `null`, list is empty, modal is closed), include a prior presence assertion confirming the expected state existed before the action that removed it. A test whose only assertion is an absence check can pass vacuously if setup silently failed. - When asserting a mock or spy was called with specific arguments, always constrain as tightly as possible. In order of preference: (1) assert called exactly once with those args (`assert_called_once_with` in Python, `toHaveBeenCalledExactlyOnceWith` in Vitest/Jest); (2) if multiple calls are expected, assert the total call count and use a positional or last-call assertion (`nthCalledWith`, `lastCalledWith` / `assert_has_calls` with `call_args_list[n]`); (3) plain "called with at any point" (`toHaveBeenCalledWith`, `assert_called_with`) is a last resort only when neither the call count nor the call order can reasonably be constrained. - When asserting an exception is raised, verify the error message includes all key constructor arguments — not just one identifying field. This ensures the error message is fully populated and catches cases where arguments are swapped or missing. In Python: use the `match` parameter in `pytest.raises`. In TypeScript: use a regex or substring in `toThrow`, or catch and assert on error properties individually. @@ -85,7 +89,7 @@ - ❌ Never use `uv run python -c "import ...; print(...)"` or `inspect` to introspect Python source. ✅ Read source files directly or grep for symbols — the code is on disk and can be read without running it. - Check .devcontainer/devcontainer.json for tooling versions (Python, Node, etc.) when reasoning about version-specific stdlib or tooling behavior. - For frontend tests, run commands via `pnpm` scripts from `frontend/package.json` — never invoke tools directly (not pnpm exec , npx , etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ... -- For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyright`, `ruff`, `ruff-format`. +- For linting and type-checking, prefer `pre-commit run ` over invoking tools directly — this matches the permission allow-list and mirrors what CI runs. Key hook IDs: `typescript-check`, `eslint`, `pyrefly`, `ruff`, `ruff-format`. - Never rely on IDE diagnostics for ruff warnings — the IDE may not respect the project's ruff.toml config. Run `pre-commit run ruff -a` to get accurate results. - Never use `pnpm --prefix ` or `uv --directory ` to target a different directory — these flags break the permission allow-list matcher the same way chained `cd &&` commands do. Instead, rely on the working directory already being correct (the cwd persists between Bash tool calls), or issue a plain `cd ` as a separate prior tool call to reposition before running the command. - Never use backslash line continuations in shell commands — always write the full command on a single line. Backslashes break the permission allow-list matcher.