From f2340a1a2fc28552aa074a8001db8d6fc43d2e65 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Wed, 12 Aug 2026 10:26:44 +0000 Subject: [PATCH 1/2] pull in --- .claude/hooks/bash-chain-guard.js | 27 ++++- .claude/settings/models.jsonc | 20 ++++ .claude/settings/permissions/bash.jsonc | 1 + .../address-pr-comments/fetch-pr-comments.py | 3 + .coderabbit.yaml | 2 +- .config/.copier-answers.yml | 2 +- .config/.copier-managed-files.json | 6 +- .config/.coveragerc | 2 +- .config/pyrefly.toml | 2 +- .config/ruff.toml | 6 + .devcontainer/devcontainer.json | 12 +- .devcontainer/install-ci-tooling.py | 6 +- .github/workflows/hash_git_files.py | 15 +-- .pre-commit-config.yaml | 28 +---- AGENTS.md | 13 +- copier_template_resources/vcrpy_fixtures.py | 16 +-- extensions/context.py | 20 ++-- pyproject.toml | 7 +- pyrightconfig.json | 113 ------------------ template/.claude/hooks/bash-chain-guard.js | 27 ++++- template/.claude/settings/models.jsonc | 20 ++++ .../.claude/settings/permissions/bash.jsonc | 1 + .../address-pr-comments/fetch-pr-comments.py | 3 + template/.coderabbit.yaml | 2 +- template/.config/.coveragerc | 2 +- template/.config/pyrefly.toml | 2 +- template/.config/ruff.toml | 6 + .../.devcontainer/devcontainer.json.jinja | 10 +- template/.github/workflows/hash_git_files.py | 15 +-- template/.pre-commit-config.yaml | 28 +---- template/AGENTS.md | 13 +- template/pyrightconfig.json | 113 ------------------ tests/__init__.py | 7 ++ tests/conftest.py | 15 +++ uv.lock | 92 ++++---------- 35 files changed, 229 insertions(+), 428 deletions(-) create mode 100644 .claude/settings/models.jsonc delete mode 100644 pyrightconfig.json create mode 100644 template/.claude/settings/models.jsonc delete mode 100644 template/pyrightconfig.json create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py diff --git a/.claude/hooks/bash-chain-guard.js b/.claude/hooks/bash-chain-guard.js index 10a9d04c..0e13f9a2 100644 --- a/.claude/hooks/bash-chain-guard.js +++ b/.claude/hooks/bash-chain-guard.js @@ -1,7 +1,19 @@ #!/usr/bin/env node // PreToolUse hook: blocks command chaining in Bash tool calls -// Detects &&/|| which violate AGENTS.md: "execute exactly one command per tool call" +// Enforces AGENTS.md: "execute exactly one command per tool call" +// Quoted spans and backslash-escapes are replaced with a space before scanning, so a `;` inside a +// string literal (or the trailing `\;` of `find -exec`) is not mistaken for a chain. Replacing with +// a space rather than deleting keeps tokens separate, so `printf hi >\x&` cannot collapse to +// `printf hi >&` and hide the backgrounding `&` behind the redirection lookbehind. // Once https://github.com/anthropics/claude-code/issues/16561 is resolved, this should no longer be necessary +// Improvements based on https://github.com/iandunn/dotfiles/blob/main/claude/hooks/block-chained-commands.js + +const CHAIN_OPERATORS = /&&|\|\||;/; // a single `|` is a pipe, which is allowed +const BACKGROUND_AMPERSAND = /(?])&(?!>)/; // a lone `&` backgrounds the command, but `2>&1` and `&>file` are redirections + +function stripQuotedAndEscaped(command) { + return command.replace(/\\.|'[^']*'|"(?:\\.|[^"\\])*"/g, " "); +} let input = ""; process.stdin.on("data", (chunk) => { @@ -11,15 +23,24 @@ process.stdin.on("end", () => { try { const data = JSON.parse(input); const command = (data.tool_input?.command || "").trim(); + const scannable = stripQuotedAndEscaped(command); - if (/\s(&&|\|\||;)\s|\s&(\s|$)/.test(command)) { + if (CHAIN_OPERATORS.test(scannable)) { process.stderr.write( - "AGENTS.md violation: never chain commands with &&, ||, ;, or &. " + + "AGENTS.md violation: never chain commands with &&, ||, or ;. " + "Run one command per tool call. " + "Use cd as a separate prior tool call instead of cd && ...", ); process.exit(2); } + + if (BACKGROUND_AMPERSAND.test(scannable)) { + process.stderr.write( + "AGENTS.md violation: never background a command with &. " + + "Use the Bash tool's run_in_background option instead.", + ); + process.exit(2); + } } catch (e) { // Silent fail — never block on hook errors } diff --git a/.claude/settings/models.jsonc b/.claude/settings/models.jsonc new file mode 100644 index 00000000..d8918e14 --- /dev/null +++ b/.claude/settings/models.jsonc @@ -0,0 +1,20 @@ +/* + * ============== WARNING ============================================================================== + * File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git + * See .config/.copier-managed-files.json for details. + * + * You are welcome to make changes to this file in your repo if they are custom to your project, + * but if the change should be shared with other projects, please backport it to the template repo. + * ===================================================================================================== + */ +{ + "availableModels": [ + "sonnet", + "opus", + "haiku", + "claude-opus-5", + "claude-opus-4-8", + "claude-sonnet-5", + "claude-sonnet-4-6" + ] +} diff --git a/.claude/settings/permissions/bash.jsonc b/.claude/settings/permissions/bash.jsonc index 4d477c6e..df881f30 100644 --- a/.claude/settings/permissions/bash.jsonc +++ b/.claude/settings/permissions/bash.jsonc @@ -110,6 +110,7 @@ "Bash(just *)", "Bash(lsof *)", "Bash(test *)", + "Bash(xxd)", // this invocation can be used to read, but not to write (so no " *" after it) "Bash(zk *)", // Node.js "Bash(pnpm list *)", diff --git a/.claude/skills/address-pr-comments/fetch-pr-comments.py b/.claude/skills/address-pr-comments/fetch-pr-comments.py index c88b27af..283a46a4 100755 --- a/.claude/skills/address-pr-comments/fetch-pr-comments.py +++ b/.claude/skills/address-pr-comments/fetch-pr-comments.py @@ -70,6 +70,7 @@ def gh_rest(*args: str) -> Any: # noqa: ANN401 — return type is genuinely unk ["gh", *call_args], # noqa: S607 — gh is expected on PATH capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) @@ -91,6 +92,7 @@ def gh_graphql(query: str, variables: dict[str, str | int]) -> Any: # noqa: ANN args, capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) @@ -105,6 +107,7 @@ def fetch_current_user_login() -> str: ["gh", "api", "user", "--jq", ".login"], # noqa: S607 — gh is expected on PATH capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index f7ab5a0c..addc595c 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -13,7 +13,7 @@ reviews: - path: "**/vendor_files/**" instructions: "These files came from a vendor and we're not allowed to change them. Refer to it if you need to understand how the main code interacts with it, but do not make comments about it." - path: "**/*.py" - instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyright prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." + instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyrefly prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." - path: "**/.copier-answers.yml" instructions: "Do not comment about the `_commit` value needing to be a clean release tag. A CI job will fail if that is not the case." - path: ".config/.copier-managed-files.json" diff --git a/.config/.copier-answers.yml b/.config/.copier-answers.yml index 1e3daa07..9426d4f9 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.140 +_commit: v0.0.151 _src_path: gh:LabAutomationAndScreening/copier-base-template.git copier_answers_directory: .config description: Copier template for creating Python libraries and executables diff --git a/.config/.copier-managed-files.json b/.config/.copier-managed-files.json index a050fb85..fa5bf049 100644 --- a/.config/.copier-managed-files.json +++ b/.config/.copier-managed-files.json @@ -33,6 +33,7 @@ ".claude/package.json", ".claude/settings/basics.jsonc", ".claude/settings/hooks.jsonc", + ".claude/settings/models.jsonc", ".claude/settings/permissions/additional-dirs.jsonc", ".claude/settings/permissions/bash.jsonc", ".claude/settings/permissions/read.jsonc", @@ -101,7 +102,6 @@ "copier_template_resources/vcrpy_fixtures.py", "extensions/context.py", "pyproject.toml", - "pyrightconfig.json", "pytest.ini", "scripts/delete_false_positive_rej.py", "sh.bat", @@ -138,6 +138,7 @@ "template/.claude/package.json", "template/.claude/settings/basics.jsonc", "template/.claude/settings/hooks.jsonc", + "template/.claude/settings/models.jsonc", "template/.claude/settings/permissions/additional-dirs.jsonc", "template/.claude/settings/permissions/bash.jsonc", "template/.claude/settings/permissions/read.jsonc", @@ -195,10 +196,11 @@ "template/CHANGELOG.md", "template/README.md.jinja", "template/biome.jsonc", - "template/pyrightconfig.json", "template/pytest.ini", "template/scripts/delete_false_positive_rej.py", "template/sh.bat", + "tests/__init__.py", + "tests/conftest.py", "tests/copier_data/data1.yaml", "tests/copier_data/data2.yaml", "tests/unit/copier_tasks/__init__.py", diff --git a/.config/.coveragerc b/.config/.coveragerc index 86e870c8..c45b0e39 100644 --- a/.config/.coveragerc +++ b/.config/.coveragerc @@ -16,7 +16,7 @@ exclude_also = raise NotImplementedError raise AssertionError - # Don't complain about code specifically to help with pyright import resolution + # Don't complain about code specifically to help with pyrefly import resolution if TYPE_CHECKING: # Don't complain if non-runnable code isn't run: diff --git a/.config/pyrefly.toml b/.config/pyrefly.toml index 56043074..17aa02d6 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 12331cc1..db5ea4bc 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 e5577b5e..7692aee4 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.21.0", + "coderabbit.coderabbit-vscode@0.21.3", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905", - "anthropic.claude-code@2.1.211", + "anthropic.claude-code@2.1.220", // Python "ms-python.python@2026.5.2026052901", - "meta.pyrefly@1.1.9002", + "meta.pyrefly@1.1.9003", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.60.0", + "charliermarsh.ruff@2026.68.0", // Misc file formats "bierner.markdown-mermaid@1.29.0", @@ -49,7 +49,7 @@ // TODO: see if there's a way to specify different configurations for different folders "ruff.configuration": "/workspaces/copier-python-package-template/.config/ruff-non-src.toml", // use the non-src configuration since it's less restrictive and won't show false positives and underline things "python.pyrefly.configPath": "/workspaces/copier-python-package-template/.config/pyrefly.toml", - "prettier.configPath": ".config/.prettierrc", + "prettier.configPath": "/workspaces/copier-python-package-template/.config/.prettierrc", "[jsonc][json][javascript][typescript][graphql][css][scss][html][vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true @@ -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): d56eca75 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): bf3441ae # spellchecker:disable-line } diff --git a/.devcontainer/install-ci-tooling.py b/.devcontainer/install-ci-tooling.py index 12e9f19e..c73a3133 100644 --- a/.devcontainer/install-ci-tooling.py +++ b/.devcontainer/install-ci-tooling.py @@ -14,9 +14,9 @@ import tempfile from pathlib import Path -UV_VERSION = "0.11.25" -PNPM_VERSION = "11.9.0" -COPIER_VERSION = "==9.16.0" +UV_VERSION = "0.12.1" +PNPM_VERSION = "11.20.0" +COPIER_VERSION = "==9.17.0" COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3" PRE_COMMIT_VERSION = "4.5.1" GITHUB_WINDOWS_RUNNER_BIN_PATH = r"C:\Users\runneradmin\.local\bin" diff --git a/.github/workflows/hash_git_files.py b/.github/workflows/hash_git_files.py index 152c03ca..be7b172c 100644 --- a/.github/workflows/hash_git_files.py +++ b/.github/workflows/hash_git_files.py @@ -96,24 +96,15 @@ def compute_adler32(repo_path: Path, files: list[str], extra_paths: list[Path] | if file_path.name == "uv.lock": checksum = zlib.adler32(get_uv_lock_bytes_for_hashing(file_path), checksum) else: - with file_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + # Normalize CRLF to LF so the checksum is identical on Windows (autocrlf) and Linux CI checkouts. + checksum = zlib.adler32(file_path.read_bytes().replace(b"\r\n", b"\n"), checksum) except IsADirectoryError: # Ignore symlinks that on windows sometimes get confused as being directories continue for extra_path in sorted(extra_paths or []): checksum = zlib.adler32(str(extra_path).encode("utf-8"), checksum) - with extra_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + checksum = zlib.adler32(extra_path.read_bytes().replace(b"\r\n", b"\n"), checksum) return checksum diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c66af060..7de32702 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -90,7 +90,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| .*biome.jsonc| .*/generated/open[-_]api/.*| @@ -109,7 +108,6 @@ repos: .*generated/graphql.ts| .*/generated/open[-_]api/.*| .*/schema.graphql| - .*pyrightconfig\.json| )$ - repo: https://github.com/rbubley/mirrors-prettier @@ -123,7 +121,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| .*/__snapshots__/.*| .*.min.js| @@ -167,7 +164,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ - id: check-yaml @@ -198,7 +194,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ @@ -235,6 +230,9 @@ repos: hooks: - id: hadolint-docker name: Lint Dockerfiles + # The hadolint pre-commit has a problem where it just always pulls the `latest` image at runtime. + # Explicitly pinning the image here to the same rev as the hook to patch it until https://github.com/hadolint/hadolint/pull/1177 gets resolved. + entry: ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint exclude: .*\.jinja$ description: Runs hadolint to lint Dockerfiles @@ -285,26 +283,6 @@ repos: - --rcfile=./.config/pylintrc.toml verbose: true - - repo: local - hooks: - - id: pyright - name: pyright - entry: bash -c "uv run pyright" - files: '.+\.py$' - exclude: | - (?x)^( - .*/generated/graphql/.*| - .*/generated/open[-_]api/.*| - )$ - # don't pass filenames else the command line sees them twice - pass_filenames: false - language: system - types: [python] - # use require_serial so that script is only called once per commit - require_serial: true - # print the number of files as a sanity-check - verbose: true - - repo: local hooks: - id: pyrefly diff --git a/AGENTS.md b/AGENTS.md index f6f733d5..819663c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,17 +9,22 @@ This project is a Copier template used to generate other copier templates. It is - 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. +- Never manually start services prior to running E2E tests. The test harness boots and tears down its own services (backend, frontend, supporting services) via session fixtures. - When iterating on a single test, run that test in isolation first and confirm it is in the expected state (red or green) before widening to the full suite. Use the most targeted invocation available: a specific test function for Python (e.g. `uv run pytest path/to/test.py::test_name --no-cov`) or a file path and name filter for TypeScript (e.g. `pnpm test-unit path/to/test.spec.ts -t "test name" --no-coverage`). Only run the full suite once the target test behaves as expected. - Test coverage requirements are usually at 100%, so when running a subset of tests, always disable test coverage to avoid the test run failing for insufficient coverage. - 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) @@ -46,7 +51,7 @@ This project is a Copier template used to generate other copier templates. It is ### Python Testing -- Keep every behavioral `assert` inside the test function body — never factor assertions into helper functions or fixtures. pytest's assertion rewriting (the rich failure diff) only applies to asserts located in the test module (or conftest/registered plugins), so an `assert` hidden in an ordinary helper reports an opaque failure with no useful diff. +- Do not apply the keyword-only parameter rule (`*`) to test functions or fixtures — pytest injects its parameters, so `*` has no effect. - When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` - Before writing new mock/spy helpers, check the `tests/unit/` folder for pre-built helpers in files like `fixtures.py` or `*mocks.py` - When a test needs a fixture only for its side effects (not its return value), use `@pytest.mark.usefixtures(fixture_name.__name__)` instead of adding an unused parameter with a noqa comment @@ -77,7 +82,7 @@ This project is a Copier template used to generate other copier templates. It is - ❌ 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/copier_template_resources/vcrpy_fixtures.py b/copier_template_resources/vcrpy_fixtures.py index ec014430..901ad92a 100644 --- a/copier_template_resources/vcrpy_fixtures.py +++ b/copier_template_resources/vcrpy_fixtures.py @@ -10,13 +10,12 @@ import os import traceback from typing import Any -from typing import cast import pytest from vcr import VCR from vcr import matchers as _vcr_matchers from vcr.request import Request as VCRRequest -from vcr.util import read_body as _vcr_read_body # pyright: ignore[reportUnknownVariableType] # vcrpy is untyped +from vcr.util import read_body as _vcr_read_body logger = logging.getLogger(__name__) @@ -48,17 +47,17 @@ def vcr_config() -> dict[str, list[str]]: def _read_body_as_str(request: VCRRequest) -> str: - raw = _vcr_read_body(request) # pyright: ignore[reportUnknownVariableType] # vcrpy is untyped + raw = _vcr_read_body(request) if raw is None: return "" if isinstance(raw, bytes): return raw.decode("utf-8") - return str(raw) # pyright: ignore[reportUnknownArgumentType] # vcrpy is untyped + return str(raw) def _logging_body_matcher(r1: VCRRequest, r2: VCRRequest) -> None: try: - _vcr_matchers.body(r1, r2) # pyright: ignore[reportUnknownMemberType] # vcrpy is untyped + _vcr_matchers.body(r1, r2) except AssertionError as err: tb_frames = traceback.extract_tb(err.__traceback__) if ( @@ -88,12 +87,7 @@ def pytest_recording_configure( config: pytest.Config, # noqa: ARG001 # the config argument MUST be present (even when unused) or pytest-recording throws an error vcr: VCR, ): - vcr.match_on = cast(tuple[str, ...], vcr.match_on) # pyright: ignore[reportUnknownMemberType] # I know vcr.match_on is unknown, that's why I'm casting and isinstance-ing it...not sure if there's a different approach pyright prefers - assert isinstance(vcr.match_on, tuple), ( - f"vcr.match_on is not a tuple, it is a {type(vcr.match_on)} with value {vcr.match_on}" - ) - - vcr.register_matcher("logging_body", _logging_body_matcher) # pyright: ignore[reportUnknownMemberType] # vcrpy is not fully typed + vcr.register_matcher("logging_body", _logging_body_matcher) vcr.match_on += ("logging_body",) # body is not included by default, but it seems relevant def before_record_response(response: dict[str, str | dict[str, Any]]) -> dict[str, str | dict[str, Any]]: diff --git a/extensions/context.py b/extensions/context.py index e18a194e..7d64a608 100644 --- a/extensions/context.py +++ b/extensions/context.py @@ -19,16 +19,15 @@ class ContextUpdater(ContextHook): def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just creating the dict self, context: dict[Any, Any] ) -> dict[Any, Any]: - context["uv_version"] = "0.11.25" - context["pnpm_version"] = "11.9.0" + context["uv_version"] = "0.12.1" + context["pnpm_version"] = "11.20.0" context["npm_version"] = "11.13.0" context["nvm_version"] = "0.40.5" context["pre_commit_version"] = "4.5.1" - context["pyright_version"] = ">=1.1.411" context["pytest_version"] = ">=9.1.1" context["pytest_randomly_version"] = ">=4.1.0" context["pytest_cov_version"] = ">=7.1.0" - context["copier_version"] = "==9.16.0" + context["copier_version"] = "==9.17.0" context["copier_template_extensions_version"] = "==0.3.3" context["sphinx_version"] = "9.0.4" context["pulumi_version"] = ">=3.245.0" @@ -61,12 +60,13 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["pytest_asyncio_version"] = ">=1.4.0" context["pytest_timeout_version"] = ">=2.4.0" context["pytest_reserial_version"] = ">=0.6.1" - context["python_faker_version"] = ">=40.28.1" - context["mutmut_version"] = ">=3.6.0" - context["pyrefly_version"] = ">=1.1.1" + context["python_faker_version"] = ">=40.36.0" + context["mutmut_version"] = ">=3.7.0" + context["pyrefly_version"] = ">=1.2.0" + context["vacuum_openapi_version"] = "0.30.0" context["default_node_version"] = "24.11.1" - context["nuxt_ui_version"] = "^4.8.1" + context["nuxt_ui_version"] = "^4.10.0" context["nuxt_version"] = "^4.4.6" context["nuxt_icon_version"] = "^2.2.1" context["typescript_version"] = "^6.0.2" @@ -96,6 +96,10 @@ def hook( # noqa: PLR0915 # yes, this is a lot of statements, but it's all just context["vue_eslint_parser_version"] = "^10.4.0" context["happy_dom_version"] = "^20.10.1" context["node_kiota_bundle_version"] = "1.0.0-preview.103" + context["labsync_nuxt_common_version"] = "^0.2.3" + context["tanstack_vue_table_version"] = "^8.21.3" + context["unplugin_auto_import_version"] = "^21.0.0" + context["openapi_types_version"] = "^12.1.3" context["gha_checkout"] = "v6.0.2" context["gha_setup_python"] = "v6.2.0" diff --git a/pyproject.toml b/pyproject.toml index 7ce01727..2893f9c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,10 +19,9 @@ dependencies = [ "pytest>=9.1.1", "pytest-cov>=7.1.0", "pytest-randomly>=4.1.0", - "faker>=40.28.1", - "pyright[nodejs]>=1.1.411", - "pyrefly>=1.1.1", - "copier==9.16.0", + "faker>=40.36.0", + "pyrefly>=1.2.0", + "copier==9.17.0", "copier-template-extensions==0.3.3" ] diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index d50a68ab..00000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "exclude": [ - // excludes managed by this repository - - // excludes managed by template - "**/node_modules", - "**/.precommit_cache", - "**/.ruff_cache", - "**/.npm_cache", - "**/.pipx_cache", - "**/__pycache__", - "**/vendor_files", - "**/generated/graphql", - "**/generated/open_api", - "**/.venv", - "**/venv", - "**/copier_template_resources", - "**/mutants", - "**/.claude/plugins" - ], - "strictListInference": true, - "strictDictionaryInference": true, - "strictSetInference": true, - "analyzeUnannotatedFunctions": true, - "strictParameterNoneValue": true, - "enableTypeIgnoreComments": true, - "deprecateTypingAliases": true, - "reportGeneralTypeIssues": true, - "reportPropertyTypeMismatch": true, - "reportFunctionMemberAccess": true, - "reportMissingImports": true, - "reportMissingModuleSource": true, - "reportInvalidTypeForm": true, - "reportMissingTypeStubs": false, - "reportImportCycles": true, - "reportUnusedClass": true, - "reportUnusedFunction": true, - "reportUnusedVariable": false, // already covered by ruff rule F841 - "reportWildcardImportFromLibrary": true, - "reportAbstractUsage": true, - "reportArgumentType": true, - "reportAssertTypeFailure": true, - "reportAssignmentType": true, - "reportAttributeAccessIssue": true, - "reportCallIssue": true, - "reportInconsistentOverload": true, - "reportIndexIssue": true, - "reportInvalidTypeArguments": true, - "reportNoOverloadImplementation": true, - "reportOperatorIssue": true, - "reportOptionalSubscript": true, - "reportOptionalMemberAccess": true, - "reportOptionalCall": true, - "reportOptionalIterable": true, - "reportOptionalContextManager": true, - "reportOptionalOperand": true, - "reportRedeclaration": true, - "reportReturnType": true, - "reportTypedDictNotRequiredAccess": true, - "reportUntypedFunctionDecorator": true, - "reportUntypedClassDecorator": true, - "reportUntypedBaseClass": true, - "reportUntypedNamedTuple": true, - "reportPrivateUsage": false, // already covered by ruff rule SLF001 - "reportTypeCommentUsage": true, - "reportPrivateImportUsage": true, - "reportConstantRedefinition": true, - "reportDeprecated": true, - "reportIncompatibleMethodOverride": true, - "reportIncompatibleVariableOverride": true, - "reportInconsistentConstructor": true, - "reportOverlappingOverload": true, - "reportPossiblyUnboundVariable": true, - "reportMissingSuperCall": true, - "reportUninitializedInstanceVariable": true, - "reportInvalidStringEscapeSequence": true, - "reportUnknownParameterType": true, - "reportUnknownArgumentType": true, - "reportUnknownLambdaType": true, - "reportUnknownVariableType": true, - "reportUnknownMemberType": true, - "reportMissingParameterType": true, - "reportMissingTypeArgument": true, - "reportInvalidTypeVarUse": true, - "reportCallInDefaultInitializer": true, - "reportUnnecessaryIsInstance": true, - "reportUnnecessaryCast": true, - "reportUnnecessaryComparison": true, - "reportUnnecessaryContains": true, - "reportAssertAlwaysTrue": true, - "reportSelfClsParameterName": true, - "reportImplicitStringConcatenation": true, - "reportUndefinedVariable": true, - "reportUnboundVariable": true, - "reportInvalidStubStatement": true, - "reportIncompleteStub": true, - "reportUnsupportedDunderAll": true, - "reportUnusedCallResult": true, - "reportUnusedCoroutine": true, - "reportUnusedExcept": true, - "reportUnusedExpression": true, - "reportUnnecessaryTypeIgnoreComment": true, - "reportMatchNotExhaustive": true, - "reportImplicitOverride": true, - "pythonPlatform": "Linux", - "executionEnvironments": [ - { - "root": "tests", - // reportUninitializedInstanceVariable doesn't make any sense for test cases. See https://github.com/microsoft/pyright/discussions/4834#discussioncomment-8496931 - "reportUninitializedInstanceVariable": false - } - ] -} diff --git a/template/.claude/hooks/bash-chain-guard.js b/template/.claude/hooks/bash-chain-guard.js index 10a9d04c..0e13f9a2 100644 --- a/template/.claude/hooks/bash-chain-guard.js +++ b/template/.claude/hooks/bash-chain-guard.js @@ -1,7 +1,19 @@ #!/usr/bin/env node // PreToolUse hook: blocks command chaining in Bash tool calls -// Detects &&/|| which violate AGENTS.md: "execute exactly one command per tool call" +// Enforces AGENTS.md: "execute exactly one command per tool call" +// Quoted spans and backslash-escapes are replaced with a space before scanning, so a `;` inside a +// string literal (or the trailing `\;` of `find -exec`) is not mistaken for a chain. Replacing with +// a space rather than deleting keeps tokens separate, so `printf hi >\x&` cannot collapse to +// `printf hi >&` and hide the backgrounding `&` behind the redirection lookbehind. // Once https://github.com/anthropics/claude-code/issues/16561 is resolved, this should no longer be necessary +// Improvements based on https://github.com/iandunn/dotfiles/blob/main/claude/hooks/block-chained-commands.js + +const CHAIN_OPERATORS = /&&|\|\||;/; // a single `|` is a pipe, which is allowed +const BACKGROUND_AMPERSAND = /(?])&(?!>)/; // a lone `&` backgrounds the command, but `2>&1` and `&>file` are redirections + +function stripQuotedAndEscaped(command) { + return command.replace(/\\.|'[^']*'|"(?:\\.|[^"\\])*"/g, " "); +} let input = ""; process.stdin.on("data", (chunk) => { @@ -11,15 +23,24 @@ process.stdin.on("end", () => { try { const data = JSON.parse(input); const command = (data.tool_input?.command || "").trim(); + const scannable = stripQuotedAndEscaped(command); - if (/\s(&&|\|\||;)\s|\s&(\s|$)/.test(command)) { + if (CHAIN_OPERATORS.test(scannable)) { process.stderr.write( - "AGENTS.md violation: never chain commands with &&, ||, ;, or &. " + + "AGENTS.md violation: never chain commands with &&, ||, or ;. " + "Run one command per tool call. " + "Use cd as a separate prior tool call instead of cd && ...", ); process.exit(2); } + + if (BACKGROUND_AMPERSAND.test(scannable)) { + process.stderr.write( + "AGENTS.md violation: never background a command with &. " + + "Use the Bash tool's run_in_background option instead.", + ); + process.exit(2); + } } catch (e) { // Silent fail — never block on hook errors } diff --git a/template/.claude/settings/models.jsonc b/template/.claude/settings/models.jsonc new file mode 100644 index 00000000..d8918e14 --- /dev/null +++ b/template/.claude/settings/models.jsonc @@ -0,0 +1,20 @@ +/* + * ============== WARNING ============================================================================== + * File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git + * See .config/.copier-managed-files.json for details. + * + * You are welcome to make changes to this file in your repo if they are custom to your project, + * but if the change should be shared with other projects, please backport it to the template repo. + * ===================================================================================================== + */ +{ + "availableModels": [ + "sonnet", + "opus", + "haiku", + "claude-opus-5", + "claude-opus-4-8", + "claude-sonnet-5", + "claude-sonnet-4-6" + ] +} diff --git a/template/.claude/settings/permissions/bash.jsonc b/template/.claude/settings/permissions/bash.jsonc index 4d477c6e..df881f30 100644 --- a/template/.claude/settings/permissions/bash.jsonc +++ b/template/.claude/settings/permissions/bash.jsonc @@ -110,6 +110,7 @@ "Bash(just *)", "Bash(lsof *)", "Bash(test *)", + "Bash(xxd)", // this invocation can be used to read, but not to write (so no " *" after it) "Bash(zk *)", // Node.js "Bash(pnpm list *)", diff --git a/template/.claude/skills/address-pr-comments/fetch-pr-comments.py b/template/.claude/skills/address-pr-comments/fetch-pr-comments.py index c88b27af..283a46a4 100755 --- a/template/.claude/skills/address-pr-comments/fetch-pr-comments.py +++ b/template/.claude/skills/address-pr-comments/fetch-pr-comments.py @@ -70,6 +70,7 @@ def gh_rest(*args: str) -> Any: # noqa: ANN401 — return type is genuinely unk ["gh", *call_args], # noqa: S607 — gh is expected on PATH capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) @@ -91,6 +92,7 @@ def gh_graphql(query: str, variables: dict[str, str | int]) -> Any: # noqa: ANN args, capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) @@ -105,6 +107,7 @@ def fetch_current_user_login() -> str: ["gh", "api", "user", "--jq", ".login"], # noqa: S607 — gh is expected on PATH capture_output=True, text=True, + encoding="utf-8", check=True, timeout=GH_TIMEOUT_SECONDS, ) diff --git a/template/.coderabbit.yaml b/template/.coderabbit.yaml index f7ab5a0c..addc595c 100644 --- a/template/.coderabbit.yaml +++ b/template/.coderabbit.yaml @@ -13,7 +13,7 @@ reviews: - path: "**/vendor_files/**" instructions: "These files came from a vendor and we're not allowed to change them. Refer to it if you need to understand how the main code interacts with it, but do not make comments about it." - path: "**/*.py" - instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyright prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." + instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyrefly prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that." - path: "**/.copier-answers.yml" instructions: "Do not comment about the `_commit` value needing to be a clean release tag. A CI job will fail if that is not the case." - path: ".config/.copier-managed-files.json" diff --git a/template/.config/.coveragerc b/template/.config/.coveragerc index 86e870c8..c45b0e39 100644 --- a/template/.config/.coveragerc +++ b/template/.config/.coveragerc @@ -16,7 +16,7 @@ exclude_also = raise NotImplementedError raise AssertionError - # Don't complain about code specifically to help with pyright import resolution + # Don't complain about code specifically to help with pyrefly import resolution if TYPE_CHECKING: # Don't complain if non-runnable code isn't run: diff --git a/template/.config/pyrefly.toml b/template/.config/pyrefly.toml index 56043074..17aa02d6 100644 --- a/template/.config/pyrefly.toml +++ b/template/.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/template/.config/ruff.toml b/template/.config/ruff.toml index 12331cc1..db5ea4bc 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 93091070..4ffdca55 100644 --- a/template/.devcontainer/devcontainer.json.jinja +++ b/template/.devcontainer/devcontainer.json.jinja @@ -34,16 +34,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.21.0", + "coderabbit.coderabbit-vscode@0.21.3", "ms-vscode.live-server@0.5.2025051301", "MS-vsliveshare.vsliveshare@1.0.5905",{% endraw %}{% if install_claude_cli %}{% 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 %} + "anthropic.claude-code@2.1.220",{% 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.9002", + "meta.pyrefly@1.1.9003", "ms-vscode-remote.remote-containers@0.414.0", - "charliermarsh.ruff@2026.60.0",{% endraw %}{% endif %}{% raw %} + "charliermarsh.ruff@2026.68.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", @@ -74,7 +74,7 @@ // TODO: see if there's a way to specify different configurations for different folders "ruff.configuration": "/workspaces/{% endraw %}{{ repo_name }}{% raw %}/.config/ruff-non-src.toml", // use the non-src configuration since it's less restrictive and won't show false positives and underline things "python.pyrefly.configPath": "/workspaces/{% endraw %}{{ repo_name }}{% raw %}/.config/pyrefly.toml",{% endraw %}{% endif %}{% raw %} - "prettier.configPath": ".config/.prettierrc", + "prettier.configPath": "/workspaces/{% endraw %}{{ repo_name }}{% raw %}/.config/.prettierrc", "[jsonc][json][javascript][typescript][graphql][css][scss][html][vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true diff --git a/template/.github/workflows/hash_git_files.py b/template/.github/workflows/hash_git_files.py index 152c03ca..be7b172c 100644 --- a/template/.github/workflows/hash_git_files.py +++ b/template/.github/workflows/hash_git_files.py @@ -96,24 +96,15 @@ def compute_adler32(repo_path: Path, files: list[str], extra_paths: list[Path] | if file_path.name == "uv.lock": checksum = zlib.adler32(get_uv_lock_bytes_for_hashing(file_path), checksum) else: - with file_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + # Normalize CRLF to LF so the checksum is identical on Windows (autocrlf) and Linux CI checkouts. + checksum = zlib.adler32(file_path.read_bytes().replace(b"\r\n", b"\n"), checksum) except IsADirectoryError: # Ignore symlinks that on windows sometimes get confused as being directories continue for extra_path in sorted(extra_paths or []): checksum = zlib.adler32(str(extra_path).encode("utf-8"), checksum) - with extra_path.open("rb") as f: - while True: - chunk = f.read(4096) - if not chunk: - break - checksum = zlib.adler32(chunk, checksum) + checksum = zlib.adler32(extra_path.read_bytes().replace(b"\r\n", b"\n"), checksum) return checksum diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml index c66af060..7de32702 100644 --- a/template/.pre-commit-config.yaml +++ b/template/.pre-commit-config.yaml @@ -90,7 +90,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| .*biome.jsonc| .*/generated/open[-_]api/.*| @@ -109,7 +108,6 @@ repos: .*generated/graphql.ts| .*/generated/open[-_]api/.*| .*/schema.graphql| - .*pyrightconfig\.json| )$ - repo: https://github.com/rbubley/mirrors-prettier @@ -123,7 +121,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| .*/__snapshots__/.*| .*.min.js| @@ -167,7 +164,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ - id: check-yaml @@ -198,7 +194,6 @@ repos: .devcontainer/devcontainer.json| .devcontainer/devcontainer-lock.json| .*devcontainer.json| - .*pyrightconfig.json| .*tsconfig.json| )$ @@ -235,6 +230,9 @@ repos: hooks: - id: hadolint-docker name: Lint Dockerfiles + # The hadolint pre-commit has a problem where it just always pulls the `latest` image at runtime. + # Explicitly pinning the image here to the same rev as the hook to patch it until https://github.com/hadolint/hadolint/pull/1177 gets resolved. + entry: ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint exclude: .*\.jinja$ description: Runs hadolint to lint Dockerfiles @@ -285,26 +283,6 @@ repos: - --rcfile=./.config/pylintrc.toml verbose: true - - repo: local - hooks: - - id: pyright - name: pyright - entry: bash -c "uv run pyright" - files: '.+\.py$' - exclude: | - (?x)^( - .*/generated/graphql/.*| - .*/generated/open[-_]api/.*| - )$ - # don't pass filenames else the command line sees them twice - pass_filenames: false - language: system - types: [python] - # use require_serial so that script is only called once per commit - require_serial: true - # print the number of files as a sanity-check - verbose: true - - repo: local hooks: - id: pyrefly diff --git a/template/AGENTS.md b/template/AGENTS.md index 490b4969..885ef042 100644 --- a/template/AGENTS.md +++ b/template/AGENTS.md @@ -9,17 +9,22 @@ This project is a Python library. - 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. +- Never manually start services prior to running E2E tests. The test harness boots and tears down its own services (backend, frontend, supporting services) via session fixtures. - When iterating on a single test, run that test in isolation first and confirm it is in the expected state (red or green) before widening to the full suite. Use the most targeted invocation available: a specific test function for Python (e.g. `uv run pytest path/to/test.py::test_name --no-cov`) or a file path and name filter for TypeScript (e.g. `pnpm test-unit path/to/test.spec.ts -t "test name" --no-coverage`). Only run the full suite once the target test behaves as expected. - Test coverage requirements are usually at 100%, so when running a subset of tests, always disable test coverage to avoid the test run failing for insufficient coverage. - 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) @@ -46,7 +51,7 @@ This project is a Python library. ### Python Testing -- Keep every behavioral `assert` inside the test function body — never factor assertions into helper functions or fixtures. pytest's assertion rewriting (the rich failure diff) only applies to asserts located in the test module (or conftest/registered plugins), so an `assert` hidden in an ordinary helper reports an opaque failure with no useful diff. +- Do not apply the keyword-only parameter rule (`*`) to test functions or fixtures — pytest injects its parameters, so `*` has no effect. - When using `mocker.spy` on a class-level method (including inherited ones), the spy records the unbound call, so assertions need `ANY` as the first argument to match self: `spy.assert_called_once_with(ANY, expected_arg)` - Before writing new mock/spy helpers, check the `tests/unit/` folder for pre-built helpers in files like `fixtures.py` or `*mocks.py` - When a test needs a fixture only for its side effects (not its return value), use `@pytest.mark.usefixtures(fixture_name.__name__)` instead of adding an unused parameter with a noqa comment @@ -77,7 +82,7 @@ This project is a Python library. - ❌ 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/template/pyrightconfig.json b/template/pyrightconfig.json deleted file mode 100644 index d50a68ab..00000000 --- a/template/pyrightconfig.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "exclude": [ - // excludes managed by this repository - - // excludes managed by template - "**/node_modules", - "**/.precommit_cache", - "**/.ruff_cache", - "**/.npm_cache", - "**/.pipx_cache", - "**/__pycache__", - "**/vendor_files", - "**/generated/graphql", - "**/generated/open_api", - "**/.venv", - "**/venv", - "**/copier_template_resources", - "**/mutants", - "**/.claude/plugins" - ], - "strictListInference": true, - "strictDictionaryInference": true, - "strictSetInference": true, - "analyzeUnannotatedFunctions": true, - "strictParameterNoneValue": true, - "enableTypeIgnoreComments": true, - "deprecateTypingAliases": true, - "reportGeneralTypeIssues": true, - "reportPropertyTypeMismatch": true, - "reportFunctionMemberAccess": true, - "reportMissingImports": true, - "reportMissingModuleSource": true, - "reportInvalidTypeForm": true, - "reportMissingTypeStubs": false, - "reportImportCycles": true, - "reportUnusedClass": true, - "reportUnusedFunction": true, - "reportUnusedVariable": false, // already covered by ruff rule F841 - "reportWildcardImportFromLibrary": true, - "reportAbstractUsage": true, - "reportArgumentType": true, - "reportAssertTypeFailure": true, - "reportAssignmentType": true, - "reportAttributeAccessIssue": true, - "reportCallIssue": true, - "reportInconsistentOverload": true, - "reportIndexIssue": true, - "reportInvalidTypeArguments": true, - "reportNoOverloadImplementation": true, - "reportOperatorIssue": true, - "reportOptionalSubscript": true, - "reportOptionalMemberAccess": true, - "reportOptionalCall": true, - "reportOptionalIterable": true, - "reportOptionalContextManager": true, - "reportOptionalOperand": true, - "reportRedeclaration": true, - "reportReturnType": true, - "reportTypedDictNotRequiredAccess": true, - "reportUntypedFunctionDecorator": true, - "reportUntypedClassDecorator": true, - "reportUntypedBaseClass": true, - "reportUntypedNamedTuple": true, - "reportPrivateUsage": false, // already covered by ruff rule SLF001 - "reportTypeCommentUsage": true, - "reportPrivateImportUsage": true, - "reportConstantRedefinition": true, - "reportDeprecated": true, - "reportIncompatibleMethodOverride": true, - "reportIncompatibleVariableOverride": true, - "reportInconsistentConstructor": true, - "reportOverlappingOverload": true, - "reportPossiblyUnboundVariable": true, - "reportMissingSuperCall": true, - "reportUninitializedInstanceVariable": true, - "reportInvalidStringEscapeSequence": true, - "reportUnknownParameterType": true, - "reportUnknownArgumentType": true, - "reportUnknownLambdaType": true, - "reportUnknownVariableType": true, - "reportUnknownMemberType": true, - "reportMissingParameterType": true, - "reportMissingTypeArgument": true, - "reportInvalidTypeVarUse": true, - "reportCallInDefaultInitializer": true, - "reportUnnecessaryIsInstance": true, - "reportUnnecessaryCast": true, - "reportUnnecessaryComparison": true, - "reportUnnecessaryContains": true, - "reportAssertAlwaysTrue": true, - "reportSelfClsParameterName": true, - "reportImplicitStringConcatenation": true, - "reportUndefinedVariable": true, - "reportUnboundVariable": true, - "reportInvalidStubStatement": true, - "reportIncompleteStub": true, - "reportUnsupportedDunderAll": true, - "reportUnusedCallResult": true, - "reportUnusedCoroutine": true, - "reportUnusedExcept": true, - "reportUnusedExpression": true, - "reportUnnecessaryTypeIgnoreComment": true, - "reportMatchNotExhaustive": true, - "reportImplicitOverride": true, - "pythonPlatform": "Linux", - "executionEnvironments": [ - { - "root": "tests", - // reportUninitializedInstanceVariable doesn't make any sense for test cases. See https://github.com/microsoft/pyright/discussions/4834#discussioncomment-8496931 - "reportUninitializedInstanceVariable": false - } - ] -} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..1e7a3c39 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +# ============== WARNING ============================================================================== +# File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git +# See .config/.copier-managed-files.json for details. +# +# You are welcome to make changes to this file in your repo if they are custom to your project, +# but if the change should be shared with other projects, please backport it to the template repo. +# ===================================================================================================== diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..3fc5cccf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# ============== WARNING ============================================================================== +# File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git +# See .config/.copier-managed-files.json for details. +# +# You are welcome to make changes to this file in your repo if they are custom to your project, +# but if the change should be shared with other projects, please backport it to the template repo. +# ===================================================================================================== +import pytest + +# pytest only rewrites asserts in the modules it collects as tests (plus conftest and registered plugins), so +# an assert reached through fixtures.py/helpers.py reports a bare AssertionError with no diff unless its +# module is registered here, so a new top-level test package needs adding to this list. The subpackages are +# named individually rather than registering `tests`, which is already imported by the time this conftest runs +# and would only warn. +pytest.register_assert_rewrite("tests.unit") diff --git a/uv.lock b/uv.lock index 51ea3941..2099aee6 100644 --- a/uv.lock +++ b/uv.lock @@ -22,7 +22,7 @@ wheels = [ [[package]] name = "copier" -version = "9.16.0" +version = "9.17.0" source = { registry = "https://pypi.org/simple/" } dependencies = [ { name = "colorama" }, @@ -39,9 +39,9 @@ dependencies = [ { name = "pyyaml" }, { name = "questionary" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/c7/3cd18cd539ff41e8e7a95b0146dbace0ba332a4c97f468066b2c2daca2ed/copier-9.16.0.tar.gz", hash = "sha256:4db1a9861d0760f745cc6241f99be37b476f849b8ad700133e2f620b7df92eb2", size = 643997, upload-time = "2026-06-23T17:09:04.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/6a/73ccc2ce53c17a29d650246a625df6342f74cb9d4f15b78a77d311fc6007/copier-9.17.0.tar.gz", hash = "sha256:d966b043a15c74595f7904a6af89f3291135682f8313c4b71ef368811ed554f2", size = 645743, upload-time = "2026-07-13T14:33:04.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/a4/cf8df35b6488c04cae9aa7338c7b531a87aae2515da3993d2906d49b1dc1/copier-9.16.0-py3-none-any.whl", hash = "sha256:fadc55a22db6fb0f99d52878d3df4df3fa9c314dd96b0d81fef785a5e803114b", size = 65483, upload-time = "2026-06-23T17:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/22/2c/68dec88ee26f96af2d4d8ee9d8567313a0826f4fd6bc5ae4527be670d354/copier-9.17.0-py3-none-any.whl", hash = "sha256:fe4e7b59faf4c0e1386eccbb79c6797e6df33ca20d5de49a8372571d8669f2ad", size = 65954, upload-time = "2026-07-13T14:33:03.301Z" }, ] [[package]] @@ -53,7 +53,6 @@ dependencies = [ { name = "copier-template-extensions" }, { name = "faker" }, { name = "pyrefly" }, - { name = "pyright", extra = ["nodejs"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-randomly" }, @@ -61,11 +60,10 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "copier", specifier = "==9.16.0" }, + { name = "copier", specifier = "==9.17.0" }, { name = "copier-template-extensions", specifier = "==0.3.3" }, - { name = "faker", specifier = ">=40.28.1" }, - { name = "pyrefly", specifier = ">=1.1.1" }, - { name = "pyright", extras = ["nodejs"], specifier = ">=1.1.411" }, + { name = "faker", specifier = ">=40.36.0" }, + { name = "pyrefly", specifier = ">=1.2.0" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-randomly", specifier = ">=4.1.0" }, @@ -161,14 +159,14 @@ wheels = [ [[package]] name = "faker" -version = "40.28.1" +version = "40.36.0" source = { registry = "https://pypi.org/simple/" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/2d/3ead106cef42eab305e15f9c089dcc68a40387d5ce04af18585af4ebbb44/faker-40.28.1.tar.gz", hash = "sha256:2a63fb51abab8790636d4030a094cf942404cbccc9c288d1cad70e2e3e9ecd58", size = 2022717, upload-time = "2026-07-01T22:23:43.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/d2/026af1e002bbc6df534d1f8262b18ec79a974f928e9290bfbfdfe7c7b2af/faker-40.36.0.tar.gz", hash = "sha256:754048c76c03afa7de83eee8f4bcee3cf668cbb7d995f54a4e9678db7f110308", size = 2025903, upload-time = "2026-07-24T21:11:33.088Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/a6/6111b9f13c1564b0e2f5dbeeb611fd00dc731e6add2336f62b798598c73d/faker-40.28.1-py3-none-any.whl", hash = "sha256:e8d3f5c469100a553d246dce7937c291308068a5ec6c9c3a228d7878b50720be", size = 2061052, upload-time = "2026-07-01T22:23:41.946Z" }, + { url = "https://files.pythonhosted.org/packages/50/9a/b947ed175ce9a0dcb070ccf3607f0ce8720cfb5ed1a36166a150b2acd5af/faker-40.36.0-py3-none-any.whl", hash = "sha256:82b9497d9cfe017048075bcf969298a74b1b6e39f5e4dad1211085d1133f7b62", size = 2062829, upload-time = "2026-07-24T21:11:31.37Z" }, ] [[package]] @@ -252,30 +250,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[package]] -name = "nodejs-wheel-binaries" -version = "22.12.0" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/a0/e9c4a93a09da08395044219af1adceb8c7e9bfca2b6b3ec35374a265851a/nodejs_wheel_binaries-22.12.0.tar.gz", hash = "sha256:e9b707766498322bb2b79fb4f6f425ef86904feccc72cc426f8d96f75488518e", size = 7869, upload-time = "2024-12-06T23:10:48.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9e/d3208b2f3e8962b3abc3c623f21a8d242a1babe7770137f54b2fe76729ea/nodejs_wheel_binaries-22.12.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ed26eac5a96f3bfcd36f1eb9c397f07392e8b166895e0a65eb6033baefa0217", size = 51050283, upload-time = "2024-12-06T23:10:14.631Z" }, - { url = "https://files.pythonhosted.org/packages/62/61/e90e791462aab3c0dedbdd96bd8973fc61629a96c82ea13a62ffad00d559/nodejs_wheel_binaries-22.12.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:ca853c919e18860eb75ac3563985391d07eb50f6ab0aadcb48842bcc0ee8dd45", size = 51855510, upload-time = "2024-12-06T23:10:19.015Z" }, - { url = "https://files.pythonhosted.org/packages/87/f1/3b35a777b9206fa99d690de24f1c003a1448ff14567d80d8bf2ab9994ec5/nodejs_wheel_binaries-22.12.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6df3f2606e26d3334de1fc15c4e6a833980a02ed2eae528d90ddaacaaab2dd1", size = 57176964, upload-time = "2024-12-06T23:10:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3e/9a925efa56fbbdc6c262e6a5b5e2d8ec3695f840822a1d58b69444e155e1/nodejs_wheel_binaries-22.12.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:335176ffa5f753c4d447af83f70987f5f82becdf84fadf56d5c0036379826cff", size = 57510549, upload-time = "2024-12-06T23:10:31.399Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/2b6d0fdb3a1cc47fc95c3f6d98376844870e2e46881749a65177689ff2fa/nodejs_wheel_binaries-22.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:44f0b372a9ea218be885d14b370e774366d09697ae193b741b83cf04c6753884", size = 59632749, upload-time = "2024-12-06T23:10:36.373Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7e/6ef2d8a0cd18075a669c06d1e07c5790997ac79d57609e97aec0a536b347/nodejs_wheel_binaries-22.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:d1a63fc84b0a5a94b19f428ecc1eb1bf4d09c9fc26390db52231c01c3667fe82", size = 40123839, upload-time = "2024-12-06T23:10:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f8/52c23c74ffe1b5f6f17d69b6193e2cf6a766929934e52409b377214a1b6a/nodejs_wheel_binaries-22.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:cc363752be784bf6496329060c53c4d8a993dca73815bb179b37dba5c6d6db1c", size = 35999369, upload-time = "2024-12-06T23:10:44.063Z" }, -] - [[package]] name = "packaging" version = "24.2" @@ -400,39 +374,21 @@ wheels = [ [[package]] name = "pyrefly" -version = "1.1.1" -source = { registry = "https://pypi.org/simple/" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, - { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, -] - -[[package]] -name = "pyright" -version = "1.1.411" -source = { registry = "https://pypi.org/simple/" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, -] - -[package.optional-dependencies] -nodejs = [ - { name = "nodejs-wheel-binaries" }, +version = "1.2.0" +source = { registry = "https://pypi.org/simple/" } +sdist = { url = "https://files.pythonhosted.org/packages/89/01/a86e9f24722b095c3f88e3616132b75a21b0df53804bdc6a45314dd4d93c/pyrefly-1.2.0.tar.gz", hash = "sha256:5485f960fc2481617068c918335c39ab1507ef90b6b5bd35bf57726e60e73185", size = 6243654, upload-time = "2026-08-01T02:56:27.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/9d/3c0ef1d4843987b22f996ed381ec9cf5a3b1273e29804db276252e4c95eb/pyrefly-1.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7f46d983ac49ddd2b043694960a01dc6a19a5cfd8eec609d6bd9c42866f91b4e", size = 14026305, upload-time = "2026-08-01T02:56:02.611Z" }, + { url = "https://files.pythonhosted.org/packages/0a/06/03bbb78fbea54cdc65b626619f3597d5611aca4fdef11e72a4e8360e7e63/pyrefly-1.2.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:756f669b5555090f5c1a4fef30db1785fabe657764f7e4e6dc88994dfb8ca82d", size = 13463880, upload-time = "2026-08-01T02:56:04.93Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/7d8bc00a38e93bbc9c3e7bd14d305f7948717e667c9bcddeab9dd42fd255/pyrefly-1.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3465812ce5ef4781fb592edbf2724547296f0a3124be115d73c7e8b2401862d", size = 13907329, upload-time = "2026-08-01T02:56:07.104Z" }, + { url = "https://files.pythonhosted.org/packages/be/94/9e08b4bf799d0b8f36b55a2783c7ba5f51730cf0632a85a67b5b5ed876cd/pyrefly-1.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5de7b2ad2bba5c8055181681a84b74143eac2234a48ba5d1b7ed7e7a722b02bd", size = 15039020, upload-time = "2026-08-01T02:56:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/5b/bd/bca5fd0c80f4daf8ee6903a29df9f3de1feb05ff0946b8f35ec8c5096b13/pyrefly-1.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25822ea9505f589ea8a725e4268b475132fb89e038fbf092e446510443ac142a", size = 14986199, upload-time = "2026-08-01T02:56:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/f07087f3d185ad2eced0c56cef89ca5474dfb4ff25f146cd50a861c97553/pyrefly-1.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90efe75e17491ef5d636e10469e9278d7d0256b3b4c5e1f4750069bf3ae0f5d1", size = 14393715, upload-time = "2026-08-01T02:56:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/d3/70/0d142c320e284b9e3ce35e9b1e58b8ce2ee1f578f2a7234bc30e5022b94f/pyrefly-1.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:368aaf7eee4f511ddc0f8e564cf14e01ab2f10b0db9105c6d5b153bf498d07bf", size = 13933008, upload-time = "2026-08-01T02:56:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e8/e84f11b6e1f63fd453ad3654213b9a0f6f4de8cef6b58038eef2d0d5955d/pyrefly-1.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d52d5da7bc65fb7675fbaa80eda879d4f8787c494f04cac21603330d3abbdbbe", size = 14431827, upload-time = "2026-08-01T02:56:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/810d31380f66c75e1c0779a408d3b16117b1b368b57894f6aa66bef21686/pyrefly-1.2.0-py3-none-win32.whl", hash = "sha256:8c90751de8506d938e8f802659c74cf35bd7a0036510ee6c634a38eebb280bfa", size = 13229447, upload-time = "2026-08-01T02:56:20.921Z" }, + { url = "https://files.pythonhosted.org/packages/ed/98/4dafa3c7a1caed2dc8cc708dde09ba27963c7736508f55b626fff3024113/pyrefly-1.2.0-py3-none-win_amd64.whl", hash = "sha256:8a8964c224ccc4882730130955815de21ff443c1ac3f0b90685b19bf63848170", size = 14087387, upload-time = "2026-08-01T02:56:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1c/df3cb0a2e5591660ded7a1836cd2f29dc48c91adb1c0a3a700a96f6d09e1/pyrefly-1.2.0-py3-none-win_arm64.whl", hash = "sha256:3a90bb8df39dfbac74b1f3b2e9d7c526b8f80568884c3944d955023a73ebf61e", size = 13430873, upload-time = "2026-08-01T02:56:25.425Z" }, ] [[package]] From 60f4861a9accd62f540911975f7041068854d574 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Wed, 12 Aug 2026 10:27:17 +0000 Subject: [PATCH 2/2] conftest --- template/pyproject.toml.jinja | 1 - template/tests/__init__.py | 1 + template/tests/conftest.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 120000 template/tests/__init__.py create mode 120000 template/tests/conftest.py diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index 0e27747d..10bb75a3 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -25,7 +25,6 @@ dev = [ # Managed by upstream template - "pyright{% endraw %}{{ pyright_version }}{% raw %}", "pyrefly{% endraw %}{{ pyrefly_version }}{% raw %}", "pytest{% endraw %}{{ pytest_version }}{% raw %}", "pytest-cov{% endraw %}{{ pytest_cov_version }}{% raw %}", diff --git a/template/tests/__init__.py b/template/tests/__init__.py new file mode 120000 index 00000000..f77367df --- /dev/null +++ b/template/tests/__init__.py @@ -0,0 +1 @@ +../../tests/__init__.py \ No newline at end of file diff --git a/template/tests/conftest.py b/template/tests/conftest.py new file mode 120000 index 00000000..8c4990f6 --- /dev/null +++ b/template/tests/conftest.py @@ -0,0 +1 @@ +../../tests/conftest.py \ No newline at end of file