From c1de7c1aba9256eabca17ff58532ae19f2b57eaf Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:34:30 +0000 Subject: [PATCH 01/11] stub: replace stop_machine v0 impl with RuntimeError stub Updated the stop machine implementation to indicate it's a non-canonical legacy stub and provided a link to the canonical source. --- primitives/stop-machine-v0/stop_machine.py | 87 +++------------------- 1 file changed, 9 insertions(+), 78 deletions(-) diff --git a/primitives/stop-machine-v0/stop_machine.py b/primitives/stop-machine-v0/stop_machine.py index 9737b8d..8678dc2 100644 --- a/primitives/stop-machine-v0/stop_machine.py +++ b/primitives/stop-machine-v0/stop_machine.py @@ -1,80 +1,11 @@ -"""StopMachine -- deterministic finite-state stop controller. +"""NON-CANONICAL STUB — do not use. -States: GREEN -> AMBER -> RED -RED is terminal and absorbing: once entered, no event can leave it. -The transition table is the single source of truth. -There is no implicit behaviour. +Canonical source: https://github.com/LalaSkye/constraint-workshop +Canonical file: stop_machine.py +Pinned commit: 3780882 """ -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import Enum -from typing import Dict, List, Tuple - - -class State(Enum): - GREEN = "GREEN" - AMBER = "AMBER" - RED = "RED" - - -class Event(Enum): - TICK = "TICK" - WARN = "WARN" - STOP = "STOP" - RESET = "RESET" - - -# -- Transition table -------------------------------------------------------- -# Key: (current_state, event) -# Value: next_state -# Every (State, Event) pair is listed. Nothing is implicit. - -TRANSITIONS: Dict[Tuple[State, Event], State] = { - # GREEN - (State.GREEN, Event.TICK): State.GREEN, - (State.GREEN, Event.WARN): State.AMBER, - (State.GREEN, Event.STOP): State.RED, - (State.GREEN, Event.RESET): State.GREEN, - # AMBER - (State.AMBER, Event.TICK): State.AMBER, - (State.AMBER, Event.WARN): State.AMBER, - (State.AMBER, Event.STOP): State.RED, - (State.AMBER, Event.RESET): State.GREEN, - # RED (absorbing) - (State.RED, Event.TICK): State.RED, - (State.RED, Event.WARN): State.RED, - (State.RED, Event.STOP): State.RED, - (State.RED, Event.RESET): State.RED, -} - - -@dataclass -class StopMachine: - """Finite-state stop controller. Deterministic. No side-effects.""" - - _state: State = State.GREEN - _history: List[Tuple[State, Event, State]] = field(default_factory=list) - - def send(self, event: Event) -> State: - """Apply *event*, return the new state. Pure lookup -- no branching.""" - prev = self._state - nxt = TRANSITIONS[(prev, event)] - self._state = nxt - self._history.append((prev, event, nxt)) - return nxt - - @property - def state(self) -> State: - return self._state - - @property - def history(self) -> List[Tuple[State, Event, State]]: - """Immutable copy of transition log.""" - return list(self._history) - - def is_terminal(self) -> bool: - return self._state is State.RED - - def __repr__(self) -> str: - return f"StopMachine(state={self._state.value})" +raise RuntimeError( + "This is a non-canonical legacy stub (v0). " + "The canonical StopMachine lives in constraint-workshop @ commit 3780882. " + "See: https://github.com/LalaSkye/constraint-workshop/blob/main/stop_machine.py" +) From e3e83740cdf32768acfc5b62444f914d28ab2198 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:35:30 +0000 Subject: [PATCH 02/11] test: replace v0 stop_machine tests with stub-raises test Added a test to confirm that importing the v0 stop_machine raises a RuntimeError. --- .../stop-machine-v0/test_stop_machine.py | 94 ++----------------- 1 file changed, 6 insertions(+), 88 deletions(-) diff --git a/primitives/stop-machine-v0/test_stop_machine.py b/primitives/stop-machine-v0/test_stop_machine.py index 1655f5d..3df0bf9 100644 --- a/primitives/stop-machine-v0/test_stop_machine.py +++ b/primitives/stop-machine-v0/test_stop_machine.py @@ -1,91 +1,9 @@ -"""Property tests for StopMachine. - -Three guarantees, tested exhaustively over the finite domain: - 1. Determinism -- same (state, event) always yields same next_state. - 2. Absorption -- RED is terminal; no event can leave it. - 3. Completeness -- every (state, event) pair has an entry. -""" +"""Stub test: confirms the v0 stop_machine raises RuntimeError on import.""" import pytest -from stop_machine import Event, State, StopMachine, TRANSITIONS - -ALL_STATES = list(State) -ALL_EVENTS = list(Event) -ALLOWED = set(ALL_STATES) -SEVERITY = {State.GREEN: 0, State.AMBER: 1, State.RED: 2} - - -# -- Determinism ------------------------------------------------------------- - -def test_determinism_replay_identical_history(): - """Same input sequence -> same output sequence. Always.""" - seq = [Event.TICK, Event.WARN, Event.TICK, Event.STOP, Event.RESET, Event.TICK] - m1 = StopMachine() - m2 = StopMachine() - for ev in seq: - m1.send(ev) - m2.send(ev) - assert m1.state == m2.state - assert m1.history == m2.history - - -@pytest.mark.parametrize("state", ALL_STATES) -@pytest.mark.parametrize("event", ALL_EVENTS) -def test_determinism_table_lookup_same_value_twice(state, event): - """The table returns the same value on every read.""" - a = TRANSITIONS[(state, event)] - b = TRANSITIONS[(state, event)] - assert a is b - - -# -- Absorption (RED is terminal) -------------------------------------------- - -@pytest.mark.parametrize("event", ALL_EVENTS) -def test_absorption_red_to_red_for_each_event(event): - assert TRANSITIONS[(State.RED, event)] is State.RED - - -def test_absorption_enter_red_then_fire_every_event_stays_red(): - sm = StopMachine() - sm.send(Event.STOP) - assert sm.is_terminal() - for event in ALL_EVENTS: - sm.send(event) - assert sm.state is State.RED - assert sm.is_terminal() - - -# -- Completeness ------------------------------------------------------------ - -def test_completeness_every_state_event_pair_exists(): - for s in ALL_STATES: - for e in ALL_EVENTS: - assert (s, e) in TRANSITIONS - - -# -- Exhaustive transition closure ------------------------------------------- - -def test_exhaustive_transition_closure(): - """ - For every (state, event) pair: - - next_state is in {GREEN, AMBER, RED} - - RED is absorbing - - determinism holds - """ - for s in ALL_STATES: - for e in ALL_EVENTS: - a = TRANSITIONS[(s, e)] - b = TRANSITIONS[(s, e)] - assert a in ALLOWED, f"({s}, {e}) -> {a} not in allowed set" - assert a is b # determinism - if s is State.RED: - assert a is State.RED, f"RED escaped via {e} -> {a}" - - -# -- Monotonicity ------------------------------------------------------------ -@pytest.mark.parametrize("state", ALL_STATES) -@pytest.mark.parametrize("event", [Event.WARN, Event.STOP]) -def test_monotonicity_warn_and_stop_never_decrease_severity(state, event): - nxt = TRANSITIONS[(state, event)] - assert SEVERITY[nxt] >= SEVERITY[state] +def test_import_stop_machine_v0_raises(): + """Importing the v0 stub must raise RuntimeError.""" + with pytest.raises(RuntimeError, match="non-canonical legacy stub"): + import importlib + importlib.import_module("stop_machine") From 769662ce0b134566c096d500dc181d3c65ab7e21 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:36:22 +0000 Subject: [PATCH 03/11] docs: mark stop-machine-v0 README as non-canonical legacy Updated README to clarify the non-functional status of the StopMachine implementation and provided links to the canonical source. --- primitives/stop-machine-v0/README.md | 64 +++------------------------- 1 file changed, 6 insertions(+), 58 deletions(-) diff --git a/primitives/stop-machine-v0/README.md b/primitives/stop-machine-v0/README.md index 8f174e9..8f1763d 100644 --- a/primitives/stop-machine-v0/README.md +++ b/primitives/stop-machine-v0/README.md @@ -1,60 +1,8 @@ -# StopMachine +NON-CANONICAL LEGACY (V0). Canonical: [constraint-workshop](https://github.com/LalaSkye/constraint-workshop) @ `3780882`. -Deterministic finite-state stop controller. +This folder contains a **non-functional stub** that raises `RuntimeError` on import. +The canonical `StopMachine` implementation lives in +[constraint-workshop/stop_machine.py](https://github.com/LalaSkye/constraint-workshop/blob/main/stop_machine.py). -``` -Inputs (events) ──▶ [ StopMachine ] ──▶ Output state - - GREEN - │ - ▼ - AMBER - │ - ▼ - RED (terminal, absorbing) -``` - -- **RED is terminal** (cannot be bypassed) -- **Transition table is explicit** (no hidden behaviour) -- **Determinism is tested** (replay stable) - -## Invariants (all tested) - -| Invariant | Meaning | Tested | -|---|---|:--:| -| Determinism | Same state + same event => same next state; replay is stable | ✅ | -| Absorption | RED is terminal: (RED, *) -> RED | ✅ | -| Completeness | Every (State, Event) pair exists in the table | ✅ | -| Monotonicity | WARN and STOP never decrease severity (GREEN < AMBER < RED) | ✅ | - -## Full transition table - -| Current | TICK | WARN | STOP | RESET | -|---|---|---|---|---| -| GREEN | GREEN | AMBER | RED | GREEN | -| AMBER | AMBER | AMBER | RED | GREEN | -| RED | RED | RED | RED | RED | - -**The table is the implementation. There is no branching logic.** - -## Why this matters - -In real systems, "optimisation" often means adding behaviour without tightening failure modes. This primitive does the opposite: it makes **stop-rights** explicit, deterministic, and testable. - -- You can replay decisions and get identical results. -- You can prove terminal behaviour (absorption) rather than hoping it holds. -- You can inspect the entire behavioural surface area in one table. - -## Quickstart - -```bash -cd primitives/stop-machine -pip install pytest -pytest test_stop_machine.py -v -``` - -## Scope - -- No orchestration logic. -- No selection logic. -- No opinions. +Do not modify this folder. Update the canonical source instead. +See [CANONICAL.md](../../CANONICAL.md) for details. From 091b2cf856b11e92c9a696a81c84a349a1c23874 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:37:10 +0000 Subject: [PATCH 04/11] stub: replace authority_gate v0 impl with RuntimeError stub Updated the authority gate implementation to indicate it is a non-canonical legacy stub and provided a reference to the canonical source. --- primitives/authority-gate-v0/gate.py | 54 +++++----------------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/primitives/authority-gate-v0/gate.py b/primitives/authority-gate-v0/gate.py index cc76ad8..c5ac7e5 100644 --- a/primitives/authority-gate-v0/gate.py +++ b/primitives/authority-gate-v0/gate.py @@ -1,47 +1,11 @@ -"""AuthorityGate -- deterministic authority wrapper. +"""NON-CANONICAL STUB — do not use. -Execution requires explicit authority. No implicit permissions. -Authority levels are ordered: NONE < USER_CONFIRMED < OWNER_CONFIRMED < ADMIN_APPROVED. +Canonical source: https://github.com/LalaSkye/constraint-workshop +Canonical file: authority_gate.py +Pinned commit: 70ed2c9 """ -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import IntEnum -from typing import Any, Callable, List - - -class Authority(IntEnum): - NONE = 0 - USER_CONFIRMED = 1 - OWNER_CONFIRMED = 2 - ADMIN_APPROVED = 3 - - -@dataclass(frozen=True) -class Decision: - required: Authority - provided: Authority - allowed: bool - - -@dataclass -class AuthorityGate: - """Deterministic authority gate. No implicit permissions.""" - - required: Authority = Authority.USER_CONFIRMED - _history: List[Decision] = field(default_factory=list) - - def call(self, fn: Callable[..., Any], *args: Any, authority: Authority, **kwargs: Any) -> Any: - """Execute *fn* only if authority >= required. Pure comparison.""" - allowed = authority >= self.required - self._history.append(Decision(self.required, authority, allowed)) - if not allowed: - raise PermissionError(f"authority {authority.name} < required {self.required.name}") - return fn(*args, **kwargs) - - @property - def history(self) -> List[Decision]: - return list(self._history) - - def is_satisfied(self, authority: Authority) -> bool: - return authority >= self.required +raise RuntimeError( + "This is a non-canonical legacy stub (v0). " + "The canonical AuthorityGate lives in constraint-workshop @ commit 70ed2c9. " + "See: https://github.com/LalaSkye/constraint-workshop/blob/main/authority_gate.py" +) From b78ae0c0a870eb04bccecdcb1295df1842c0e99c Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:37:58 +0000 Subject: [PATCH 05/11] test: replace v0 authority_gate tests with stub-raises test --- primitives/authority-gate-v0/test_gate.py | 54 +++-------------------- 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/primitives/authority-gate-v0/test_gate.py b/primitives/authority-gate-v0/test_gate.py index 31bcc86..b5062e3 100644 --- a/primitives/authority-gate-v0/test_gate.py +++ b/primitives/authority-gate-v0/test_gate.py @@ -1,51 +1,9 @@ -# primitives/authority-gate/test_gate.py - +"""Stub test: confirms the v0 authority_gate raises RuntimeError on import.""" import pytest -from gate import Authority, AuthorityGate - -ALL = list(Authority) - - -def add(a, b): - return a + b - - -def test_determinism_replay_history_and_result(): - g1 = AuthorityGate(required=Authority.OWNER_CONFIRMED) - g2 = AuthorityGate(required=Authority.OWNER_CONFIRMED) - seq = [Authority.NONE, Authority.USER_CONFIRMED, Authority.OWNER_CONFIRMED, Authority.ADMIN_APPROVED] - out1, out2 = [], [] - for a in seq: - try: - out1.append(g1.call(add, 1, 2, authority=a)) - except PermissionError: - out1.append("DENY") - try: - out2.append(g2.call(add, 1, 2, authority=a)) - except PermissionError: - out2.append("DENY") - assert out1 == out2 - assert g1.history == g2.history - - -@pytest.mark.parametrize("required", ALL) -@pytest.mark.parametrize("provided", ALL) -def test_monotonicity_authority_required_is_threshold(required, provided): - g = AuthorityGate(required=required) - ok = provided >= required - assert g.is_satisfied(provided) is ok - if ok: - assert g.call(add, 2, 3, authority=provided) == 5 - else: - with pytest.raises(PermissionError): - g.call(add, 2, 3, authority=provided) -def test_history_records_decisions_in_order(): - g = AuthorityGate(required=Authority.USER_CONFIRMED) - with pytest.raises(PermissionError): - g.call(add, 1, 1, authority=Authority.NONE) - assert g.call(add, 1, 1, authority=Authority.USER_CONFIRMED) == 2 - assert len(g.history) == 2 - assert g.history[0].allowed is False - assert g.history[1].allowed is True +def test_import_authority_gate_v0_raises(): + """Importing the v0 stub must raise RuntimeError.""" + with pytest.raises(RuntimeError, match="non-canonical legacy stub"): + import importlib + importlib.import_module("gate") From 5c9446b6be8e7d734bff70a5b2f83b5374afb4ac Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:38:45 +0000 Subject: [PATCH 06/11] docs: mark authority-gate-v0 README as non-canonical legacy Updated README to clarify the non-functional status of the legacy implementation and provided links to the canonical source. --- primitives/authority-gate-v0/README.md | 41 ++++---------------------- 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/primitives/authority-gate-v0/README.md b/primitives/authority-gate-v0/README.md index 9a15192..2aeaa4f 100644 --- a/primitives/authority-gate-v0/README.md +++ b/primitives/authority-gate-v0/README.md @@ -1,37 +1,8 @@ -# AuthorityGate +NON-CANONICAL LEGACY (V0). Canonical: [constraint-workshop](https://github.com/LalaSkye/constraint-workshop) @ `70ed2c9`. -A tiny, deterministic wrapper that makes **execution require explicit authority**. +This folder contains a **non-functional stub** that raises `RuntimeError` on import. +The canonical `AuthorityGate` implementation lives in +[constraint-workshop/authority_gate.py](https://github.com/LalaSkye/constraint-workshop/blob/main/authority_gate.py). -## Invariants (all tested) - -| Invariant | Meaning | Tested | -|---|---|:--:| -| Determinism | Same inputs => same allow/deny + same history | Yes | -| Monotonicity | Higher authority never loses permissions | Yes | -| Auditability | Every call records {required, provided, allowed} | Yes | - -## Authority levels - -Ordered (weak to strong): - -- `NONE` -- `USER_CONFIRMED` -- `OWNER_CONFIRMED` -- `ADMIN_APPROVED` - -## Why this matters - -Most "governance" documents talk about approval, but runtime systems still execute on vibes. -This primitive forces the missing mechanical step: **no explicit authority, no execution**. - -## Quickstart - -```bash -python -m pytest primitives/authority-gate -v -``` - -## Scope - -- No policy engine. -- No orchestration logic. -- No opinions. +Do not modify this folder. Update the canonical source instead. +See [CANONICAL.md](../../CANONICAL.md) for details. From 48ad0faaf3fc51bbfc1c141e69faf45188fbad33 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:39:50 +0000 Subject: [PATCH 07/11] remove: delete v0 demo_stop_machine.py (references stubbed v0 folder) --- examples/demo_stop_machine.py | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 examples/demo_stop_machine.py diff --git a/examples/demo_stop_machine.py b/examples/demo_stop_machine.py deleted file mode 100644 index c3a5690..0000000 --- a/examples/demo_stop_machine.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Demo: shows the full StopMachine lifecycle in ~15 lines.""" -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "primitives" / "stop-machine-v0")) - -from stop_machine import Event, StopMachine # noqa: E402 - -m = StopMachine() -print(m) # StopMachine(state=GREEN) - -m.send(Event.TICK) -print(m) # StopMachine(state=GREEN) - -m.send(Event.WARN) -print(m) # StopMachine(state=AMBER) - -m.send(Event.STOP) -print(m) # StopMachine(state=RED) - -m.send(Event.RESET) # attempt escape -print(m) # StopMachine(state=RED) <- absorbed - -print(f"\nTerminal: {m.is_terminal()}") -print(f"History: {m.history}") From 708e2543199564e5a946e57d5675c9051b648290 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:41:12 +0000 Subject: [PATCH 08/11] remove: delete v0 demo_authority_gate.py (references stubbed v0 folder) --- examples/demo_authority_gate.py | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 examples/demo_authority_gate.py diff --git a/examples/demo_authority_gate.py b/examples/demo_authority_gate.py deleted file mode 100644 index f8b221f..0000000 --- a/examples/demo_authority_gate.py +++ /dev/null @@ -1,17 +0,0 @@ -# examples/demo_authority_gate.py - -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "primitives" / "authority-gate-v0")) - -from gate import Authority, AuthorityGate # noqa: E402 - -g = AuthorityGate(required=Authority.OWNER_CONFIRMED) -print("required:", g.required.name) -for a in [Authority.NONE, Authority.USER_CONFIRMED, Authority.OWNER_CONFIRMED]: - try: - print(a.name, "->", g.call(lambda: "OK", authority=a)) - except PermissionError as e: - print(a.name, "-> DENY", str(e)) -print("history:", g.history) From d28f5e74048bd58e58c8c96da12be6da90320bb7 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:42:12 +0000 Subject: [PATCH 09/11] ci: add drift alarms for v0 stub enforcement Added drift alarm checks for StopMachine and Gate classes in primitives. --- .github/workflows/ci.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e76cc11..2f13eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,35 @@ name: CI - on: push: branches: [main] pull_request: branches: [main] - jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12"] - steps: - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies run: pip install pytest - + - name: Drift alarm - no StopMachine class in primitives + run: | + if grep -rn "class StopMachine" primitives/; then + echo "DRIFT DETECTED: StopMachine class found in primitives/" + exit 1 + fi + - name: Drift alarm - no Gate implementation in primitives + run: | + if grep -rn "class.*Gate" primitives/authority-gate-v0/ primitives/stop-machine-v0/; then + echo "DRIFT DETECTED: Gate class found in v0 folders" + exit 1 + fi - name: Run primitive tests run: python -m pytest primitives -v - name: Run root tests From e49c1ad33de31f406b6155bdc4c2970ce6462656 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:46:08 +0000 Subject: [PATCH 10/11] fix: use file-based import in v0 gate stub test --- primitives/authority-gate-v0/test_gate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/primitives/authority-gate-v0/test_gate.py b/primitives/authority-gate-v0/test_gate.py index b5062e3..ea95db8 100644 --- a/primitives/authority-gate-v0/test_gate.py +++ b/primitives/authority-gate-v0/test_gate.py @@ -1,9 +1,14 @@ """Stub test: confirms the v0 authority_gate raises RuntimeError on import.""" import pytest +import importlib.util +import sys +from pathlib import Path def test_import_authority_gate_v0_raises(): """Importing the v0 stub must raise RuntimeError.""" + stub = Path(__file__).resolve().parent / "gate.py" + spec = importlib.util.spec_from_file_location("gate_v0_stub", stub) + mod = importlib.util.module_from_spec(spec) with pytest.raises(RuntimeError, match="non-canonical legacy stub"): - import importlib - importlib.import_module("gate") + spec.loader.exec_module(mod) From d3c062beb964e3d462195b318c2097e7dbb448c1 Mon Sep 17 00:00:00 2001 From: Ricky Jones Date: Sun, 22 Feb 2026 12:46:59 +0000 Subject: [PATCH 11/11] fix: use file-based import in v0 stop_machine stub test Updated the test to import the stop_machine module using importlib and check for RuntimeError. --- primitives/stop-machine-v0/test_stop_machine.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/primitives/stop-machine-v0/test_stop_machine.py b/primitives/stop-machine-v0/test_stop_machine.py index 3df0bf9..a717f26 100644 --- a/primitives/stop-machine-v0/test_stop_machine.py +++ b/primitives/stop-machine-v0/test_stop_machine.py @@ -1,9 +1,13 @@ """Stub test: confirms the v0 stop_machine raises RuntimeError on import.""" import pytest +import importlib.util +from pathlib import Path def test_import_stop_machine_v0_raises(): """Importing the v0 stub must raise RuntimeError.""" + stub = Path(__file__).resolve().parent / "stop_machine.py" + spec = importlib.util.spec_from_file_location("stop_machine_v0_stub", stub) + mod = importlib.util.module_from_spec(spec) with pytest.raises(RuntimeError, match="non-canonical legacy stub"): - import importlib - importlib.import_module("stop_machine") + spec.loader.exec_module(mod)