From 16f2a5708d9e3a5415edfebd3eafdc6362659e8c Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 01:22:04 +0000 Subject: [PATCH 01/16] ignore --- ...ht_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index 065f9bdc..804a9d6a 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -5,20 +5,20 @@ import pytest from pydantic import JsonValue from vcr import VCR -ALLOWED_HOSTS = ["testserver"] # Skip recording any requests to our own server - let them run live +IGNORED_HOSTS = ["testserver"] # Skip recording any requests to our own server - let them run live -CUSTOM_ALLOWED_HOSTS: tuple[str, ...] = () +CUSTOM_IGNORED_HOSTS: tuple[str, ...] = (UNREACHABLE_IP_ADDRESS_FOR_BRIDGES,) -ALLOWED_HOSTS.extend(CUSTOM_ALLOWED_HOSTS) +IGNORED_HOSTS.extend(CUSTOM_IGNORED_HOSTS) if ( os.name == "nt" ): # on Windows (in CI), the network calls happen at a lower level socket connection even to our FastAPI test client, and can get automatically blocked. This disables that automatic network guard, which isn't great...but since it's still in place on Linux, any actual problems would hopefully get caught before pushing to CI. - ALLOWED_HOSTS.extend(["127.0.0.1", "localhost", "::1"]) + IGNORED_HOSTS.extend(["127.0.0.1", "localhost", "::1"]) @pytest.fixture(autouse=True) def vcr_config() -> dict[str, list[str]]: - return {"allowed_hosts": ALLOWED_HOSTS, "filter_headers": ["User-Agent"]} + return {"ignore_hosts": IGNORED_HOSTS, "filter_headers": ["User-Agent"]} def pytest_recording_configure( From 0f564e843f32241462501f5d1c78387c0c5c533a Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 01:25:00 +0000 Subject: [PATCH 02/16] oops --- ...plate_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index 804a9d6a..00f24069 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -7,7 +7,7 @@ from vcr import VCR IGNORED_HOSTS = ["testserver"] # Skip recording any requests to our own server - let them run live -CUSTOM_IGNORED_HOSTS: tuple[str, ...] = (UNREACHABLE_IP_ADDRESS_FOR_BRIDGES,) +CUSTOM_IGNORED_HOSTS: tuple[str, ...] = () IGNORED_HOSTS.extend(CUSTOM_IGNORED_HOSTS) if ( From e1dcbc321b66367f794ac13b3cc2e5492b4926e6 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 01:28:59 +0000 Subject: [PATCH 03/16] single-line --- template/.github/workflows/ci.yaml.jinja-base | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/template/.github/workflows/ci.yaml.jinja-base b/template/.github/workflows/ci.yaml.jinja-base index 9432d668..ac256848 100644 --- a/template/.github/workflows/ci.yaml.jinja-base +++ b/template/.github/workflows/ci.yaml.jinja-base @@ -20,7 +20,8 @@ jobs: contents: write # needed for updating dependabot branches pre-commit: - needs: [ get-values ] + needs: + - get-values uses: ./.github/workflows/pre-commit.yaml permissions: contents: write # needed for mutex @@ -29,7 +30,8 @@ jobs: python-version: {% endraw %}{{ python_version }}{% raw %} lint-matrix: - needs: [ pre-commit ] + needs: + - pre-commit strategy: matrix: os: @@ -133,7 +135,9 @@ jobs: required-check: runs-on: {% endraw %}{{ gha_linux_runner }}{% raw %} timeout-minutes: {% endraw %}{{ gha_short_timeout_minutes }}{% raw %} - needs: [ lint-matrix, get-values ] + needs: + - lint-matrix + - get-values permissions: statuses: write # needed for updating status on Dependabot PRs if: always() From 3ee2af81d18301fa8703913654e99ab481583723 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:05:10 +0000 Subject: [PATCH 04/16] question --- copier.yaml | 6 ++++++ tests/copier_data/data1.yaml | 1 + tests/copier_data/data2.yaml | 1 + 3 files changed, 8 insertions(+) diff --git a/copier.yaml b/copier.yaml index 19af399e..47585f52 100644 --- a/copier.yaml +++ b/copier.yaml @@ -72,6 +72,12 @@ template_might_want_to_use_vcrpy: default: no when: "{{ template_uses_python }}" +template_might_want_to_use_python_asyncio: + type: bool + help: Is this template for something that might want to use Python asyncio? + default: no + when: "{{ template_uses_python }}" + _min_copier_version: "9.4" diff --git a/tests/copier_data/data1.yaml b/tests/copier_data/data1.yaml index 7e695e71..1294dfe9 100644 --- a/tests/copier_data/data1.yaml +++ b/tests/copier_data/data1.yaml @@ -12,3 +12,4 @@ template_uses_javascript: false template_uses_vuejs: false template_might_want_to_install_aws_ssm_port_forwarding_plugin: true template_might_want_to_use_vcrpy: false +template_might_want_to_use_python_asyncio: true diff --git a/tests/copier_data/data2.yaml b/tests/copier_data/data2.yaml index 4e92e5f7..3821467e 100644 --- a/tests/copier_data/data2.yaml +++ b/tests/copier_data/data2.yaml @@ -14,3 +14,4 @@ template_uses_javascript: true template_uses_vuejs: true template_might_want_to_install_aws_ssm_port_forwarding_plugin: false template_might_want_to_use_vcrpy: true +template_might_want_to_use_python_asyncio: false From 054a5bdb6ce29f02244b3735f9cd39c85c590c9f Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:06:42 +0000 Subject: [PATCH 05/16] add files --- .../asyncio_fixtures.py | 36 ++++++++++++++++++ .../background_tasks.py | 37 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/asyncio_fixtures.py create mode 100644 template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py diff --git a/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/asyncio_fixtures.py b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/asyncio_fixtures.py new file mode 100644 index 00000000..43bb4405 --- /dev/null +++ b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/asyncio_fixtures.py @@ -0,0 +1,36 @@ +import asyncio + +import pytest +from backend_api.background_tasks import background_task_exceptions +from backend_api.background_tasks import background_tasks_set + + +async def _wait_for_tasks(tasks_list: list[asyncio.Task[None]]): + _, pending = await asyncio.wait(tasks_list, timeout=5.0) + if pending: + raise RuntimeError(f"There are still pending tasks: {pending}") + + +@pytest.fixture(autouse=True) +def fail_on_background_task_errors(): + """Automatically fail tests if ANY background task raises an exception.""" + background_task_exceptions.clear() + + yield + + # Wait for background tasks to complete (using asyncio.run for sync fixture) + if background_tasks_set: + tasks_list = list(background_tasks_set) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + asyncio.run(_wait_for_tasks(tasks_list)) + else: + loop.run_until_complete(_wait_for_tasks(tasks_list)) + + # Fail if any exceptions occurred + if background_task_exceptions: + pytest.fail( + f"Background tasks raised {len(background_task_exceptions)} exception(s):\n" + + "\n\n".join(f"{type(e).__name__}: {e}" for e in background_task_exceptions) + ) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py new file mode 100644 index 00000000..9a84910a --- /dev/null +++ b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py @@ -0,0 +1,37 @@ +import asyncio +import logging +import traceback +from collections import deque +from weakref import WeakKeyDictionary +from weakref import WeakSet + +logger = logging.getLogger(__name__) +background_tasks_set: WeakSet[asyncio.Task[None]] = WeakSet() +background_task_exceptions: deque[Exception] = deque( + maxlen=100 # don't grow infinitely in production +) +# Store creation tracebacks for debugging +_task_creation_tracebacks: WeakKeyDictionary[asyncio.Task[None], str] = WeakKeyDictionary() + + +def _task_done_callback(task: asyncio.Task[None]): + background_tasks_set.discard(task) + task_creation_traceback = _task_creation_tracebacks.pop(task, None) + try: + task.result() + except ( # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so + asyncio.CancelledError + ): + return + except Exception as e: # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so + logger.exception(f"Unhandled exception in background task\nTask was created from:\n{task_creation_traceback}") + background_task_exceptions.append(e) + + +def register_task(task: asyncio.Task[None]) -> None: + # Capture the stack trace at task creation time (excluding this function) + creation_stack = "".join(traceback.format_stack()[:-1]) + _task_creation_tracebacks[task] = creation_stack + + background_tasks_set.add(task) + task.add_done_callback(_task_done_callback) From 1c3a1e6ad591fdf6425fc55c465ed7e77aad6774 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:10:37 +0000 Subject: [PATCH 06/16] add question --- template/copier.yml.jinja-base | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/template/copier.yml.jinja-base b/template/copier.yml.jinja-base index 525cf469..c5288879 100644 --- a/template/copier.yml.jinja-base +++ b/template/copier.yml.jinja-base @@ -46,6 +46,10 @@ install_aws_ssm_port_forwarding_plugin: configure_vcrpy: type: bool help: Should VCRpy be configured for use during unit testing in Python? + default: no{% endraw %}{% endif %}{% if template_might_want_to_use_python_asyncio %}{% raw %} +configure_python_asyncio: + type: bool + help: Will python code be using asyncio? default: no{% endraw %}{% endif %}{% raw %} {% endraw %} python_version: From 2e78e3292198329ce4415e0568f6bb966a40718b Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:12:57 +0000 Subject: [PATCH 07/16] tests --- template/tests/copier_data/data1.yaml.jinja-base | 1 + template/tests/copier_data/data2.yaml.jinja-base | 1 + 2 files changed, 2 insertions(+) diff --git a/template/tests/copier_data/data1.yaml.jinja-base b/template/tests/copier_data/data1.yaml.jinja-base index 0f58e4f0..b09e0f20 100644 --- a/template/tests/copier_data/data1.yaml.jinja-base +++ b/template/tests/copier_data/data1.yaml.jinja-base @@ -8,6 +8,7 @@ ssh_port_number: 12345 use_windows_in_ci: false {% endraw %}{% if template_might_want_to_install_aws_ssm_port_forwarding_plugin %}{% raw %}install_aws_ssm_port_forwarding_plugin: true{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_might_want_to_use_vcrpy %}{% raw %}configure_vcrpy: true{% endraw %}{% endif %}{% raw %} +{% endraw %}{% if template_might_want_to_use_python_asyncio %}{% raw %}configure_python_asyncio: true{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_uses_javascript %}{% raw %} node_version: 22.13.0{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_uses_python %}{% raw %} diff --git a/template/tests/copier_data/data2.yaml.jinja-base b/template/tests/copier_data/data2.yaml.jinja-base index 51712214..c5a93aac 100644 --- a/template/tests/copier_data/data2.yaml.jinja-base +++ b/template/tests/copier_data/data2.yaml.jinja-base @@ -8,6 +8,7 @@ ssh_port_number: 54321 use_windows_in_ci: true {% endraw %}{% if template_might_want_to_install_aws_ssm_port_forwarding_plugin %}{% raw %}install_aws_ssm_port_forwarding_plugin: false{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_might_want_to_use_vcrpy %}{% raw %}configure_vcrpy: false{% endraw %}{% endif %}{% raw %} +{% endraw %}{% if template_might_want_to_use_python_asyncio %}{% raw %}configure_python_asyncio: false{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_uses_javascript %}{% raw %} node_version: 22.14.0{% endraw %}{% endif %}{% raw %} {% endraw %}{% if template_uses_python %}{% raw %} From b37b08ad3cb6bdc2ca410213f4863841c31c21a9 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:53:14 +0000 Subject: [PATCH 08/16] streamline --- .../background_tasks.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py index 9a84910a..815f1721 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py +++ b/template/copier_template_resources/{% if template_might_want_to_use_python_asyncio %}python_asyncio{% endif %}/background_tasks.py @@ -2,7 +2,6 @@ import logging import traceback from collections import deque -from weakref import WeakKeyDictionary from weakref import WeakSet logger = logging.getLogger(__name__) @@ -11,27 +10,32 @@ maxlen=100 # don't grow infinitely in production ) # Store creation tracebacks for debugging -_task_creation_tracebacks: WeakKeyDictionary[asyncio.Task[None], str] = WeakKeyDictionary() +_task_creation_tracebacks: dict[int, str] = {} def _task_done_callback(task: asyncio.Task[None]): + task_id = id(task) background_tasks_set.discard(task) - task_creation_traceback = _task_creation_tracebacks.pop(task, None) try: task.result() except ( # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so asyncio.CancelledError ): + _ = _task_creation_tracebacks.pop(task_id, None) return except Exception as e: # pragma: no cover # hard to unit test this, but it'd be good to think of a way to do so - logger.exception(f"Unhandled exception in background task\nTask was created from:\n{task_creation_traceback}") + creation_tb = _task_creation_tracebacks.pop(task_id, "No traceback available") + logger.exception(f"Unhandled exception in background task\nTask was created from:\n{creation_tb}") background_task_exceptions.append(e) + else: + # Clean up on successful completion + _ = _task_creation_tracebacks.pop(task_id, None) def register_task(task: asyncio.Task[None]) -> None: # Capture the stack trace at task creation time (excluding this function) creation_stack = "".join(traceback.format_stack()[:-1]) - _task_creation_tracebacks[task] = creation_stack + _task_creation_tracebacks[id(task)] = creation_stack background_tasks_set.add(task) task.add_done_callback(_task_done_callback) From 84c8a06e6a21e8e29fa7d8345f7cac2d80bec8ad Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:56:06 +0000 Subject: [PATCH 09/16] vcr --- ...e_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index 00f24069..8c6e72c3 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -18,7 +18,11 @@ if ( @pytest.fixture(autouse=True) def vcr_config() -> dict[str, list[str]]: - return {"ignore_hosts": IGNORED_HOSTS, "filter_headers": ["User-Agent"]} + return { + "ignore_hosts": IGNORED_HOSTS, + "filter_headers": ["User-Agent"], + "allowed_hosts": IGNORED_HOSTS, # without also "allowing" these hosts, we get "network blocked" errors in Windows CI + } def pytest_recording_configure( From e7a5c9ffe8e42b4317fdaebd6c43c2dcb4c3c7f4 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 10:59:55 +0000 Subject: [PATCH 10/16] diff --- .devcontainer/devcontainer.json | 2 +- .pre-commit-config.yaml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3f826879..d21ea083 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -62,5 +62,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): 5b56d8b0 # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): a704e2ea # spellchecker:disable-line } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9e1bbd5..80ce69f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -253,7 +253,7 @@ repos: hooks: - id: ruff name: ruff-src - args: [--fix, --config=./ruff.toml] + args: [--fix, --diff, --config=./ruff.toml] files: src/.+\.py$ exclude: | (?x)^( @@ -263,7 +263,7 @@ repos: )$ - id: ruff name: ruff-tests - args: [--fix, --config=./ruff-test.toml] + args: [--fix, --diff, --config=./ruff-test.toml] files: tests?/.+\.py$ exclude: | (?x)^( @@ -272,6 +272,7 @@ repos: template/.*| )$ - id: ruff-format + args: [--diff] exclude: | (?x)^( .*/generated/graphql/.*| From 74e39082b0144dff46f1a1a61c6519ba04b76988 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:13:56 +0000 Subject: [PATCH 11/16] run --- .devcontainer/devcontainer.json | 2 +- .github/workflows/ci.yaml | 9 ++++++++- .pre-commit-config.yaml | 5 ++--- template/.github/workflows/pre-commit.yaml.jinja-base | 9 ++++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d21ea083..3f826879 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -62,5 +62,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): a704e2ea # spellchecker:disable-line + // Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): 5b56d8b0 # spellchecker:disable-line } diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 75063b00..0fcb50b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -60,7 +60,14 @@ jobs: ${{ matrix.os }}-${{ matrix.python-version }}-build-${{ env.cache-name }}- - name: Run pre-commit - run: pre-commit run -a + run: | + pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi lint-matrix: needs: [ pre-commit ] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 80ce69f4..a9e1bbd5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -253,7 +253,7 @@ repos: hooks: - id: ruff name: ruff-src - args: [--fix, --diff, --config=./ruff.toml] + args: [--fix, --config=./ruff.toml] files: src/.+\.py$ exclude: | (?x)^( @@ -263,7 +263,7 @@ repos: )$ - id: ruff name: ruff-tests - args: [--fix, --diff, --config=./ruff-test.toml] + args: [--fix, --config=./ruff-test.toml] files: tests?/.+\.py$ exclude: | (?x)^( @@ -272,7 +272,6 @@ repos: template/.*| )$ - id: ruff-format - args: [--diff] exclude: | (?x)^( .*/generated/graphql/.*| diff --git a/template/.github/workflows/pre-commit.yaml.jinja-base b/template/.github/workflows/pre-commit.yaml.jinja-base index 1ed8ad96..1a9c838b 100644 --- a/template/.github/workflows/pre-commit.yaml.jinja-base +++ b/template/.github/workflows/pre-commit.yaml.jinja-base @@ -69,4 +69,11 @@ jobs: {% endraw %}{{ gha_linux_runner }}{% raw %}-py${{ inputs.python-version }}-node-${{ inputs.node-version}}-${{ env.cache-name }}- - name: Run pre-commit - run: pre-commit run -a{% endraw %} + run: | + pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi{% endraw %} From 38fff63b6161a27902e0763c1dca02db322a311b Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:14:28 +0000 Subject: [PATCH 12/16] in job --- template/.github/workflows/ci.yaml.jinja-base | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/template/.github/workflows/ci.yaml.jinja-base b/template/.github/workflows/ci.yaml.jinja-base index ac256848..fa901a94 100644 --- a/template/.github/workflows/ci.yaml.jinja-base +++ b/template/.github/workflows/ci.yaml.jinja-base @@ -123,7 +123,13 @@ jobs: - name: Run pre-commit run: | # skip devcontainer context hash because the template instantiation may make it different every time - SKIP=git-dirty,compute-devcontainer-context-hash pre-commit run -a + SKIP=git-dirty,compute-devcontainer-context-hash pre-commit run -a || PRE_COMMIT_EXIT_CODE=$? + if [ -n "$PRE_COMMIT_EXIT_CODE" ]; then + echo "Pre-commit failed with exit code $PRE_COMMIT_EXIT_CODE" + echo "Showing git diff:" + git --no-pager diff + exit $PRE_COMMIT_EXIT_CODE + fi - name: Upload pre-commit log if failure if: ${{ failure() }} From 9d9fd3ea3622a81dc4678c5672ddb257deeb1789 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:18:04 +0000 Subject: [PATCH 13/16] vcr --- ...te_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index 8c6e72c3..ec40d24f 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -6,6 +6,7 @@ from pydantic import JsonValue from vcr import VCR IGNORED_HOSTS = ["testserver"] # Skip recording any requests to our own server - let them run live +ALLOWED_HOSTS = [] CUSTOM_IGNORED_HOSTS: tuple[str, ...] = () @@ -13,7 +14,7 @@ IGNORED_HOSTS.extend(CUSTOM_IGNORED_HOSTS) if ( os.name == "nt" ): # on Windows (in CI), the network calls happen at a lower level socket connection even to our FastAPI test client, and can get automatically blocked. This disables that automatic network guard, which isn't great...but since it's still in place on Linux, any actual problems would hopefully get caught before pushing to CI. - IGNORED_HOSTS.extend(["127.0.0.1", "localhost", "::1"]) + ALLOWED_HOSTS.extend(["127.0.0.1", "localhost", "::1"]) @pytest.fixture(autouse=True) @@ -21,7 +22,7 @@ def vcr_config() -> dict[str, list[str]]: return { "ignore_hosts": IGNORED_HOSTS, "filter_headers": ["User-Agent"], - "allowed_hosts": IGNORED_HOSTS, # without also "allowing" these hosts, we get "network blocked" errors in Windows CI + "allowed_hosts": ALLOWED_HOSTS, # without also "allowing" these hosts, we get "network blocked" errors in Windows CI } From c0454a7ec6273907ecd14c1b1178fbac64ef9e07 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:34:32 +0000 Subject: [PATCH 14/16] switch --- ...plate_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index ec40d24f..eb103bc6 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -27,8 +27,8 @@ def vcr_config() -> dict[str, list[str]]: def pytest_recording_configure( - vcr: VCR, 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), ( From af7da0b45067db11ed57a7f316c01b925b39ddec Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:36:16 +0000 Subject: [PATCH 15/16] rfc --- ...e_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index eb103bc6..991b83a5 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -5,7 +5,11 @@ import pytest from pydantic import JsonValue from vcr import VCR -IGNORED_HOSTS = ["testserver"] # Skip recording any requests to our own server - let them run live +UNREACHABLE_IP_ADDRESS = "192.0.2.1" # RFC 5737 TEST-NET-1 +IGNORED_HOSTS = [ + "testserver", # Skip recording any requests to our own server - let them run live + UNREACHABLE_IP_ADDRESS, # allow this through VCR in order to be able to test network failure handling +] ALLOWED_HOSTS = [] CUSTOM_IGNORED_HOSTS: tuple[str, ...] = () From ca9d214b1d4ff62a1e1319d2f2a0252c21248ee6 Mon Sep 17 00:00:00 2001 From: Eli Fine Date: Fri, 12 Dec 2025 11:45:03 +0000 Subject: [PATCH 16/16] conditional --- ...might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} index 991b83a5..cb33e4ff 100644 --- a/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} +++ b/template/copier_template_resources/{% if template_might_want_to_use_vcrpy %}vcrpy_fixtures.py{% endif %} @@ -10,7 +10,7 @@ IGNORED_HOSTS = [ "testserver", # Skip recording any requests to our own server - let them run live UNREACHABLE_IP_ADDRESS, # allow this through VCR in order to be able to test network failure handling ] -ALLOWED_HOSTS = [] +ALLOWED_HOSTS: list[str] = [] CUSTOM_IGNORED_HOSTS: tuple[str, ...] = () @@ -23,11 +23,13 @@ if ( @pytest.fixture(autouse=True) def vcr_config() -> dict[str, list[str]]: - return { + cfg: dict[str, list[str]] = { "ignore_hosts": IGNORED_HOSTS, "filter_headers": ["User-Agent"], - "allowed_hosts": ALLOWED_HOSTS, # without also "allowing" these hosts, we get "network blocked" errors in Windows CI } + if ALLOWED_HOSTS: + cfg["allowed_hosts"] = ALLOWED_HOSTS + return cfg def pytest_recording_configure(