Skip to content

feat: fine grained filters#199

Open
gazorby wants to merge 2 commits into
mainfrom
feat/fine-grained-filters
Open

feat: fine grained filters#199
gazorby wants to merge 2 commits into
mainfrom
feat/fine-grained-filters

Conversation

@gazorby

@gazorby gazorby commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for custom-apply, fine-grained filter fields with operator restrictions and configurable join strategies.
    • Declared filter fields can now drive virtual/custom filter behavior and folding into the query tree.
    • Filter comparison types are now available from the top-level package.
  • Refactor

    • Streamlined field and mutation configuration APIs to use forwarded keyword arguments.
    • Reworked filter input generation to be metadata-driven for consistent operator exposure.
  • Bug Fixes

    • Improved DTO-to-GraphQL field handling when prebuilt GraphQL field metadata is provided.
  • Tests

    • Added unit and integration coverage for fine-grained/custom filters.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces "fine-grained filter fields" allowing users to annotate GraphQL filter input classes with filter_field() markers that restrict exposed operators or inject custom SELECT-mutating callbacks. Backs this with a metadata-driven _StrawchemyComparison refactor of all comparison classes, a CustomFilter DTO with correlated EXISTS/IN SQL generation in the transpiler, and a unified **field_kwargs: Unpack[...] signature for all public Strawchemy methods.

Changes

Fine-Grained Filter Fields

Layer / File(s) Summary
Metadata-driven comparison infrastructure
src/strawchemy/schema/filters/inputs.py, src/strawchemy/schema/filters/__init__.py, src/strawchemy/schema/filters/geo.py
Introduces Op descriptor, operator sets, comparison_input decorator, and _StrawchemyComparison metadata container replacing __strawchemy_filter__ wiring. Migrates all concrete comparison inputs (equality/order/text/array/date/time/datetime/timedelta), JSON comparisons, and geo comparisons to the new metadata system; re-exports _StrawchemyComparison.
Filter marker contracts and utilities
src/strawchemy/schema/filters/fields.py, src/strawchemy/utils/annotation.py, src/strawchemy/dto/inspectors/sqlalchemy.py
Adds FilterFieldMarker sentinel, CustomFilterApply protocol, JoinStrategy literal/validator; adds get_origin_or_self utility; renames get_field_comparison to get_comparison with subscribed flag for returning unsubscripted types.
DTO support for custom filters
src/strawchemy/dto/strawberry.py, src/strawchemy/dto/backend/strawberry.py
Extends GraphQLFieldDefinition with graphql_field slot; adds CustomFilterFieldDefinition (apply + join) and CustomFilter DTO; expands Filter.and_ to include CustomFilter; updates BooleanFilterDTO.filters_tree to emit custom filters; wires DTO backend to use prebuilt Strawberry fields.
Filter factory declared field parsing and generation
src/strawchemy/schema/factories/inputs.py
Adds parse_declared_filter_fields, validation/restriction/custom-field helpers; overhauls iter_field_definitions to apply marker logic, override emitted fields, validate column mappings, emit custom virtual fields, and strip injected annotations.
Typed field kwargs TypedDicts
src/strawchemy/schema/field.py
Introduces StrawberryFieldKwargs, OutputFieldKwargs, MutationFieldKwargs TypedDicts to group strawberry.field-compatible keyword arguments by context.
*Unified Strawchemy public API via \*\field_kwargs
src/strawchemy/mapper.py
Refactors field, filter_field, create, upsert, update, update_by_ids, delete to accept **field_kwargs: Unpack[...]; adds validation parameter to mutation methods; forwards configuration through kwargs instead of explicit parameters.
Mutation field builder refactoring
src/strawchemy/schema/mutation/field_builder.py
Simplifies MutationFieldBuilder.build to accept resolver + kw-only graphql_type + **field_kwargs; derives configuration from kwargs and forwards via standard builder interface.
Transpiler correlated EXISTS/IN SQL for CustomFilter
src/strawchemy/transpiler/_transpiler.py
Adds _custom_filter_expression generating correlated EXISTS(...) or IN predicate per CustomFilter; extends _gather_conjonctions to route CustomFilter instances through custom predicate builder.
Public comparison exports
src/strawchemy/__init__.py
Re-exports all concrete comparison classes (EqualityComparison, OrderComparison, TextComparison, ArrayComparison, DateComparison, TimeComparison, TimeDeltaComparison, DateTimeComparison, GraphQLComparison) at package level.
Unit tests for fine-grained filtering
tests/unit/test_fine_grained_filters.py
Comprehensive unit suite covering marker construction, restricted() operator selection, CustomFilter participation, declared field parsing, annotation validation, strawberry field kwargs forwarding, filters tree wrapping, definition-time errors, and schema snapshot assertion.
Integration schemas and tests
tests/integration/types/sqlite.py, tests/integration/types/postgres.py, tests/integration/types/mysql.py, tests/integration/test_fine_grained_filters.py
Adds FruitFineGrainedFilter with _fruit_sweeter_than custom apply helpers and fruits_fine_grained query fields across all three DB schemas; adds four integration tests validating custom apply behavior, restricted operator rejection, OR composition, and SQL snapshots.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • gazorby/strawchemy#196: Directly modifies Strawchemy.field(...) configuration forwarding in src/strawchemy/mapper.py (adds order_by_input/default_order_by), overlapping with this PR's unified **field_kwargs refactor of the same public API.

