Add override_settings() context manager for test singleton overrides#75
Add override_settings() context manager for test singleton overrides#75henry0816191 wants to merge 3 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds Changesoverride_settings context manager
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/paperscout/config.py`:
- Around line 224-252: The override_settings function uses a simple list-based
stack (_override_stack) which is not safe for concurrent or overlapping
contexts, as snapshots could be restored out of order causing state leaks. Add
an owner guard mechanism by storing context identifiers (such as thread ID or
async task ID) alongside each snapshot in _override_stack, then in the finally
block verify that the popped entry matches the current context before
restoring—raising a RuntimeError if a non-LIFO pattern is detected where the
stack owner doesn't match the current execution context.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 23f2d788-a420-4757-adaa-db30af948a9b
📒 Files selected for processing (4)
src/paperscout/config.pytests/conftest.pytests/test_config.pytests/test_sources.py
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/paperscout/config.py (1)
233-233:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject cross-owner overrides before mutating global settings.
The stack check happens only in
finally, so a second thread/task can still enter and overwrite the singleton while the first override is active. If the second exits first, no error is raised; if the first exits first, it raises without restoring and the later restore can leave the first override value behind. Gate entry with a lock and reject active frames owned by another execution context.Proposed hardening: serialize stack access and reject cross-owner overlap on entry
_override_stack: list[_OverrideFrame] = [] +_override_lock = threading.RLock() @@ - frame = _OverrideFrame(snapshot=settings.model_dump(), owner=_execution_context()) - _override_stack.append(frame) + owner = _execution_context() + with _override_lock: + if _override_stack and _override_stack[-1].owner != owner: + raise RuntimeError( + "override_settings: concurrent override detected; " + "nested overrides must use the same thread/task" + ) + frame = _OverrideFrame(snapshot=settings.model_dump(), owner=owner) + _override_stack.append(frame) try: for key, value in kwargs.items(): setattr(settings, key, value) yield settings finally: current = _execution_context() - if frame.owner != current: - raise RuntimeError( - "override_settings: owner context changed during override " - f"(entered as {frame.owner!r}, exiting as {current!r})" - ) - if not _override_stack or _override_stack[-1] is not frame: - if frame in _override_stack: - _override_stack.remove(frame) - raise RuntimeError( - "override_settings: non-LIFO or concurrent override detected; " - "nested overrides must exit in reverse order on the same thread/task" - ) - _override_stack.pop() - _restore_snapshot(frame.snapshot) + with _override_lock: + if frame.owner != current: + if _override_stack and _override_stack[-1] is frame: + _override_stack.pop() + _restore_snapshot(frame.snapshot) + raise RuntimeError( + "override_settings: owner context changed during override " + f"(entered as {frame.owner!r}, exiting as {current!r})" + ) + if not _override_stack or _override_stack[-1] is not frame: + if frame in _override_stack: + _override_stack.remove(frame) + raise RuntimeError( + "override_settings: non-LIFO or concurrent override detected; " + "nested overrides must exit in reverse order on the same thread/task" + ) + _override_stack.pop() + _restore_snapshot(frame.snapshot)Also applies to: 271-292
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/paperscout/config.py` at line 233, The global _override_stack variable has a race condition where multiple threads or tasks can interfere with each other because concurrent access is not synchronized and ownership validation happens too late in the finally block. Add a threading lock to serialize all access to _override_stack and implement ownership checking at the entry point of override operations (before pushing to the stack) rather than only in the finally block. When entering an override context, acquire the lock and verify that no active frames in the stack are owned by a different execution context. If a conflict is detected, reject the override immediately. This prevents the scenario where a second execution context can overwrite or corrupt the stack state while a first override is still active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/paperscout/config.py`:
- Around line 271-276: The code directly uses setattr to apply override values
to the settings object without validation, which bypasses Pydantic's constraints
since the model does not have validate_assignment enabled, allowing invalid
values like negative timeouts to be assigned. Instead of iterating through
kwargs and using setattr directly, create a validated copy of the settings by
calling settings.model_validate on the merged configuration (combining the
current model_dump with the overrides from kwargs), then apply only the
validated result back to the settings object to ensure all constrained fields
like wg21_index_timeout_s are properly validated before mutation.
In `@tests/test_config.py`:
- Around line 147-182: The test_override_settings_rejects_concurrent_threads
test is non-deterministic because both threads start immediately without
ensuring thread A enters its override_settings context first. Modify the test to
wait for the b_inside event to be set by thread B before actually starting
thread B (start thread A first, then wait for b_inside to be signaled, then
start thread B). Additionally, verify that both t_a.join() and t_b.join()
actually complete within their timeout (check that they return True) before
clearing the config_module._override_stack to ensure the threads have fully
finished execution.
---
Duplicate comments:
In `@src/paperscout/config.py`:
- Line 233: The global _override_stack variable has a race condition where
multiple threads or tasks can interfere with each other because concurrent
access is not synchronized and ownership validation happens too late in the
finally block. Add a threading lock to serialize all access to _override_stack
and implement ownership checking at the entry point of override operations
(before pushing to the stack) rather than only in the finally block. When
entering an override context, acquire the lock and verify that no active frames
in the stack are owned by a different execution context. If a conflict is
detected, reject the override immediately. This prevents the scenario where a
second execution context can overwrite or corrupt the stack state while a first
override is still active.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4ee3da17-3d27-42fa-a173-e976d982943d
📒 Files selected for processing (2)
src/paperscout/config.pytests/test_config.py
Summary
override_settings(**kwargs)to temporarily override fields on the module-levelsettingssingleton in tests and integration scenarios.Settingsinstance in place so allfrom paperscout.config import settingsimporters see changes (reassignment would not propagate to already-imported bindings).withblock raises; nested overrides are supported.config.pymodule docstring and clarify inconftest.pythat_PAPERSCOUT_TESTINGis import-time only for Slack validation bypass.test_download_uses_wg21_index_timeout_from_settingsto demonstrate real usage withoutcfg=make_test_settings(...).Test plan
uv run pytest tests/test_config.py::TestOverrideSettings -quv run pytest tests/test_sources.py::TestWG21Index::test_download_uses_wg21_index_timeout_from_settings -quv run pytest tests/ -q --cov=paperscout --cov-fail-under=90(382 passed, 93% coverage)Summary by CodeRabbit
Release Notes
New Features
override_settingscontext manager for temporary runtime configuration overrides that automatically restore afterward, with validation of override keys and values.Tests
override_settings.Documentation