Type probe pipeline internals: ProbeTarget, frozen ProbeHit, typed diff, validated PollResult#91
Type probe pipeline internals: ProbeTarget, frozen ProbeHit, typed diff, validated PollResult#91henry0816191 wants to merge 1 commit into
Conversation
…ff, validated PollResult
📝 WalkthroughWalkthroughThis PR replaces untyped probe pipeline internals with typed structures: a new ChangesTyped probe pipeline refactor
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 `@tests/test_monitor.py`:
- Around line 205-228: The pytest assertions in test_rejects_invalid_diff_type,
test_rejects_invalid_probe_hit_element,
test_rejects_invalid_dp_transition_element, and
test_rejects_invalid_per_user_matches_value use raw regex patterns where the dot
in PollResult.* is being treated as a metacharacter. Update each match= string
to escape literal punctuation, or build the pattern with re.escape, so the
exception checks in PollResult and its related constructors are explicit and
unambiguous.
🪄 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: 211de7e1-331e-4459-aabb-3a526b509e6c
📒 Files selected for processing (6)
src/paperscout/models.pysrc/paperscout/monitor.pysrc/paperscout/protocols.pysrc/paperscout/sources.pytests/test_monitor.pytests/test_sources.py
| def test_rejects_invalid_diff_type(self): | ||
| with pytest.raises(TypeError, match="PollResult.diff must be a DiffResult"): | ||
| PollResult(diff={}, probe_hits=[]) # type: ignore[arg-type] | ||
|
|
||
| def test_rejects_invalid_probe_hit_element(self): | ||
| diff = DiffResult(new_papers=[], updated_papers=[]) | ||
| with pytest.raises(TypeError, match="PollResult.probe_hits must contain only ProbeHit"): | ||
| PollResult(diff=diff, probe_hits=["not-a-hit"]) # type: ignore[list-item] | ||
|
|
||
| def test_rejects_invalid_dp_transition_element(self): | ||
| diff = DiffResult(new_papers=[], updated_papers=[]) | ||
| with pytest.raises( | ||
| TypeError, match="PollResult.dp_transitions must contain only DPTransition" | ||
| ): | ||
| PollResult(diff=diff, probe_hits=[], dp_transitions=["not-a-transition"]) # type: ignore[list-item] | ||
|
|
||
| def test_rejects_invalid_per_user_matches_value(self): | ||
| diff = DiffResult(new_papers=[], updated_papers=[]) | ||
| with pytest.raises( | ||
| TypeError, | ||
| match="PollResult.per_user_matches values must be PerUserMatches", | ||
| ): | ||
| PollResult(diff=diff, probe_hits=[], per_user_matches={"U1": "bad"}) # type: ignore[dict-item] | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Escape regex metacharacters in match= patterns (RUF043).
The . in strings like "PollResult.diff must be a DiffResult" is a regex metacharacter, not a literal dot. It happens to still match here, but per the static analysis hint this is unintentional and should be escaped for correctness/clarity.
🔧 Proposed fix using re.escape
+import re
+
def test_rejects_invalid_diff_type(self):
- with pytest.raises(TypeError, match="PollResult.diff must be a DiffResult"):
+ with pytest.raises(TypeError, match=re.escape("PollResult.diff must be a DiffResult")):
PollResult(diff={}, probe_hits=[]) # type: ignore[arg-type]
def test_rejects_invalid_probe_hit_element(self):
diff = DiffResult(new_papers=[], updated_papers=[])
- with pytest.raises(TypeError, match="PollResult.probe_hits must contain only ProbeHit"):
+ with pytest.raises(
+ TypeError, match=re.escape("PollResult.probe_hits must contain only ProbeHit")
+ ):
PollResult(diff=diff, probe_hits=["not-a-hit"]) # type: ignore[list-item]
def test_rejects_invalid_dp_transition_element(self):
diff = DiffResult(new_papers=[], updated_papers=[])
with pytest.raises(
- TypeError, match="PollResult.dp_transitions must contain only DPTransition"
+ TypeError,
+ match=re.escape("PollResult.dp_transitions must contain only DPTransition"),
):
PollResult(diff=diff, probe_hits=[], dp_transitions=["not-a-transition"]) # type: ignore[list-item]
def test_rejects_invalid_per_user_matches_value(self):
diff = DiffResult(new_papers=[], updated_papers=[])
with pytest.raises(
TypeError,
- match="PollResult.per_user_matches values must be PerUserMatches",
+ match=re.escape("PollResult.per_user_matches values must be PerUserMatches"),
):
PollResult(diff=diff, probe_hits=[], per_user_matches={"U1": "bad"}) # type: ignore[dict-item]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def test_rejects_invalid_diff_type(self): | |
| with pytest.raises(TypeError, match="PollResult.diff must be a DiffResult"): | |
| PollResult(diff={}, probe_hits=[]) # type: ignore[arg-type] | |
| def test_rejects_invalid_probe_hit_element(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises(TypeError, match="PollResult.probe_hits must contain only ProbeHit"): | |
| PollResult(diff=diff, probe_hits=["not-a-hit"]) # type: ignore[list-item] | |
| def test_rejects_invalid_dp_transition_element(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises( | |
| TypeError, match="PollResult.dp_transitions must contain only DPTransition" | |
| ): | |
| PollResult(diff=diff, probe_hits=[], dp_transitions=["not-a-transition"]) # type: ignore[list-item] | |
| def test_rejects_invalid_per_user_matches_value(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises( | |
| TypeError, | |
| match="PollResult.per_user_matches values must be PerUserMatches", | |
| ): | |
| PollResult(diff=diff, probe_hits=[], per_user_matches={"U1": "bad"}) # type: ignore[dict-item] | |
| import re | |
| def test_rejects_invalid_diff_type(self): | |
| with pytest.raises(TypeError, match=re.escape("PollResult.diff must be a DiffResult")): | |
| PollResult(diff={}, probe_hits=[]) # type: ignore[arg-type] | |
| def test_rejects_invalid_probe_hit_element(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises(TypeError, match=re.escape("PollResult.probe_hits must contain only ProbeHit")): | |
| PollResult(diff=diff, probe_hits=["not-a-hit"]) # type: ignore[list-item] | |
| def test_rejects_invalid_dp_transition_element(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises( | |
| TypeError, | |
| match=re.escape("PollResult.dp_transitions must contain only DPTransition"), | |
| ): | |
| PollResult(diff=diff, probe_hits=[], dp_transitions=["not-a-transition"]) # type: ignore[list-item] | |
| def test_rejects_invalid_per_user_matches_value(self): | |
| diff = DiffResult(new_papers=[], updated_papers=[]) | |
| with pytest.raises( | |
| TypeError, | |
| match=re.escape("PollResult.per_user_matches values must be PerUserMatches"), | |
| ): | |
| PollResult(diff=diff, probe_hits=[], per_user_matches={"U1": "bad"}) # type: ignore[dict-item] |
🧰 Tools
🪛 Ruff (0.15.20)
[warning] 206-206: Pattern passed to match= contains metacharacters but is neither escaped nor raw
(RUF043)
[warning] 211-211: Pattern passed to match= contains metacharacters but is neither escaped nor raw
(RUF043)
[warning] 217-217: Pattern passed to match= contains metacharacters but is neither escaped nor raw
(RUF043)
[warning] 225-225: Pattern passed to match= contains metacharacters but is neither escaped nor raw
(RUF043)
🤖 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 `@tests/test_monitor.py` around lines 205 - 228, The pytest assertions in
test_rejects_invalid_diff_type, test_rejects_invalid_probe_hit_element,
test_rejects_invalid_dp_transition_element, and
test_rejects_invalid_per_user_matches_value use raw regex patterns where the dot
in PollResult.* is being treated as a metacharacter. Update each match= string
to escape literal punctuation, or build the pattern with re.escape, so the
exception checks in PollResult and its related constructors are explicit and
unambiguous.
Source: Linters/SAST tools
Summary
Replace untyped/mutable probe pipeline internals with named, validated types so callers cannot couple to bare tuples or silently mutate probe results.
ProbeTargetdataclass and remove the bare_Entry6-tuple; update_build_probe_list,_build_hot_list,_build_cold_slice, andrun_cycleto use named fieldsDataSource.diffasSourceDiff(DiffResult | list[ProbeHit] | list[OpenStdEntry]) and annotateWG21Index.diff→DiffResult(withTYPE_CHECKINGimport to preserve thesources ↔ monitorcycle break)ProbeHit(@dataclass(frozen=True, slots=True))PollResultto a validated dataclass with__post_init__checks fordiff,probe_hits,dp_transitions, andper_user_matchesTest plan
uv run pytest -q— 418 passed, 2 skippeduv run pytest tests/ --cov=paperscout --cov-fail-under=90— 93.55% coverageuv run mypy src/paperscout/protocols.py src/paperscout/monitor.py src/paperscout/__main__.py— strict gate cleanuv run ruff check src tests— cleanuv run ruff format --check src tests— cleanpre-commit run --all-filesRelated issues
Summary by CodeRabbit