Poem

🐇 Hop hop, the filters grow!
With restricted() ops that glow,
A correlated EXISTS quest,
Custom apply at my behest.
**field_kwargs flows neat and bright —
Fine-grained queries: pure delight! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.95% 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 "feat: fine grained filters" clearly and concisely summarizes the main purpose of the changeset, which adds support for fine-grained filtering throughout the Strawchemy framework.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ 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 feat/fine-grained-filters

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.

@gazorby gazorby changed the title feat/fine grained filters feat: fine grained filters Jun 16, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
src/strawchemy/schema/filters/__init__.py (1)

47-48: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Duplicate entry "FilterProtocol" in __all__.

Line 47 and 48 both contain "FilterProtocol". This is harmless but should be deduplicated.

♻️ Proposed fix
     "EqualityFilter",
     "FilterProtocol",
-    "FilterProtocol",
     "GraphQLComparison",
🤖 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/strawchemy/schema/filters/__init__.py` around lines 47 - 48, The __all__
list in the filters module contains a duplicate entry for "FilterProtocol" on
lines 47 and 48. Remove one of the duplicate "FilterProtocol" entries from the
__all__ list to keep only a single occurrence of each exported symbol.
src/strawchemy/dto/inspectors/sqlalchemy.py (1)

635-659: ⚠️ Potential issue | 🟡 Minor

Update stale reference in docstring to use new method name.

The verification confirms no external callers use the old get_field_comparison name. However, the docstring at line 548 still references the old method name and should be updated to get_comparison.

🤖 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/strawchemy/dto/inspectors/sqlalchemy.py` around lines 635 - 659, The
docstring contains an outdated reference to the old method name
`get_field_comparison`. Locate the docstring that references this old method
name and update it to use the new method name `get_comparison` to keep the
documentation accurate and consistent with the current codebase.
🤖 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/strawchemy/schema/factories/inputs.py`:
- Around line 288-296: The in-place mutation of `base.__annotations__` causes
issues when the same filter class is reused across multiple factory invocations
(common in tests or hot-reload scenarios). Instead of directly mutating the
user's class annotations, either create a shallow copy of the base class before
modifying its annotations, or add a sentinel attribute to the base class to
detect when it has already been processed and skip the annotation mutation on
subsequent invocations. This prevents the "needs a type annotation" error that
occurs on the second run when annotations have been stripped but filter markers
are still present via getmembers.

In `@tests/integration/types/mysql.py`:
- Line 163: The `_fruit_sweeter_than` function signature uses `Any` as the type
annotation for the `**_ctx` parameter, which violates the ANN401 lint rule.
Replace the `Any` type annotation with `object` for the `**_ctx` parameter in
the function signature. This change must be applied to the `_fruit_sweeter_than`
function in all three files where it appears with identical signatures to ensure
consistency across the codebase and satisfy the ANN401 lint rule.

In `@tests/unit/test_fine_grained_filters.py`:
- Line 53: Replace the use of Any-typed parameters and return annotations in
helper callable signatures to comply with Ruff ANN401. The _apply function at
line 53 and similar helper functions at lines 121, 139, and 299 all use Any in
their type annotations. Either define a properly typed callable alias or
protocol that accurately represents these helpers' expected signatures, or add
targeted # noqa: ANN401 comments to each of these function definitions if the
Any typing is intentional and necessary for their flexibility. Ensure all four
locations (the _apply function and the three other helper functions) are
consistently updated with the same approach.

---

Outside diff comments:
In `@src/strawchemy/dto/inspectors/sqlalchemy.py`:
- Around line 635-659: The docstring contains an outdated reference to the old
method name `get_field_comparison`. Locate the docstring that references this
old method name and update it to use the new method name `get_comparison` to
keep the documentation accurate and consistent with the current codebase.

In `@src/strawchemy/schema/filters/__init__.py`:
- Around line 47-48: The __all__ list in the filters module contains a duplicate
entry for "FilterProtocol" on lines 47 and 48. Remove one of the duplicate
"FilterProtocol" entries from the __all__ list to keep only a single occurrence
of each exported symbol.
🪄 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: 546bb4f3-3cee-433a-86f8-ea28c4ff0d1c

📥 Commits

Reviewing files that changed from the base of the PR and between ea50659 and 4f31c34.

⛔ Files ignored due to path filters (2)
  • tests/integration/__snapshots__/test_fine_grained_filters.ambr is excluded by !**/__snapshots__/**
  • tests/unit/__snapshots__/test_fine_grained_filters/test_fine_grained_schema.gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (19)
  • src/strawchemy/__init__.py
  • src/strawchemy/dto/backend/strawberry.py
  • src/strawchemy/dto/inspectors/sqlalchemy.py
  • src/strawchemy/dto/strawberry.py
  • src/strawchemy/mapper.py
  • src/strawchemy/schema/factories/inputs.py
  • src/strawchemy/schema/field.py
  • src/strawchemy/schema/filters/__init__.py
  • src/strawchemy/schema/filters/fields.py
  • src/strawchemy/schema/filters/geo.py
  • src/strawchemy/schema/filters/inputs.py
  • src/strawchemy/schema/mutation/field_builder.py
  • src/strawchemy/transpiler/_transpiler.py
  • src/strawchemy/utils/annotation.py
  • tests/integration/test_fine_grained_filters.py
  • tests/integration/types/mysql.py
  • tests/integration/types/postgres.py
  • tests/integration/types/sqlite.py
  • tests/unit/test_fine_grained_filters.py

Comment on lines +288 to +296
if base is not None and declared_filters:
# Declared filter fields supply only a data type, not a final GraphQL type.
# Drop their annotations from the base so the strawberry backend does not treat
# them as verbatim type overrides; the comparison/custom types injected above win.
base.__annotations__ = {
annotation_name: annotation_type
for annotation_name, annotation_type in inspect.get_annotations(base).items()
if annotation_name not in declared_filters
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mutating base.__annotations__ breaks reuse of the same filter class.

This in-place mutation removes declared filter annotations from the user's class. If the factory is invoked twice with the same base (e.g., in tests or during hot-reload development):

  1. First run: annotations present → works
  2. Second run: annotations already removed, markers still found via getmembers → raises misleading "needs a type annotation" error

Consider operating on a copy or storing processed state to detect re-invocation:

Suggested fix
 if base is not None and declared_filters:
-    base.__annotations__ = {
-        annotation_name: annotation_type
-        for annotation_name, annotation_type in inspect.get_annotations(base).items()
-        if annotation_name not in declared_filters
-    }
+    # Avoid mutating the original class; build a filtered copy for the backend.
+    # The backend receives `base` and merges its annotations, so we create a
+    # subclass with the filtered annotations to preserve the original.
+    filtered_annotations = {
+        k: v for k, v in inspect.get_annotations(base).items() if k not in declared_filters
+    }
+    # Only mutate if this is the first pass (annotations still contain declared fields)
+    if any(name in inspect.get_annotations(base) for name in declared_filters):
+        base.__annotations__ = filtered_annotations

Alternatively, mark the class as processed with a sentinel attribute to detect re-invocation early.

🤖 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/strawchemy/schema/factories/inputs.py` around lines 288 - 296, The
in-place mutation of `base.__annotations__` causes issues when the same filter
class is reused across multiple factory invocations (common in tests or
hot-reload scenarios). Instead of directly mutating the user's class
annotations, either create a shallow copy of the base class before modifying its
annotations, or add a sentinel attribute to the base class to detect when it has
already been processed and skip the annotation mutation on subsequent
invocations. This prevents the "needs a type annotation" error that occurs on
the second run when annotations have been stripped but filter markers are still
present via getmembers.

