Skip to content

Upgrade Pyright from basic to strict mode (closes #291)#316

Merged
nitrobass24 merged 16 commits intodevelopfrom
feat/pyright-strict
Mar 30, 2026
Merged

Upgrade Pyright from basic to strict mode (closes #291)#316
nitrobass24 merged 16 commits intodevelopfrom
feat/pyright-strict

Conversation

@nitrobass24
Copy link
Copy Markdown
Owner

@nitrobass24 nitrobass24 commented Mar 25, 2026

Summary

  • Pyright strict mode enabled with 0 errors across all non-test source files
  • Added missing type annotations to 30+ files (parameter types, generic type arguments, NamedTuple conversion)
  • Disabled reportUnknown* family rules — these produce cascading noise from untyped third-party dependencies (bottle, pexpect, tblib), not from project code
  • Disabled reportMissingTypeStubs and reportUnusedImport (conflicts with __init__.py re-export pattern)

What strict mode catches that basic didn't

Rule What it enforces
reportMissingParameterType All function params must have type annotations
reportMissingTypeArgument Generic classes need explicit type args (list[str], not list)
reportPrivateUsage No cross-class _protected access (ignored where intentional)
reportUnnecessaryComparison Dead-code conditions (ignored for defensive checks)
reportIncompatibleMethodOverride Override signatures must match base class

Deferred rules (to enable later with type stubs)

The reportUnknown* family (900+ errors) cascades from three untyped deps. These will be re-enabled as stubs become available:

  • bottle — no official type stubs
  • pexpect — no official type stubs
  • tblib — no official type stubs

Test plan

  • pyright — 0 errors in strict mode
  • ruff check — all checks passed
  • ruff format --check — all formatted
  • 425 Python unit tests pass
  • CI passes

Closes #291

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Broad typing cleanup and stricter type-checking across the codebase, plus added typing stubs and small API signature refinements.
  • Bug Fixes
    • Safer filesystem timestamp handling and adjusted constructor behavior to avoid a rare hostname error.
  • Tests
    • Added unit test verifying log records serialize formatted messages with arguments.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Narrowed and added Python type annotations across many modules, introduced postponed-evaluation imports in select files, standardized equality/repr signatures to accept object with NotImplemented guards, converted several untyped containers to generics, adjusted a few runtime checks (removed host-none guard; birthtime via getattr), and switched Pyright to strict mode.

Changes

Cohort / File(s) Summary
Common & logging
src/python/common/app_process.py, src/python/common/multiprocessing_logger.py, src/python/common/types.py, src/python/common/__init__.py
Added from __future__ import annotations; parameterized multiprocessing/Queue types (e.g., Queue[ExceptionWrapper], multiprocessing.Queue[logging.LogRecord]); tightened decorator typing and reformatted re-exports.
Config / context / utils
src/python/common/config.py, src/python/common/context.py, src/python/web/utils.py
Widened callable types to Callable[..., Any], tightened dict/OrderedDict generics and Args.as_dict return type, annotated StreamQueue internal queue.
Controller core & internals
src/python/controller/controller.py, src/python/controller/auto_queue.py, src/python/controller/controller_persist.py
Extensive internal typing: TYPE_CHECKING guards, typed queues/commands/lists/sets, refactored update flow for extract-completion handling, many __eq__ methods changed to accept object with NotImplemented.
Extract / validate / processes
src/python/controller/extract/..., src/python/controller/validate/validate_process.py, src/python/controller/extract/extract_process.py
Parameterized multiprocessing queues/result types, CompletedProcess[str] annotation, TYPE_CHECKING imports, tightened equality and request/status signatures.
Model & system structures
src/python/model/*, src/python/system/file.py, src/python/scan_fs.py, src/python/system/scanner.py
Typed internal containers/fields (e.g., dict[str, ModelFile], list[SystemFile], `datetime
LFTP & pexpect typings
src/python/lftp/job_status.py, src/python/lftp/lftp.py, src/python/typings/pexpect/*
Replaced namedtuple with NamedTuple, narrowed caches/process types, improved decorator typing, and added new pexpect stubs.
Web layer & handlers
src/python/web/*.py, src/python/web/handler/*, src/python/web/serialize/*, src/python/web/web_app.py, src/python/web/web_app_job.py
Tightened handler signatures to Callable[..., Any], typed request/response/log-entry shapes, added WSGI/middleware typing, and refined streaming handler kwargs.
CLI, SSH, seedsync
src/python/seedsync.py, src/python/ssh/sshcp.py
Added precise arg/return annotations (`FrameType
Typings & Pyright
src/python/pyrightconfig.json, src/python/typings/*
Switched Pyright typeCheckingMode from basic to strict; added multiple .pyi stubs (bottle, pexpect, tblib) and related typing declarations.
Re-exports / packaging imports
many src/python/*/__init__.py files
Converted package-level re-exports to explicit as aliases and grouped import formatting; exported names unchanged.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

refactor

Poem

🐰 I nibbled hints on every line,

I hopped through types to make them shine,
From basic checks to strict and bright,
Annotations snug at night,
A tiny rabbit's typed design.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main change: upgrading Pyright from basic to strict mode, which aligns with the primary objective of enabling strict type checking.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements in issue #291: strict mode enabled, 0 errors achieved, type annotations added across 30+ files, and third-party dependency issues managed via type stubs and suppressions.
Out of Scope Changes check ✅ Passed All changes are directly related to upgrading Pyright to strict mode: type annotations, generic type parameters, NamedTuple conversions, type stubs, and pyrightconfig.json adjustments. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pyright-strict

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/controller/extract/dispatch.py`:
- Around line 62-63: The __eq__ method on ExtractStatus blindly accesses
other.__dict__, which raises AttributeError for primitives or objects without
__dict__; add a type guard similar to ModelDiff.__eq__: check isinstance(other,
ExtractStatus) (or return NotImplemented when other is not an ExtractStatus) and
only then compare self.__dict__ == other.__dict__; this preserves correct
equality semantics and lets Python fallback to other comparisons when
appropriate.

In `@src/python/controller/validate/validate_process.py`:
- Around line 61-62: The __eq__ implementation on ValidateStatus should first
check that other is an instance of ValidateStatus and return NotImplemented for
non-matching types to follow Python's equality protocol; replace the direct
other.__dict__ access in ValidateStatus.__eq__ with an isinstance(other,
ValidateStatus) guard and only then compare the relevant attributes (e.g.,
self.__dict__ == other.__dict__) so comparing to ints/strings returns
NotImplemented instead of raising AttributeError.

In `@src/python/lftp/job_status.py`:
- Around line 85-86: The __eq__ implementation in LftpJobStatus should guard
against non-LftpJobStatus inputs to avoid AttributeError; update
LftpJobStatus.__eq__ to first check isinstance(other, LftpJobStatus) and if not,
return NotImplemented, otherwise compare the two objects (e.g., via
self.__dict__ == other.__dict__); apply the same pattern to other vulnerable
__eq__ implementations you saw (in file.py, validate_process.py, dispatch.py,
diff.py) to follow PEP 207.

In `@src/python/model/diff.py`:
- Around line 25-26: The ModelDiff.__eq__ method currently accesses
other.__dict__ directly which raises AttributeError for non-ModelDiff values;
update ModelDiff.__eq__ to first check isinstance(other, ModelDiff) (mirroring
the pattern used in AutoQueuePattern.__eq__) and return NotImplemented when the
other object is not a ModelDiff, otherwise compare the dicts as before.

In `@src/python/system/file.py`:
- Around line 29-30: The __eq__ implementation on SystemFile should guard
against non-SystemFile comparisons to avoid AttributeError; modify
SystemFile.__eq__ to first check the type (e.g., isinstance(other, SystemFile))
and if it is not the same type return NotImplemented (or False), otherwise
compare the internal state (self.__dict__) as before; reference the
SystemFile.__eq__ method and ensure the function returns NotImplemented for
unsupported types so Python can handle symmetric comparisons.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 10a0b4e6-6f93-4bba-93f1-d31a6670eb8f

📥 Commits

Reviewing files that changed from the base of the PR and between e5ece79 and 607c51b.

📒 Files selected for processing (32)
  • src/python/common/app_process.py
  • src/python/common/config.py
  • src/python/common/context.py
  • src/python/common/multiprocessing_logger.py
  • src/python/common/path_pairs_config.py
  • src/python/common/types.py
  • src/python/controller/auto_queue.py
  • src/python/controller/controller.py
  • src/python/controller/controller_persist.py
  • src/python/controller/extract/dispatch.py
  • src/python/controller/extract/extract.py
  • src/python/controller/extract/extract_process.py
  • src/python/controller/model_builder.py
  • src/python/controller/notifier.py
  • src/python/controller/validate/validate_process.py
  • src/python/lftp/job_status.py
  • src/python/lftp/lftp.py
  • src/python/model/diff.py
  • src/python/model/file.py
  • src/python/pyrightconfig.json
  • src/python/scan_fs.py
  • src/python/seedsync.py
  • src/python/ssh/sshcp.py
  • src/python/system/file.py
  • src/python/system/scanner.py
  • src/python/web/handler/logs.py
  • src/python/web/handler/path_pairs.py
  • src/python/web/handler/stream_log.py
  • src/python/web/security.py
  • src/python/web/serialize/serialize_model.py
  • src/python/web/web_app.py
  • src/python/web/web_app_job.py

Comment thread src/python/controller/extract/dispatch.py
Comment thread src/python/controller/validate/validate_process.py
Comment thread src/python/lftp/job_status.py
Comment thread src/python/model/diff.py
Comment thread src/python/system/file.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/python/web/serialize/serialize_log_record.py (1)

31-35: ⚠️ Potential issue | 🟠 Major

Use getMessage() to guarantee string safety.
At line 35, record.msg can be an arbitrary object (not guaranteed to be a string), which violates the declared dict[str, str | None] type and may fail during JSON serialization. The standard library provides LogRecord.getMessage() specifically to obtain the formatted message as a string.

Proposed fix
-        json_dict[SerializeLogRecord.__KEY_MESSAGE] = record.msg
+        json_dict[SerializeLogRecord.__KEY_MESSAGE] = record.getMessage()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/python/web/serialize/serialize_log_record.py` around lines 31 - 35, The
JSON dict builder in SerializeLogRecord uses record.msg which can be non-string;
replace that usage with record.getMessage() so the value stored under
SerializeLogRecord.__KEY_MESSAGE is guaranteed to be a string and matches the
declared dict[str, str | None] type. Update the assignment that sets
json_dict[SerializeLogRecord.__KEY_MESSAGE] to call record.getMessage() instead
of accessing record.msg, leaving other keys unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/common/path_pairs_config.py`:
- Around line 73-76: The PathPair.__eq__ currently returns False for
non-PathPair operands; change its type-check branch to return NotImplemented
(instead of False) to follow PEP 207 and match AutoQueuePattern.__eq__; locate
the __eq__ method on the PathPair class and replace the "if not
isinstance(other, PathPair): return False" behavior with "return NotImplemented"
so Python can try the reflected comparison on the other operand.

In `@src/python/ssh/sshcp.py`:
- Line 32: Remove the unreachable None-check for the parameter named host:
delete the "if host is None:  # type: ignore[reportUnnecessaryComparison]"
conditional and any associated unreachable branch in sshcp.py so the function
treats host as a non-nullable str; remove the type-ignore comment and do not
replace it with another guard (call sites already provide valid strings).

---

Outside diff comments:
In `@src/python/web/serialize/serialize_log_record.py`:
- Around line 31-35: The JSON dict builder in SerializeLogRecord uses record.msg
which can be non-string; replace that usage with record.getMessage() so the
value stored under SerializeLogRecord.__KEY_MESSAGE is guaranteed to be a string
and matches the declared dict[str, str | None] type. Update the assignment that
sets json_dict[SerializeLogRecord.__KEY_MESSAGE] to call record.getMessage()
instead of accessing record.msg, leaving other keys unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bd4fc7ff-9f5d-47da-903e-c93b14099153

📥 Commits

Reviewing files that changed from the base of the PR and between 607c51b and c4a4a4c.

📒 Files selected for processing (56)
  • src/python/common/__init__.py
  • src/python/common/config.py
  • src/python/common/context.py
  • src/python/common/multiprocessing_logger.py
  • src/python/common/path_pairs_config.py
  • src/python/common/status.py
  • src/python/controller/__init__.py
  • src/python/controller/auto_queue.py
  • src/python/controller/controller.py
  • src/python/controller/controller_persist.py
  • src/python/controller/delete/__init__.py
  • src/python/controller/extract/__init__.py
  • src/python/controller/extract/dispatch.py
  • src/python/controller/extract/extract_process.py
  • src/python/controller/model_builder.py
  • src/python/controller/move/__init__.py
  • src/python/controller/notifier.py
  • src/python/controller/scan/__init__.py
  • src/python/controller/scan/active_scanner.py
  • src/python/controller/scan/scanner_process.py
  • src/python/controller/validate/__init__.py
  • src/python/controller/validate/validate_process.py
  • src/python/lftp/__init__.py
  • src/python/lftp/job_status.py
  • src/python/lftp/job_status_parser.py
  • src/python/lftp/lftp.py
  • src/python/model/__init__.py
  • src/python/model/diff.py
  • src/python/model/file.py
  • src/python/model/model.py
  • src/python/pyrightconfig.json
  • src/python/scan_fs.py
  • src/python/seedsync.py
  • src/python/ssh/__init__.py
  • src/python/ssh/sshcp.py
  • src/python/system/__init__.py
  • src/python/system/file.py
  • src/python/system/scanner.py
  • src/python/typings/bottle/__init__.pyi
  • src/python/typings/pexpect/__init__.pyi
  • src/python/typings/pexpect/exceptions.pyi
  • src/python/typings/tblib/__init__.pyi
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/__init__.py
  • src/python/web/handler/logs.py
  • src/python/web/handler/path_pairs.py
  • src/python/web/handler/stream_log.py
  • src/python/web/security.py
  • src/python/web/serialize/__init__.py
  • src/python/web/serialize/serialize_auto_queue.py
  • src/python/web/serialize/serialize_config.py
  • src/python/web/serialize/serialize_log_record.py
  • src/python/web/serialize/serialize_model.py
  • src/python/web/serialize/serialize_status.py
  • src/python/web/utils.py
  • src/python/web/web_app.py

Comment thread src/python/common/path_pairs_config.py
Comment thread src/python/ssh/sshcp.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/scan_fs.py`:
- Line 41: Replace all PEP 526 inline variable annotations like
"self.__children: List[SystemFile] = []" with Python 3.5 compatible type
comments by moving the annotation into a trailing "# type: ..." comment (e.g.,
keep the initializer and add "# type: List[SystemFile]" after it). Apply this
change for self.__children and any other occurrences of the ": Type = value"
pattern in this file (the ones flagged around the later variable declarations),
ensuring you preserve the existing initializers and use the same type names
(List, SystemFile, etc.) in the "# type:" comments.

In `@src/python/system/scanner.py`:
- Around line 48-50: Replace PEP 604 and builtin generic annotations with typing
equivalents for consistency: change the attributes on SystemScanner —
exclude_prefixes: list[str], exclude_suffixes: list[str],
__lftp_temp_file_suffix: str | None — to use typing.List and typing.Optional
(e.g., exclude_prefixes: List[str], exclude_suffixes: List[str],
__lftp_temp_file_suffix: Optional[str]); update any other occurrences in this
module (the other annotations using list[...] or str | None) similarly (e.g.,
method args/returns referenced at the same spots) and add the import line from
typing import List, Optional at the top.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3d4e7b98-7bea-4727-bc03-5a481549ffa4

📥 Commits

Reviewing files that changed from the base of the PR and between c4a4a4c and 81c828b.

📒 Files selected for processing (5)
  • src/python/common/path_pairs_config.py
  • src/python/scan_fs.py
  • src/python/ssh/sshcp.py
  • src/python/system/scanner.py
  • src/python/web/serialize/serialize_log_record.py

Comment thread src/python/scan_fs.py Outdated
Comment thread src/python/system/scanner.py
nitrobass24 and others added 6 commits March 30, 2026 12:52
Switch pyrightconfig.json to strict mode with 0 errors. Changes:

- Add missing parameter type annotations across 30+ files
- Add type arguments to generic classes (Queue, list, dict, Callable, etc.)
- Convert namedtuple to NamedTuple with typed fields (job_status.py)
- Add `from __future__ import annotations` where runtime subscript
  of multiprocessing.Queue would fail
- Add type: ignore comments for intentional patterns:
  - reportPrivateUsage: cross-class access in controller
  - reportUnusedFunction: Bottle plugin decorators in security.py
  - reportUnnecessaryComparison: defensive None checks
- Disable reportUnknown* family (cascading noise from untyped deps:
  bottle, pexpect, tblib) and reportMissingTypeStubs — to be
  re-enabled incrementally as type stubs are added
- Disable reportUnusedImport (conflicts with __init__.py re-exports)

Closes #291

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Write custom type stubs for bottle, pexpect, and tblib (no upstream
stubs exist). Add generic type parameters to all class attributes,
method signatures, and local variables across 51 files. Use X-as-X
re-export pattern in __init__.py files. Remove all reportUnknown*
suppressions from pyrightconfig.json — strict mode now passes clean
with zero errors and zero suppressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return NotImplemented for non-matching types in ExtractStatus,
ValidateStatus, LftpJobStatus, ModelDiff, and SystemFile __eq__
methods, matching the existing pattern in ModelFile and
AutoQueuePattern. Prevents AttributeError when comparing against
primitives or unrelated objects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PathPair.__eq__: return NotImplemented instead of False for
  non-PathPair types (PEP 207)
- Sshcp.__init__: remove unreachable None check on non-nullable host
- SerializeLogRecord: use record.getMessage() instead of record.msg
  to ensure the message is always a string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use getattr() instead of direct attribute access for st_birthtime,
which is macOS-only and not in the Linux stat_result type stubs.
Same runtime behavior, but now passes pyright strict on Linux CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nitrobass24 nitrobass24 force-pushed the feat/pyright-strict branch from 81c828b to d181218 Compare March 30, 2026 17:52
nitrobass24 and others added 2 commits March 30, 2026 12:54
scan_fs.py runs on remote servers with Python 3.5+ where PEP 526
variable annotations are a SyntaxError. Exclude it from pyright
checking and use type comments instead of inline annotations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
staging_path defaults to None in config, so an assert crashes at
runtime in the E2E environment. Revert to type: ignore suppressions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/python/controller/controller.py (1)

44-66: ⚠️ Potential issue | 🟠 Major

Recompute directory sizes after pruning excluded children.

Line 53 copies the original aggregate file.size, but this helper can remove whole descendants. The returned directory then still counts bytes from excluded files, which can skew model diffs and any size-based state derived from the filtered tree.

Suggested fix
-    filtered = SystemFile(
-        name=file.name,
-        size=file.size,
-        is_dir=file.is_dir,
-        time_created=file.timestamp_created,
-        time_modified=file.timestamp_modified,
-    )
+    filtered_children: list[SystemFile] = []
     for child in file.children:
         if _matches_exclude(child.name, child.is_dir, patterns):
             continue  # drop matched child (and its subtree)
         if child.is_dir:
             child = _filter_children(child, patterns)
-        filtered.add_child(child)
+        filtered_children.append(child)
+    filtered = SystemFile(
+        name=file.name,
+        size=sum(child.size for child in filtered_children),
+        is_dir=file.is_dir,
+        time_created=file.timestamp_created,
+        time_modified=file.timestamp_modified,
+    )
+    for child in filtered_children:
+        filtered.add_child(child)
     return filtered
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/python/controller/controller.py` around lines 44 - 66, The helper
_filter_children currently copies file.size into the new SystemFile and then
prunes descendants, which leaves directory sizes stale; after recursively
filtering children (in _filter_children) recompute the directory's aggregate
size by summing sizes of filtered.children and assign that total to
filtered.size (leave file.size unchanged for non-directories). Update the logic
around filtered.add_child/_filter_children to ensure the recomputed size
reflects removed descendants so callers of _filter_children see correct
directory sizes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/controller/controller.py`:
- Around line 968-974: The block that iterates and processes shared
latest_extracted_results inside _update_pair_model_state causes duplicate
handling per pair; move that extraction-completion consumption logic out of
_update_pair_model_state and into __update_model (next to the validation
completion/failure handling) so each extraction result is processed exactly
once; update __update_model to iterate latest_extracted_results, perform the
persist sync and move-spawn checks there, and clear or mark consumed entries so
_update_pair_model_state no longer walks latest_extracted_results (keep
references to the same symbols: _update_pair_model_state, __update_model, and
latest_extracted_results).

In `@src/python/controller/validate/validate_process.py`:
- Around line 115-119: Change ValidateRequest.pair_id to allow None (type str |
None) so it matches how Controller constructs requests from _PairContext and
removes the need for type: ignore at call sites; update the __active_validations
key type from dict[tuple[str, str], ValidateRequest] to dict[tuple[str | None,
str], ValidateRequest] to reflect the actual (pair_id, request_id) key shape and
keep ValidateStatus/ValidateCompletedResult/ValidateFailedResult alignment with
the new optional pair_id.

In `@src/python/typings/tblib/pickling_support.pyi`:
- Around line 4-7: Change the type annotation for the get_locals parameter of
the install function so it accepts a callable receiving a frame object rather
than a TracebackType; update the signature in install to use a generic frame/Any
input and a simple dict return type (i.e., Callable[[Any], dict] | None) so
get_locals correctly types functions that take a frame; ensure you update the
import hints or typing references if needed and keep the rest of the install
signature unchanged.

In `@src/python/web/serialize/serialize_log_record.py`:
- Line 35: Add a unit test named something like test_serialize_message_with_args
that constructs a logging.LogRecord (or uses the same helper used in other
tests) with msg containing format placeholders (e.g. "%s %d" or "{}" depending
on formatter) and a non-empty args tuple, call SerializeLogRecord to produce the
json_dict, and assert that json_dict[SerializeLogRecord.__KEY_MESSAGE] equals
record.getMessage() (i.e. the fully formatted message). Ensure the test triggers
the same code path that uses record.getMessage() so the regression is covered.

In `@src/python/web/serialize/serialize_model.py`:
- Line 94: Replace the explicit generic constructor usage with a literal empty
list for readability: in the assignment to
json_dict[SerializeModel.__KEY_FILE_CHILDREN] (where json_dict is typed as
dict[str, Any]), change the initializer from list[dict[str, Any]]() to [] so the
code uses the idiomatic literal list form.

---

Outside diff comments:
In `@src/python/controller/controller.py`:
- Around line 44-66: The helper _filter_children currently copies file.size into
the new SystemFile and then prunes descendants, which leaves directory sizes
stale; after recursively filtering children (in _filter_children) recompute the
directory's aggregate size by summing sizes of filtered.children and assign that
total to filtered.size (leave file.size unchanged for non-directories). Update
the logic around filtered.add_child/_filter_children to ensure the recomputed
size reflects removed descendants so callers of _filter_children see correct
directory sizes.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93e0d87c-d46e-486e-88cc-0251fbfa6f98

📥 Commits

Reviewing files that changed from the base of the PR and between 81c828b and 5bff350.

📒 Files selected for processing (60)
  • src/python/common/__init__.py
  • src/python/common/app_process.py
  • src/python/common/config.py
  • src/python/common/context.py
  • src/python/common/multiprocessing_logger.py
  • src/python/common/path_pairs_config.py
  • src/python/common/status.py
  • src/python/common/types.py
  • src/python/controller/__init__.py
  • src/python/controller/auto_queue.py
  • src/python/controller/controller.py
  • src/python/controller/controller_persist.py
  • src/python/controller/delete/__init__.py
  • src/python/controller/extract/__init__.py
  • src/python/controller/extract/dispatch.py
  • src/python/controller/extract/extract.py
  • src/python/controller/extract/extract_process.py
  • src/python/controller/model_builder.py
  • src/python/controller/move/__init__.py
  • src/python/controller/notifier.py
  • src/python/controller/scan/__init__.py
  • src/python/controller/scan/active_scanner.py
  • src/python/controller/scan/scanner_process.py
  • src/python/controller/validate/__init__.py
  • src/python/controller/validate/validate_process.py
  • src/python/lftp/__init__.py
  • src/python/lftp/job_status.py
  • src/python/lftp/job_status_parser.py
  • src/python/lftp/lftp.py
  • src/python/model/__init__.py
  • src/python/model/diff.py
  • src/python/model/file.py
  • src/python/model/model.py
  • src/python/pyrightconfig.json
  • src/python/scan_fs.py
  • src/python/seedsync.py
  • src/python/ssh/__init__.py
  • src/python/ssh/sshcp.py
  • src/python/system/__init__.py
  • src/python/system/file.py
  • src/python/system/scanner.py
  • src/python/typings/bottle/__init__.pyi
  • src/python/typings/pexpect/__init__.pyi
  • src/python/typings/pexpect/exceptions.pyi
  • src/python/typings/tblib/__init__.pyi
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/__init__.py
  • src/python/web/handler/logs.py
  • src/python/web/handler/path_pairs.py
  • src/python/web/handler/stream_log.py
  • src/python/web/security.py
  • src/python/web/serialize/__init__.py
  • src/python/web/serialize/serialize_auto_queue.py
  • src/python/web/serialize/serialize_config.py
  • src/python/web/serialize/serialize_log_record.py
  • src/python/web/serialize/serialize_model.py
  • src/python/web/serialize/serialize_status.py
  • src/python/web/utils.py
  • src/python/web/web_app.py
  • src/python/web/web_app_job.py

Comment thread src/python/controller/controller.py
Comment thread src/python/controller/validate/validate_process.py Outdated
Comment thread src/python/typings/tblib/pickling_support.pyi
Comment thread src/python/web/serialize/serialize_log_record.py
Comment thread src/python/web/serialize/serialize_model.py Outdated
nitrobass24 and others added 2 commits March 30, 2026 13:26
StreamQueue[LogRecord].__init__(self) resolves to
_GenericAlias.__init__, not StreamQueue.__init__, so the internal
queue was never created. This broke SSE config delivery to the
frontend, causing all E2E settings/autoqueue/path-pairs tests to fail
with empty/disabled fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix duplicate extraction-completion handling: move extraction result
  processing from _update_pair_model_state to __update_model so each
  result is consumed exactly once
- Make ValidateRequest.pair_id accept None to match _PairContext.pair_id
  type, removing type: ignore at call sites
- Fix tblib stub: get_locals takes a frame (Any), not TracebackType
- Add test_serialize_message_with_args regression test for getMessage()
- Use idiomatic [] literal instead of list[dict[str, Any]]()
- Recompute directory sizes in _filter_children after pruning excluded
  children so filtered directories report accurate aggregate sizes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/controller/controller.py`:
- Around line 1347-1361: Add an assertion to narrow the type of
self.__context.config.controller.staging_path before computing staging_source so
Pyright can infer it's a str and you can remove the redundant "type: ignore"
comments; specifically, assert the staging_path is not None and is a str (e.g.
assert isinstance(self.__context.config.controller.staging_path, str)) before
using os.path.join to build staging_source and staging_file, then drop the
corresponding type: ignore annotations around staging_source, staging_file, and
the MoveProcess instantiation.

In `@src/python/web/handler/stream_log.py`:
- Line 34: Change the type annotation for the cached records to use typing.List
for repo consistency: update the annotation on self.__cached_records to
List[logging.LogRecord] and add or adjust the import from typing (e.g., ensure
List is imported) so the file uses the project-standard typing.List instead of
the built-in list generic; locate the attribute declaration in the class where
self.__cached_records is defined and update the annotation and imports
accordingly.

In `@src/python/web/serialize/serialize_model.py`:
- Line 71: Replace the dict() initialization with a literal to match the
idiomatic style used elsewhere: change the declaration of json_dict (json_dict:
dict[str, Any] = dict()) to use an empty dict literal {} so it becomes
json_dict: dict[str, Any] = {} for consistency with other literal
initializations (e.g., the empty list on line 94).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 046023a2-a02a-46d9-8e54-a6a0fa696ade

📥 Commits

Reviewing files that changed from the base of the PR and between 5bff350 and 4af049c.

📒 Files selected for processing (6)
  • src/python/controller/controller.py
  • src/python/controller/validate/validate_process.py
  • src/python/tests/unittests/test_web/test_serialize/test_serialize_log_record.py
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/handler/stream_log.py
  • src/python/web/serialize/serialize_model.py

Comment thread src/python/controller/controller.py Outdated
Comment thread src/python/web/handler/stream_log.py
Comment thread src/python/web/serialize/serialize_model.py Outdated
- Add isinstance assert on staging_path in __spawn_move_process (all
  callers already guard staging_path is truthy), removing 5 type: ignore
  comments
- Use {} literal instead of dict() in serialize_model for consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/controller/controller.py`:
- Around line 61-64: The code is replacing scanner-reported directory sizes by
summing kept_children into SystemFile.size (in the block constructing
SystemFile), which causes remote_size to diverge; instead preserve the original
scanner size and only prune children: when creating SystemFile (the constructor
call shown with name=file.name, size=...), set size=file.size (do not replace
with sum(c.size for c in kept_children)), and ensure child pruning logic only
updates the children list (kept_children) without mutating file.size;
references: _apply_exclude_patterns, SystemFile, file.is_dir, kept_children,
ModelBuilder/ModelFile.
- Around line 722-737: The extraction handler currently spawns a move as soon as
extraction finishes which races ongoing validation; change the block that
iterates latest_extracted_results to only call __spawn_move_process(result.name,
owner_pc) when no validation was queued for that pair+name. Implement this by
checking an existing validation-tracking mechanism (or add a small helper like
_is_validation_queued(pair_id, name)) before deciding to move: compute pkey via
_persist_key(result.pair_id, result.name), mark extracted as before, but only
call __spawn_move_process when _is_validation_queued(...) returns False;
otherwise leave the handoff to the validation completion/failure paths and still
call _sync_persist_to_all_builders() as before.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6afaccd8-540f-492d-b396-c3cd10f180f8

📥 Commits

Reviewing files that changed from the base of the PR and between 5bff350 and d2eb564.

📒 Files selected for processing (6)
  • src/python/controller/controller.py
  • src/python/controller/validate/validate_process.py
  • src/python/tests/unittests/test_web/test_serialize/test_serialize_log_record.py
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/handler/stream_log.py
  • src/python/web/serialize/serialize_model.py

Comment thread src/python/controller/controller.py Outdated
Comment thread src/python/controller/controller.py
Preserve scanner-reported directory sizes in _filter_children instead of
recomputing from kept children, which caused remote_size to diverge.

Add __pending_validation_keys tracking so extraction completion skips
__spawn_move_process when validation is queued, deferring the move to the
validation completion/failure paths that already call _spawn_deferred_move.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/python/web/handler/stream_log.py (1)

101-106: ⚠️ Potential issue | 🟡 Minor

Guard logger extraction from kwargs before use.

Line 104 assumes kwargs["logger"] always exists and is a logging.Logger; misuse will raise a raw KeyError/AttributeError. Add a defensive type check for clearer failure behavior.

Suggested patch
 `@classmethod`
 `@overrides`(IStreamHandler)
 def register(cls, web_app: WebApp, **kwargs: Any) -> None:
+    logger = kwargs.get("logger")
+    if not isinstance(logger, logging.Logger):
+        raise TypeError("LogStreamHandler.register requires keyword argument 'logger: logging.Logger'")
+
     # Initialize our cache when we register
     LogStreamHandler._cache = CachedQueueLogHandler(history_size_in_ms=LogStreamHandler._CACHE_HISTORY_SIZE_IN_MS)
-    kwargs["logger"].addHandler(LogStreamHandler._cache)
+    logger.addHandler(LogStreamHandler._cache)

     super().register(web_app=web_app, **kwargs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/python/web/handler/stream_log.py` around lines 101 - 106, In register
(class LogStreamHandler) guard extraction of kwargs["logger"] before using it:
check that "logger" is present in kwargs and that its value is an instance of
logging.Logger (or at least exposes addHandler), and if not, raise a clear
TypeError/ValueError or use kwargs.get with a descriptive exception; then only
call LogStreamHandler._cache = CachedQueueLogHandler(...) and
kwargs["logger"].addHandler(...) after the check succeeds so you avoid raw
KeyError/AttributeError and provide a clear failure message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/controller/controller.py`:
- Around line 45-50: The docstring for _filter_children incorrectly states that
directory sizes are recomputed from filtered children; update the docstring to
reflect the current behavior: the implementation preserves the original
file.size for directories (it does not recompute sizes) and only removes
excluded children/subtrees. Locate the _filter_children function in
controller.py and change the sentence about recomputing directory sizes to state
that directory sizes are preserved (or not modified) after filtering, keeping
any other descriptive text about child filtering intact.
- Around line 234-236: Change the post_callback parameter/attribute type in the
__init__ signature from Callable[..., Any] to Callable[[], None] so it
accurately reflects the zero-argument contract used when invoking
post_callback() (and update the attribute annotation accordingly); ensure the
call sites that supply callbacks—e.g., the bound method force_scan and the local
delete callback—conform to Callable[[], None] (add an explicit -> None return
annotation to local delete and add or tighten typing for force_scan so you can
remove the existing # type: ignore[assignment]).

---

Outside diff comments:
In `@src/python/web/handler/stream_log.py`:
- Around line 101-106: In register (class LogStreamHandler) guard extraction of
kwargs["logger"] before using it: check that "logger" is present in kwargs and
that its value is an instance of logging.Logger (or at least exposes
addHandler), and if not, raise a clear TypeError/ValueError or use kwargs.get
with a descriptive exception; then only call LogStreamHandler._cache =
CachedQueueLogHandler(...) and kwargs["logger"].addHandler(...) after the check
succeeds so you avoid raw KeyError/AttributeError and provide a clear failure
message.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 788ef505-b140-498a-ba65-5fbc56e37642

📥 Commits

Reviewing files that changed from the base of the PR and between 5bff350 and 89efae4.

📒 Files selected for processing (6)
  • src/python/controller/controller.py
  • src/python/controller/validate/validate_process.py
  • src/python/tests/unittests/test_web/test_serialize/test_serialize_log_record.py
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/handler/stream_log.py
  • src/python/web/serialize/serialize_model.py

Comment thread src/python/controller/controller.py
Comment thread src/python/controller/controller.py Outdated
nitrobass24 and others added 2 commits March 30, 2026 16:45
…e[[], None]

Update docstring to reflect that directory sizes are preserved (not recomputed).
Narrow CommandProcessWrapper.post_callback from Callable[..., Any] to
Callable[[], None] matching the zero-argument call contract, removing the
type: ignore on force_scan assignment. Add -> None annotation to force_scan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Avoids reassigning post_callback variable which Pyright typed from the
def in the DELETE_LOCAL branch, causing a type conflict in DELETE_REMOTE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/python/controller/controller.py (1)

1222-1226: 🧹 Nitpick | 🔵 Trivial

Consider removing the Any type hint on _pc parameter.

The _pc: Any annotation defeats type checking. Since _pc is captured from the enclosing pc variable (which is _PairContext), the annotation should be _PairContext:

♻️ Suggested improvement
-                    def post_callback(delete_path: str = delete_path, _pc: Any = pc) -> None:
+                    def post_callback(delete_path: str = delete_path, _pc: _PairContext = pc) -> None:
                         _pc.local_scan_process.force_scan()
                         if delete_path != _pc.local_path:
                             _pc.active_scan_process.force_scan()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/python/controller/controller.py` around lines 1222 - 1226, The parameter
annotation on post_callback currently uses _pc: Any even though _pc is captured
from the enclosing pc (an _PairContext); change the annotation to _pc:
_PairContext (i.e. def post_callback(delete_path: str = delete_path, _pc:
_PairContext = pc) -> None) and ensure _PairContext is imported or
forward-referenced (use a direct import or a typing-only import / TYPE_CHECKING
guard or string-forward reference if needed) so the type checker sees the
correct type instead of Any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/python/typings/tblib/pickling_support.pyi`:
- Line 6: Update the type of get_locals in the pickling_support.pyi stub:
replace the restrictive Callable[[Any], dict[str, object]] | None with a plain
Callable[[Any], dict] | None so the signature matches upstream tblib and allows
None/heterogeneous local values; modify the declaration for get_locals
accordingly to preserve the optional None default and keep the symbol name
get_locals unchanged.

---

Outside diff comments:
In `@src/python/controller/controller.py`:
- Around line 1222-1226: The parameter annotation on post_callback currently
uses _pc: Any even though _pc is captured from the enclosing pc (an
_PairContext); change the annotation to _pc: _PairContext (i.e. def
post_callback(delete_path: str = delete_path, _pc: _PairContext = pc) -> None)
and ensure _PairContext is imported or forward-referenced (use a direct import
or a typing-only import / TYPE_CHECKING guard or string-forward reference if
needed) so the type checker sees the correct type instead of Any.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a0a6507e-18be-4e21-b1b1-05f179b56485

📥 Commits

Reviewing files that changed from the base of the PR and between 5bff350 and d478829.

📒 Files selected for processing (7)
  • src/python/controller/controller.py
  • src/python/controller/scan/scanner_process.py
  • src/python/controller/validate/validate_process.py
  • src/python/tests/unittests/test_web/test_serialize/test_serialize_log_record.py
  • src/python/typings/tblib/pickling_support.pyi
  • src/python/web/handler/stream_log.py
  • src/python/web/serialize/serialize_model.py


def install(
*exc_classes_or_instances: type[BaseException] | BaseException,
get_locals: Callable[[Any], dict[str, object]] | None = None,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using plain dict for the get_locals return type to match the upstream library signature.

The current dict[str, object] is more restrictive than necessary—frame locals can contain None values, which object excludes in strict typing (you would need object | None). The previous review comment recommended Callable[[Any], dict] to align with tblib's documented signature. While this doesn't cause issues currently (since the only call site passes no arguments), using plain dict would be more accurate and future-proof.

📝 Suggested adjustment to match upstream signature
-    get_locals: Callable[[Any], dict[str, object]] | None = None,
+    get_locals: Callable[[Any], dict] | None = None,
📝 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
get_locals: Callable[[Any], dict[str, object]] | None = None,
def install(
*exc_classes_or_instances: type[BaseException] | BaseException,
get_locals: Callable[[Any], dict] | None = None,
) -> type[BaseException] | None: ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/python/typings/tblib/pickling_support.pyi` at line 6, Update the type of
get_locals in the pickling_support.pyi stub: replace the restrictive
Callable[[Any], dict[str, object]] | None with a plain Callable[[Any], dict] |
None so the signature matches upstream tblib and allows None/heterogeneous local
values; modify the declaration for get_locals accordingly to preserve the
optional None default and keep the symbol name get_locals unchanged.

nitrobass24 and others added 2 commits March 30, 2026 17:15
Loosen pickling_support.pyi get_locals return type to plain dict to match
upstream tblib. Replace _pc: Any with _pc: _PairContext in post_callback
closure for proper type checking. Remove now-unused Any import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eArgument

Bare dict triggers reportMissingTypeArgument in Pyright strict mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nitrobass24 nitrobass24 merged commit 53127b2 into develop Mar 30, 2026
12 checks passed
@nitrobass24 nitrobass24 deleted the feat/pyright-strict branch March 30, 2026 22:25
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.

1 participant