Skip to content

Type probe pipeline internals: ProbeTarget, frozen ProbeHit, typed diff, validated PollResult#91

Open
henry0816191 wants to merge 1 commit into
developfrom
fix/replace-untyped-internals
Open

Type probe pipeline internals: ProbeTarget, frozen ProbeHit, typed diff, validated PollResult#91
henry0816191 wants to merge 1 commit into
developfrom
fix/replace-untyped-internals

Conversation

@henry0816191

@henry0816191 henry0816191 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replace untyped/mutable probe pipeline internals with named, validated types so callers cannot couple to bare tuples or silently mutate probe results.

  • Add frozen ProbeTarget dataclass and remove the bare _Entry 6-tuple; update _build_probe_list, _build_hot_list, _build_cold_slice, and run_cycle to use named fields
  • Type DataSource.diff as SourceDiff (DiffResult | list[ProbeHit] | list[OpenStdEntry]) and annotate WG21Index.diffDiffResult (with TYPE_CHECKING import to preserve the sources ↔ monitor cycle break)
  • Freeze ProbeHit (@dataclass(frozen=True, slots=True))
  • Convert PollResult to a validated dataclass with __post_init__ checks for diff, probe_hits, dp_transitions, and per_user_matches

Test plan

  • uv run pytest -q — 418 passed, 2 skipped
  • uv run pytest tests/ --cov=paperscout --cov-fail-under=90 — 93.55% coverage
  • uv run mypy src/paperscout/protocols.py src/paperscout/monitor.py src/paperscout/__main__.py — strict gate clean
  • uv run ruff check src tests — clean
  • uv run ruff format --check src tests — clean
  • pre-commit run --all-files

Related issues

Summary by CodeRabbit

  • Bug Fixes
    • Improved polling and probing reliability by tightening data validation and handling scheduled probe targets more consistently.
    • Reduced the chance of malformed results causing runtime issues during monitoring.
    • Updated probe discovery to keep returned items consistent and easier to process.

@henry0816191 henry0816191 self-assigned this Jul 7, 2026
@henry0816191 henry0816191 requested a review from wpak-ai as a code owner July 7, 2026 21:15
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces untyped probe pipeline internals with typed structures: a new ProbeTarget frozen dataclass replaces tuple-based probe entries in sources.py, PollResult becomes a validated dataclass, and DataSource.diff/WG21Index.diff gain concrete return types via a SourceDiff type alias. Tests updated accordingly.

Changes

Typed probe pipeline refactor

Layer / File(s) Summary
ProbeTarget dataclass
src/paperscout/models.py
Adds frozen, slotted ProbeTarget dataclass with url, tier, prefix, number, revision, extension fields.
Probe-list construction using ProbeTarget
src/paperscout/sources.py
Removes tuple-based _Entry type; _build_probe_list, _build_hot_list, _build_cold_slice, run_cycle, and task construction now produce/consume ProbeTarget objects by named attribute.
PollResult dataclass validation
src/paperscout/monitor.py, tests/test_monitor.py
Converts PollResult to a slotted dataclass with default factories and __post_init__ runtime type validation; casts WG21 diff result to DiffResult; adds TestPollResult tests.
DataSource protocol return typing
src/paperscout/protocols.py, src/paperscout/sources.py
Adds SourceDiff type alias under TYPE_CHECKING and narrows DataSource.diff/WG21Index.diff return types from Any to concrete types.
Test updates for ProbeTarget attributes
tests/test_sources.py
Updates assertions across probe-list tests to use named attributes (.url, .number, .tier, .prefix, .revision) instead of tuple indices.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • cppalliance/paperscout#5: Both PRs modify ISO probing/tiering logic in sources.py, connecting the ProbeTarget refactor to prior Tier/probe-flow changes.
  • cppalliance/paperscout#27: Both PRs modify the ISOProber pipeline's probe-list building and _probe_one wiring.
  • cppalliance/paperscout#65: Builds on the retrieved PR's introduction of the DataSource protocol by refining its diff return type.

Suggested reviewers: wpak-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific and accurately summarizes the main refactor: typed probe targets, immutable probe hits, and validated poll results.
Description check ✅ Passed It follows the required sections and includes a summary, test plan, and related issue link, so the template is mostly complete.
Linked Issues check ✅ Passed The changes implement the requested type-safety and immutability updates for the probe pipeline, matching issue #86.
Out of Scope Changes check ✅ Passed The additional import, typing, and test updates support the requested refactor and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/replace-untyped-internals

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4970494 and ca131f3.

📒 Files selected for processing (6)
  • src/paperscout/models.py
  • src/paperscout/monitor.py
  • src/paperscout/protocols.py
  • src/paperscout/sources.py
  • tests/test_monitor.py
  • tests/test_sources.py

Comment thread tests/test_monitor.py
Comment on lines +205 to +228
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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace untyped probe pipeline internals:

1 participant