class FruitFilter: ...


def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view the specific line
cat -n tests/integration/types/mysql.py | sed -n '160,166p'

Repository: gazorby/strawchemy

Length of output: 298


🏁 Script executed:

# Look for lint/flake8 configuration files
fd -i "pyproject\.toml|setup\.cfg|\.flake8|tox\.ini|ruff\.toml" -type f

Repository: gazorby/strawchemy

Length of output: 233


🏁 Script executed:

# Search for similar patterns with **_ctx: Any in the codebase
rg "\*\*_ctx:\s*Any" -type py

Repository: gazorby/strawchemy

Length of output: 90


🏁 Script executed:

# Check if there are imports of Any in the file
head -20 tests/integration/types/mysql.py | cat -n

Repository: gazorby/strawchemy

Length of output: 745


🏁 Script executed:

# Search for all lint/flake8 configuration files more broadly
find . -maxdepth 3 -name "pyproject.toml" -o -name "setup.cfg" -o -name ".flake8" -o -name "ruff.toml" | head -20

Repository: gazorby/strawchemy

Length of output: 125


🏁 Script executed:

# Look for rg with correct syntax - search for similar patterns
rg "\*\*_ctx:\s*Any" --max-count 20

Repository: gazorby/strawchemy

Length of output: 810


🏁 Script executed:

