[AUTO-INF-3B] features/environment.py uses # type: ignore comments in _install_fast_sleep_patch() violating CONTRIBUTING.md strict typing policy
Removed 6 `# type: ignore` suppressions from `_install_fast_sleep_patch()` in `features/environment.py`. Used `cast(Any, module)` + direct attribute assignment instead of `setattr()` (which ruff B010 disallows) and `# type: ignore` comments. Added `from collections.abc import Callable` and `from typing import cast` imports. Added new feature file `features/test_infra_sleep_patch.feature` with 4 scenarios verifying the behavior. Added new steps file `features/steps/test_infra_sleep_patch_steps.py`. ISSUES CLOSED: #9993
This commit is contained in:
+32
-17
@@ -7,8 +7,9 @@ import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from behave.model import Scenario, Status
|
||||
|
||||
@@ -392,11 +393,16 @@ def _install_fast_sleep_patch() -> None:
|
||||
operations fail deterministically, so the long sleeps are pure overhead
|
||||
(~1 s per retry cycle x hundreds of scenarios = minutes of wasted time).
|
||||
|
||||
Both functions are replaced with capped versions (≤ 10 ms). The originals
|
||||
are saved as ``time._original_sleep`` / ``asyncio._original_sleep`` and can
|
||||
be called directly by any test step that needs a genuine delay (e.g.
|
||||
CircuitBreaker recovery-timeout tests that need real wall-clock advancement
|
||||
past a 100 ms threshold).
|
||||
Both functions are replaced with capped versions (<=10 ms). The originals
|
||||
are stored on the modules as ``_original_sleep`` and can be retrieved with
|
||||
``getattr(time, "_original_sleep", time.sleep)`` by any test step that
|
||||
needs a genuine delay (e.g. CircuitBreaker recovery-timeout tests that
|
||||
need real wall-clock advancement past a 100 ms threshold).
|
||||
|
||||
``cast(Any, module)`` is used to assign dynamic attributes without
|
||||
``# type: ignore`` suppressions: the ``features/`` directory is excluded
|
||||
from Pyright's ``include`` list, so the cast is a documentation aid rather
|
||||
than a runtime necessity.
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
@@ -404,22 +410,31 @@ def _install_fast_sleep_patch() -> None:
|
||||
_MAX_SLEEP = 0.01 # 10 ms cap
|
||||
|
||||
# --- synchronous time.sleep ---
|
||||
# Store the original in a typed local variable so the inner closure can
|
||||
# call it directly. cast(Any, time) lets us assign _original_sleep and
|
||||
# replace sleep without attr-defined / assignment type errors.
|
||||
if not callable(getattr(time, "_original_sleep", None)):
|
||||
time._original_sleep = time.sleep # type: ignore[attr-defined]
|
||||
_orig_time_sleep: Callable[[float], None] = time.sleep
|
||||
_time_mod: Any = cast(Any, time)
|
||||
_time_mod._original_sleep = _orig_time_sleep
|
||||
|
||||
def _capped_sleep(seconds: float) -> None:
|
||||
time._original_sleep(min(seconds, _MAX_SLEEP)) # type: ignore[attr-defined]
|
||||
_orig_time_sleep(min(seconds, _MAX_SLEEP))
|
||||
|
||||
time.sleep = _capped_sleep # type: ignore[assignment]
|
||||
_time_mod.sleep = _capped_sleep
|
||||
|
||||
# --- asynchronous asyncio.sleep ---
|
||||
# Same pattern: capture the original in a typed local variable, then use
|
||||
# cast(Any, asyncio) to assign _original_sleep and replace sleep.
|
||||
if not callable(getattr(asyncio, "_original_sleep", None)):
|
||||
asyncio._original_sleep = asyncio.sleep # type: ignore[attr-defined]
|
||||
_orig_asyncio_sleep = asyncio.sleep
|
||||
_asyncio_mod: Any = cast(Any, asyncio)
|
||||
_asyncio_mod._original_sleep = _orig_asyncio_sleep
|
||||
|
||||
async def _capped_async_sleep(seconds: float, result: object = None) -> object:
|
||||
return await asyncio._original_sleep(min(seconds, _MAX_SLEEP), result) # type: ignore[attr-defined]
|
||||
return await _orig_asyncio_sleep(min(seconds, _MAX_SLEEP), result)
|
||||
|
||||
asyncio.sleep = _capped_async_sleep # type: ignore[assignment]
|
||||
_asyncio_mod.sleep = _capped_async_sleep
|
||||
|
||||
|
||||
def _ensure_template_db() -> None:
|
||||
@@ -478,11 +493,11 @@ def _install_template_db_patch() -> None:
|
||||
def _fast_init_or_upgrade(self: Any, **kwargs: Any) -> None:
|
||||
"""Replace Alembic migrations with fast alternatives.
|
||||
|
||||
- Process-global cache hit → immediate return (no work at all)
|
||||
- Non-SQLite databases → fall through to original
|
||||
- In-memory SQLite → ``Base.metadata.create_all()`` + alembic stamp
|
||||
- File-based SQLite with matching prefix → copy template
|
||||
- Everything else → fall through to original
|
||||
- Process-global cache hit -> immediate return (no work at all)
|
||||
- Non-SQLite databases -> fall through to original
|
||||
- In-memory SQLite -> ``Base.metadata.create_all()`` + alembic stamp
|
||||
- File-based SQLite with matching prefix -> copy template
|
||||
- Everything else -> fall through to original
|
||||
"""
|
||||
db_url: str = getattr(self, "database_url", "")
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Step definitions for the fast sleep patch type-safe implementation tests.
|
||||
|
||||
These scenarios verify the observable behaviour of ``_install_fast_sleep_patch()``
|
||||
after the ``# type: ignore`` suppressions were replaced with type-safe
|
||||
``setattr()`` calls and local typed variables (issue #9993).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from behave import then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from features.environment import _install_fast_sleep_patch
|
||||
|
||||
|
||||
@when("I call time.sleep with {seconds:f} seconds")
|
||||
def step_call_time_sleep(context: Context, seconds: float) -> None:
|
||||
"""Call the (patched) time.sleep and record elapsed wall-clock time."""
|
||||
start = time.monotonic()
|
||||
time.sleep(seconds)
|
||||
context.elapsed_seconds = time.monotonic() - start
|
||||
|
||||
|
||||
@then("the call should complete in under 500ms")
|
||||
def step_call_completes_quickly(context: Context) -> None:
|
||||
"""Assert the patched sleep completed well under the requested duration."""
|
||||
elapsed: float = context.elapsed_seconds
|
||||
assert elapsed < 0.5, (
|
||||
f"Expected patched time.sleep to complete in under 500ms, "
|
||||
f"but it took {elapsed * 1000:.1f}ms"
|
||||
)
|
||||
|
||||
|
||||
@then("time._original_sleep should be a callable")
|
||||
def step_time_original_sleep_callable(context: Context) -> None:
|
||||
"""Assert time._original_sleep was stored by the patch and is callable."""
|
||||
original = getattr(time, "_original_sleep", None)
|
||||
assert callable(original), (
|
||||
f"Expected time._original_sleep to be callable after patch installation, "
|
||||
f"got {original!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("asyncio._original_sleep should be a callable")
|
||||
def step_asyncio_original_sleep_callable(context: Context) -> None:
|
||||
"""Assert asyncio._original_sleep was stored by the patch and is callable."""
|
||||
original = getattr(asyncio, "_original_sleep", None)
|
||||
assert callable(original), (
|
||||
f"Expected asyncio._original_sleep to be callable after patch installation, "
|
||||
f"got {original!r}"
|
||||
)
|
||||
|
||||
|
||||
@when("I call _install_fast_sleep_patch a second time")
|
||||
def step_call_patch_second_time(context: Context) -> None:
|
||||
"""Record the current _original_sleep, then call the patch again."""
|
||||
context.original_sleep_before_second_call = getattr(time, "_original_sleep", None)
|
||||
_install_fast_sleep_patch()
|
||||
|
||||
|
||||
@then("time._original_sleep should remain the same callable after the second call")
|
||||
def step_original_sleep_unchanged(context: Context) -> None:
|
||||
"""Assert idempotency: a second patch call must not replace _original_sleep."""
|
||||
original_after = getattr(time, "_original_sleep", None)
|
||||
assert original_after is context.original_sleep_before_second_call, (
|
||||
"Expected time._original_sleep to remain the same callable after a "
|
||||
"second call to _install_fast_sleep_patch() (idempotency guard), "
|
||||
"but it was replaced"
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
@mock_only
|
||||
Feature: Fast sleep patch — type-safe implementation
|
||||
As a CleverAgents developer
|
||||
I want _install_fast_sleep_patch() to cap sleep durations without type suppressions
|
||||
So that Pyright strict mode passes and test execution remains fast
|
||||
|
||||
# These scenarios verify the observable behaviour of _install_fast_sleep_patch()
|
||||
# after the # type: ignore suppressions were replaced with type-safe setattr()
|
||||
# calls and local typed variables (issue #9993).
|
||||
|
||||
Scenario: time.sleep is capped at the 10ms maximum
|
||||
When I call time.sleep with 5.0 seconds
|
||||
Then the call should complete in under 500ms
|
||||
|
||||
Scenario: time._original_sleep is accessible for tests that need real delays
|
||||
Then time._original_sleep should be a callable
|
||||
|
||||
Scenario: asyncio._original_sleep is accessible for tests that need real delays
|
||||
Then asyncio._original_sleep should be a callable
|
||||
|
||||
Scenario: _install_fast_sleep_patch is idempotent when called multiple times
|
||||
When I call _install_fast_sleep_patch a second time
|
||||
Then time._original_sleep should remain the same callable after the second call
|
||||
Reference in New Issue
Block a user