Skip to content

⚙️ FEATURE-#40: Use Optional[bool] for Query flags to remove ambiguity - #60

Merged
FernandoCelmer merged 2 commits into
developfrom
feature/40
Apr 16, 2026
Merged

FernandoCelmer merged 2 commits into
developfrom
feature/40

Conversation

@FernandoCelmer

Copy link
Copy Markdown
Member

Summary

  • Changed unseen field in Query model from bool = False to Optional[bool] = None, matching the other flag fields
  • Updated _flag_clauses() to use explicit is True / is False checks for unseen, mapping unseen=False to (SEEN) and unseen=True to (UNSEEN)
  • unseen=None (default) now correctly means "no filter", removing the ambiguity where Query(unseen=False) was indistinguishable from the default

Closes #40

Test plan

  • All 149 existing tests pass
  • Verify Query(unseen=True).mount() returns (UNSEEN)
  • Verify Query(unseen=False).mount() returns (SEEN)
  • Verify Query().mount() returns ALL (no filter applied)

@FernandoCelmer FernandoCelmer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🔍 Code Review

Code issues found: 2

See inline comments below.

deleted: Optional[bool] = None
draft: Optional[bool] = None

unseen: bool = False

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Blocking]

Problem — This is a silent breaking change for existing consumers. Previously, Query(unseen=False) was the default and meant "no unseen filter" — the _flag_clauses method only checked if self.unseen: which was falsy for False. After this PR, Query(unseen=False) now emits (SEEN), actively filtering for seen-only messages.

Failure scenario

# Existing user code (pre-PR behavior):
q = Query(subject="report", unseen=False)
# Before: searches ALL emails with subject 'report'
# After:  searches ONLY SEEN emails with subject 'report'
#         -> silently drops unread emails from results!

Since the library is at version 1.0.0.dev1, there may not be many external callers yet, but this still changes documented behavior without a deprecation path.

Fix — Document this as a breaking change in the PR description and changelog. Consider if the default change from False to None is sufficient, or if unseen=False should remain a no-op for backward compatibility.

@@ -243,8 +243,10 @@ def _flag_clauses(self) -> list[str]:
parts.append(f"({name})")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Suggestion]

ProblemQuery(seen=True, unseen=True) now produces both (SEEN) and (UNSEEN) in the IMAP search command — a contradictory query that will return zero results on most servers.

The unseen field is the logical inverse of the seen field, but both can be set independently. There's also redundancy: Query(unseen=False) and Query(seen=True) now produce identical IMAP queries.

Fix — Add a Pydantic model_validator to reject contradictory combinations:

@model_validator(mode='after')
def _check_seen_unseen(self) -> 'Query':
    if self.seen is not None and self.unseen is not None:
        raise ValueError(
            'Cannot set both seen and unseen. Use seen=True or unseen=True, not both.'
        )
    return self

@FernandoCelmer
FernandoCelmer merged commit deda300 into develop Apr 16, 2026
11 checks passed
@FernandoCelmer
FernandoCelmer deleted the feature/40 branch April 16, 2026 22:04
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