# Check if there are other instances with Any in function signatures
rg "def\s+\w+\([^)]*\*\*.*:\s*Any" --max-count 20

Repository: gazorby/strawchemy

Length of output: 2607


🏁 Script executed:

# Search for pyproject.toml in root
cat pyproject.toml | head -80

Repository: gazorby/strawchemy

Length of output: 2469


🏁 Script executed:

# Check the ruff.toml configuration
cat ruff.toml

Repository: gazorby/strawchemy

Length of output: 2525


🏁 Script executed:

# Search for the context around line 163 in the sqlite and postgres files to understand if they're mirror files
head -20 tests/integration/types/sqlite.py && echo "---" && head -20 tests/integration/types/postgres.py

Repository: gazorby/strawchemy

Length of output: 1148


🏁 Script executed:

# Look for any ignore patterns or comments that might explain the Any usage
rg "ANN401|noqa.*ANN" --max-count 10

Repository: gazorby/strawchemy

Length of output: 643


🌐 Web query:

ANN401 Flake8 annotations rule - what does it require for **kwargs typing

💡 Result:

The ANN401 rule in flake8-annotations (and its implementation in Ruff) is designed to disallow the use of typing.Any in function argument annotations [1][2][3]. By default, this rule applies to all function arguments, including *args and kwargs [4][5]. If you use kwargs: Any, the rule will flag it as a violation [4]. However, you can configure the linter to suppress this behavior for variadic arguments [1][2][3]. To allow kwargs: Any (and *args: Any), you must enable the --allow-star-arg-any option [5][6]. Configuration details: - For flake8-annotations: Set --allow-star-arg-any=True in your flake8 configuration file (e.g., setup.cfg, tox.ini, or.flake8) [1][5]. - For Ruff: Use the corresponding configuration in your pyproject.toml: [tool.ruff.flake8-annotations] allow-star-arg-any = true [7] When this option is set to True, the linter will ignore ANN401 violations specifically for *args and kwargs, allowing you to use typing.Any for those parameters without triggering the error [1][5][8].

Citations:


🏁 Script executed:

# Check if allow-star-arg-any is configured in ruff.toml
rg "allow-star-arg-any" ruff.toml

Repository: gazorby/strawchemy

Length of output: 44


🏁 Script executed:

# Verify the exact context of line 163 and if _ctx is actually used in the function
sed -n '163,170p' tests/integration/types/mysql.py | cat -n

Repository: gazorby/strawchemy

Length of output: 550


🏁 Script executed:

# Check if the function signature pattern needs **_ctx at all or if it's unused
ast-grep --pattern 'def _fruit_sweeter_than($_, $_, **_ctx: $_) -> $_ {
  $$$
}'

Repository: gazorby/strawchemy

Length of output: 44


🌐 Web query:

python typing.Any vs object for **kwargs type annotation best practice

💡 Result:

When annotating kwargs in Python, the choice between Any and object depends on your intent regarding type safety, while modern practices prefer using Unpack with a TypedDict for precise annotations [1][2][3]. 1. Any vs. object - typing.Any: This is a special type that tells static type checkers to disengage. It essentially says, "I don't know the type, and I don't want you to check it." A type checker allows almost any operation on an Any value [1][4][5]. - object: This is a concrete type that represents all possible Python objects. It is type-safe; if you annotate kwargs as object, the type checker will restrict you to only those operations that are valid for all objects (which are very few). It is an excellent choice if you truly mean that the arguments can be anything but you want to maintain type safety [1][6][4]. 2. Best Practices - Precise Typing (Preferred): If you know the expected keyword arguments, the modern standard is to use the Unpack operator with a TypedDict [1][2][7]. This allows you to specify the exact types of the keys and values in the kwargs dictionary, providing full static analysis benefits [2][3]. Example of modern best practice: from typing import TypedDict, Unpack class Options(TypedDict): name: str year: int def my_func(kwargs: Unpack[Options]):... - Fallback for Pass-through: If you are writing a wrapper function that passes kwargs to another function, and you do not wish to (or cannot) define the exact signature, using object is generally considered better practice than Any if you want to avoid disabling type checking entirely [6]. However, Any is often used in practice for simplicity when the complexity of defining a precise interface outweighs the benefit [8]. - When to use which: - Use TypedDict + Unpack: When you want precise, checked, and documented keyword arguments [2][3]. - Use object: When you want to be as permissive as possible while still maintaining type safety, effectively declaring that your code does not depend on specific attributes of the arguments [6][4]. - Use Any: When you need an "escape hatch" because your code is highly dynamic or "magical," and you need the type checker to stop flagging errors [4][9].

Citations:


🏁 Script executed:

# Check if there are other functions in the same files that accept similar kwargs
rg "def\s+\w+\([^)]*\*\*" tests/integration/types/mysql.py -A 2

Repository: gazorby/strawchemy

Length of output: 429


🏁 Script executed:

# Check the strawberry filter API to understand if _ctx is expected
rg "apply=" tests/integration/types/mysql.py -B 2 -A 2

Repository: gazorby/strawchemy

Length of output: 329


🏁 Script executed:

# Check if there are any similar patterns with noqa comments
rg "\*\*.*:\s*Any.*noqa" --max-count 10

Repository: gazorby/strawchemy

Length of output: 44


🏁 Script executed:

# Check the strawchemy filter API signature to see if **_ctx is required
rg "def filter_field" -A 3 src/ | head -30

Repository: gazorby/strawchemy

Length of output: 251


🏁 Script executed:

# Check if removing **_ctx altogether would work (look for other filter functions without it)
rg "def _\w+\(statement:" tests/integration/types/mysql.py -A 2

Repository: gazorby/strawchemy

Length of output: 223


🏁 Script executed:

# Check git blame or recent changes to see why **_ctx was added
git log -p --follow -S "**_ctx: Any" -- tests/integration/types/mysql.py | head -50

Repository: gazorby/strawchemy

Length of output: 44


🏁 Script executed:

# Check if there are other instances of **_ctx: Any in the same three mirror files
rg "def _fruit_sweeter_than" tests/integration/types/ --max-count 5

Repository: gazorby/strawchemy

Length of output: 486


🏁 Script executed:

# Verify that replacing Any with object would satisfy ANN401
# Check documentation or similar patterns using object
rg "\*\*.*:\s*object" --max-count 10

Repository: gazorby/strawchemy

Length of output: 144


Replace Any with object in **_ctx parameter to satisfy ANN401 lint rule.

The **_ctx: Any parameter violates the active ANN401 rule. Replacing with object maintains type safety while allowing the function to accept arbitrary keyword arguments without disabling type checking.

Note: This same function exists in tests/integration/types/sqlite.py and tests/integration/types/postgres.py with identical signatures and should be updated similarly.

Suggested patch
-def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
+def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
📝 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 _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 163-163: Dynamically typed expressions (typing.Any) are disallowed in **_ctx

(ANN401)

🤖 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/integration/types/mysql.py` at line 163, The `_fruit_sweeter_than`
function signature uses `Any` as the type annotation for the `**_ctx` parameter,
which violates the ANN401 lint rule. Replace the `Any` type annotation with
`object` for the `**_ctx` parameter in the function signature. This change must
be applied to the `_fruit_sweeter_than` function in all three files where it
appears with identical signatures to ensure consistency across the codebase and
satisfy the ANN401 lint rule.

Source: Linters/SAST tools



def test_filter_field_with_apply_captures_callable_and_join() -> None:
def _apply(statement: Any, *_args: Any, **_kwargs: Any) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace Any-typed helper signatures to satisfy ANN401.

At Line 53, Line 121, Line 139, and Line 299, helper callables use Any in parameter/return annotations and currently trigger Ruff ANN401. This can fail lint depending on repo settings; prefer a typed callable alias/protocol (or targeted # noqa: ANN401 where intentional).

Also applies to: 121-121, 139-139, 299-299

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 53-53: Dynamically typed expressions (typing.Any) are disallowed in statement

(ANN401)


[warning] 53-53: Dynamically typed expressions (typing.Any) are disallowed in *_args

(ANN401)


[warning] 53-53: Dynamically typed expressions (typing.Any) are disallowed in **_kwargs

(ANN401)


[warning] 53-53: Dynamically typed expressions (typing.Any) are disallowed in _apply

(ANN401)

🤖 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/unit/test_fine_grained_filters.py` at line 53, Replace the use of
Any-typed parameters and return annotations in helper callable signatures to
comply with Ruff ANN401. The _apply function at line 53 and similar helper
functions at lines 121, 139, and 299 all use Any in their type annotations.
Either define a properly typed callable alias or protocol that accurately
represents these helpers' expected signatures, or add targeted # noqa: ANN401
comments to each of these function definitions if the Any typing is intentional
and necessary for their flexibility. Ensure all four locations (the _apply
function and the three other helper functions) are consistently updated with the
same approach.

Source: Linters/SAST tools

@gazorby gazorby force-pushed the feat/fine-grained-filters branch from 4f31c34 to 951e32b Compare June 17, 2026 07:11
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.86207% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.33%. Comparing base (227bc21) to head (951e32b).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/strawchemy/schema/factories/inputs.py 92.06% 3 Missing and 2 partials ⚠️
src/strawchemy/schema/filters/inputs.py 97.54% 1 Missing and 2 partials ⚠️
src/strawchemy/schema/filters/geo.py 0.00% 2 Missing ⚠️
src/strawchemy/transpiler/_transpiler.py 90.90% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #199      +/-   ##
==========================================
+ Coverage   93.26%   93.33%   +0.07%     
==========================================
  Files          69       70       +1     
  Lines        6338     6540     +202     
  Branches      853      888      +35     
==========================================
+ Hits         5911     6104     +193     
- Misses        285      290       +5     
- Partials      142      146       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/strawchemy/schema/field.py`:
- Around line 95-98: In the OutputFieldKwargs class, the type annotations for
permission_classes (line 95) and extensions (line 98) are currently using
list[...] which is narrower than the Sequence[...] types accepted by
StrawchemyField.__init__ (referenced at lines 153 and 159). Change both
permission_classes and extensions type annotations from list[...] to
Sequence[...] to match the actual parameter types accepted by
StrawchemyField.__init__ and preserve compatibility with tuple-based call sites
during static type checking.

In `@tests/integration/types/postgres.py`:
- Line 171: The type annotation `**_ctx: Any` in the function
_fruit_sweeter_than violates Ruff's ANN401 rule which disallows dynamically
typed expressions in kwargs. Replace the `Any` type annotation with `object` for
the _ctx parameter to satisfy the linting rule while maintaining the same
functionality.

In `@tests/integration/types/sqlite.py`:
- Line 162: The function _fruit_sweeter_than has a kwargs parameter type
annotation using Any, which violates ANN401. Replace the type annotation in the
**_ctx parameter from Any to object to comply with the codebase convention and
ANN401 requirements.
🪄 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: 151e96cc-b631-4337-b4c2-7247a0c1d03a

📥 Commits

Reviewing files that changed from the base of the PR and between 4f31c34 and 951e32b.

⛔ Files ignored due to path filters (2)
  • tests/integration/__snapshots__/test_fine_grained_filters.ambr is excluded by !**/__snapshots__/**
  • tests/unit/__snapshots__/test_fine_grained_filters/test_fine_grained_schema.gql is excluded by !**/__snapshots__/**
📒 Files selected for processing (19)
  • src/strawchemy/__init__.py
  • src/strawchemy/dto/backend/strawberry.py
  • src/strawchemy/dto/inspectors/sqlalchemy.py
  • src/strawchemy/dto/strawberry.py
  • src/strawchemy/mapper.py
  • src/strawchemy/schema/factories/inputs.py
  • src/strawchemy/schema/field.py
  • src/strawchemy/schema/filters/__init__.py
  • src/strawchemy/schema/filters/fields.py
  • src/strawchemy/schema/filters/geo.py
  • src/strawchemy/schema/filters/inputs.py
  • src/strawchemy/schema/mutation/field_builder.py
  • src/strawchemy/transpiler/_transpiler.py
  • src/strawchemy/utils/annotation.py
  • tests/integration/test_fine_grained_filters.py
  • tests/integration/types/mysql.py
  • tests/integration/types/postgres.py
  • tests/integration/types/sqlite.py
  • tests/unit/test_fine_grained_filters.py

Comment on lines +95 to +98
permission_classes: list[type[BasePermission]] | None
default: Any
default_factory: Callable[..., object] | object
extensions: list[FieldExtension] | None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Broaden OutputFieldKwargs collection types to preserve typed API compatibility.

Line 95 and Line 98 currently require list[...], but StrawchemyField.__init__ accepts Sequence[...] (Line 153 and Line 159). This narrows the public typed API and can break existing tuple-based call sites in static type checking.

Proposed fix
 class OutputFieldKwargs(StrawberryFieldKwargs, total=False):
@@
-    permission_classes: list[type[BasePermission]] | None
+    permission_classes: Sequence[type[BasePermission]] | None
@@
-    extensions: list[FieldExtension] | None
+    extensions: Sequence[FieldExtension] | None
🤖 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/strawchemy/schema/field.py` around lines 95 - 98, In the
OutputFieldKwargs class, the type annotations for permission_classes (line 95)
and extensions (line 98) are currently using list[...] which is narrower than
the Sequence[...] types accepted by StrawchemyField.__init__ (referenced at
lines 153 and 159). Change both permission_classes and extensions type
annotations from list[...] to Sequence[...] to match the actual parameter types
accepted by StrawchemyField.__init__ and preserve compatibility with tuple-based
call sites during static type checking.

class FruitFilter: ...


def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ruff check tests/integration/types/postgres.py --select ANN401

Repository: gazorby/strawchemy

Length of output: 588


🏁 Script executed:

#!/bin/bash
# Check if changing Any to object resolves ANN401
# First, show the current problematic line
echo "=== Current code (with ANN401 error) ===" 
sed -n '171p' tests/integration/types/postgres.py

# Now simulate the fix by creating a test file and checking it
cat > /tmp/test_fix.py << 'EOF'
from typing import Any
from sqlalchemy import Select

class Fruit:
    pass

# Original (will fail)
def _fruit_sweeter_than_old(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
    return statement.where(Fruit.sweetness >= value)

# Proposed fix
def _fruit_sweeter_than_new(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
    return statement.where(Fruit.sweetness >= value)
EOF

echo -e "\n=== Checking with object type ===" 
ruff check /tmp/test_fix.py --select ANN401

Repository: gazorby/strawchemy

Length of output: 786


🏁 Script executed:

#!/bin/bash
# Verify the fix more clearly by checking only the fixed function
cat > /tmp/test_fix_only.py << 'EOF'
from sqlalchemy import Select

class Fruit:
    pass

# Proposed fix - should have NO ANN401 error
def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
    return statement.where(Fruit.sweetness >= value)
EOF

echo "=== Checking fixed version (object instead of Any) ===" 
ruff check /tmp/test_fix_only.py --select ANN401

Repository: gazorby/strawchemy

Length of output: 147


Replace Any in **_ctx with object to satisfy ANN401.

Line 171 uses **_ctx: Any, which violates Ruff's ANN401 rule (dynamically typed expressions are disallowed in kwargs). Change the type annotation to object:

Suggested patch
-def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
+def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
📝 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 _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 171-171: Dynamically typed expressions (typing.Any) are disallowed in **_ctx

(ANN401)

🤖 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/integration/types/postgres.py` at line 171, The type annotation
`**_ctx: Any` in the function _fruit_sweeter_than violates Ruff's ANN401 rule
which disallows dynamically typed expressions in kwargs. Replace the `Any` type
annotation with `object` for the _ctx parameter to satisfy the linting rule
while maintaining the same functionality.

Source: Linters/SAST tools

class FruitFilter: ...


def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ruff check tests/integration/types/sqlite.py --select ANN401

Repository: gazorby/strawchemy

Length of output: 586


🏁 Script executed:

#!/bin/bash
# First, let's verify the suggested patch would fix the ANN401 issue
# We'll create a test to check if object type passes the check

# Search for similar **kwargs patterns in integration tests
rg '\*\*.*:\s*(Any|object)' tests/integration/types/ -A 2 -B 2

Repository: gazorby/strawchemy

Length of output: 3386


🏁 Script executed:

#!/bin/bash
# Let's check what the parameter is actually used for in the function
sed -n '162,170p' tests/integration/types/sqlite.py

Repository: gazorby/strawchemy

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Test if the suggested patch with object type would pass ANN401
cat > /tmp/test_patch.py << 'EOF'
from typing import Any
from sqlalchemy import Select

# Original (violates ANN401)
# def _fruit_sweeter_than(statement: Select[tuple], value: int, **_ctx: Any) -> Select[tuple]:

# Suggested fix
def _fruit_sweeter_than(statement: Select[tuple], value: int, **_ctx: object) -> Select[tuple]:
    return statement
EOF

# Check if object type satisfies ANN401
python3 << 'PY'
import subprocess
result = subprocess.run(
    ['rg', '\\*\\*_ctx:', 'tests/integration/types/'],
    capture_output=True,
    text=True
)
print("Current instances of **_ctx:")
print(result.stdout)
print("\nTotal occurrences:", len(result.stdout.strip().split('\n')) if result.stdout.strip() else 0)
PY

Repository: gazorby/strawchemy

Length of output: 538


🏁 Script executed:

#!/bin/bash
# Check if there are any other ANN401 violations in the file
ruff check tests/integration/types/sqlite.py --select ANN

Repository: gazorby/strawchemy

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Check if there's a ruff configuration that specifies allowed types for kwargs
find . -name "ruff.toml" -o -name "pyproject.toml" -o -name ".ruff.toml" | head -5

Repository: gazorby/strawchemy

Length of output: 125


🏁 Script executed:

#!/bin/bash
# Let's verify that the object type would actually fix the ANN401 violation
# by checking ruff documentation or testing locally

# Check the pyproject.toml for any ANN configuration
cat pyproject.toml | grep -A 10 "^\[tool.ruff" | head -30

Repository: gazorby/strawchemy

Length of output: 898


🏁 Script executed:

#!/bin/bash
# Check the ruff.toml file for ANN configuration
cat ruff.toml

Repository: gazorby/strawchemy

Length of output: 2525


🏁 Script executed:

#!/bin/bash
# Verify the suggested fix by checking if object type passes ANN401
# Let's search for similar patterns in the codebase that use object instead of Any for **kwargs
rg '\*\*.*:\s*object' --type py

Repository: gazorby/strawchemy

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Let's verify the fix by checking what happens when we use object instead of Any
# We'll look at the test file that uses object pattern for reference
cat tests/unit/mapping/test_model_field.py | grep -A 5 "_make_class"

Repository: gazorby/strawchemy

Length of output: 1740


🏁 Script executed:

#!/bin/bash
# Now let's verify that using object would actually pass the ANN401 check
# by looking at the parameter usage in the function
sed -n '162,165p' tests/integration/types/sqlite.py | cat -A

Repository: gazorby/strawchemy

Length of output: 228


🏁 Script executed:

#!/bin/bash
# Check if postgres.py and mysql.py have the same issue
ruff check tests/integration/types/postgres.py --select ANN401
ruff check tests/integration/types/mysql.py --select ANN401

Repository: gazorby/strawchemy

Length of output: 1111


Use a non-Any kwargs type in _fruit_sweeter_than.

Line 162 uses **_ctx: Any, which violates ANN401. Change to **_ctx: object to match the pattern used elsewhere in the codebase.

Suggested patch
-def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
+def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
📝 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 _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: Any) -> Select[tuple[Fruit]]:
def _fruit_sweeter_than(statement: Select[tuple[Fruit]], value: int, **_ctx: object) -> Select[tuple[Fruit]]:
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 162-162: Dynamically typed expressions (typing.Any) are disallowed in **_ctx

(ANN401)

🤖 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/integration/types/sqlite.py` at line 162, The function
_fruit_sweeter_than has a kwargs parameter type annotation using Any, which
violates ANN401. Replace the type annotation in the **_ctx parameter from Any to
object to comply with the codebase convention and ANN401 requirements.

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.

1 participant