Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/.copier-answers.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion .config/pyrefly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .config/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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 <tool>, npx <tool>, etc.). ✅ pnpm test-unit ❌ pnpm vitest ... or npx vitest ...
- For linting and type-checking, prefer `pre-commit run <hook-id>` 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 <hook-id>` 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 <path>` or `uv --directory <path>` 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 <path>` 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.
Expand Down
1 change: 1 addition & 0 deletions extensions/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion template/.config/pyrefly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions template/.config/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions template/.devcontainer/devcontainer.json.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading