Skip to content

feat(sheets): add data validation tools (set/clear + read dropdowns)#892

Open
emilio-S03 wants to merge 2 commits into
taylorwilsdon:mainfrom
emilio-S03:feat/sheets-data-validation
Open

feat(sheets): add data validation tools (set/clear + read dropdowns)#892
emilio-S03 wants to merge 2 commits into
taylorwilsdon:mainfrom
emilio-S03:feat/sheets-data-validation

Conversation

@emilio-S03

@emilio-S03 emilio-S03 commented Jun 29, 2026

Copy link
Copy Markdown

What

Adds two complementary Google Sheets data-validation (dropdown) tools:

  • manage_data_validation — set or clear validation on a range
  • read_data_validation — read back the validation rule on each cell (read-only)

Why

The server can format cells (format_sheet_range) and manage conditional formatting (manage_conditional_formatting), but there was no way to add a data-validation dropdown, nor to read one back to verify it. These fill that gap and pair naturally: set a dropdown, then confirm it.

Behavior

manage_data_validation (write)

  • action="set" + values=[...] → fixed-list dropdown (ONE_OF_LIST)
  • action="set" + source_range="Lists!A1:A20" → range-sourced dropdown (ONE_OF_RANGE; a leading = is added if omitted)
  • action="clear" → removes validation on the range
  • Options: strict (reject vs. warn), show_dropdown (the chip / showCustomUi), input_message

read_data_validation (read-only)

  • Inspects a range and returns, for each cell that has a rule: A1 address, condition type, the list options or source range, strict, and whether the dropdown chip is shown.

Implementation notes

  • Mirrors existing patterns: split _impl + decorated tool, _parse_a1_range / _format_a1_cell helpers, asyncio.to_thread(...).
  • read_data_validation uses spreadsheets.get with fields=…dataValidation… (sheets read scope only); manage_data_validation uses the existing sheets write scope. No new OAuth scopes.
  • Both registered under sheetscomplete tier in core/tool_tiers.yaml.
  • Adds tests/gsheets/test_manage_data_validation.py and tests/gsheets/test_read_data_validation.py.

Testing

uv run pytest tests/gsheets/test_manage_data_validation.py tests/gsheets/test_read_data_validation.py → 7 passed.

Summary by CodeRabbit

  • New Features

    • Added support for managing Google Sheets dropdown/data-validation rules.
    • Added a way to inspect existing validation rules on cells, including type, allowed values, and settings.
  • Bug Fixes

    • Improved handling of validation inputs so only valid combinations are accepted.
    • Clearer cell-level reporting when checking validation across a range.
  • Tests

    • Added coverage for creating, clearing, and reading validation rules.

Adds a Google Sheets tool to set or clear data validation on a range:
- ONE_OF_LIST dropdown from an explicit list of values
- ONE_OF_RANGE dropdown sourced from another A1 range
- clear validation via action="clear"

Supports strict/warn enforcement, the dropdown chip (showCustomUi), and an
optional input message. Mirrors the existing format_sheet_range /
manage_conditional_formatting pattern. Registered under the sheets "complete"
tier and uses the existing sheets write scope (no new scope). Includes unit tests.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18d80abe-a0f3-4021-bccc-88863c26da42

📥 Commits

Reviewing files that changed from the base of the PR and between 6c10b40 and 913d93a.

📒 Files selected for processing (3)
  • core/tool_tiers.yaml
  • gsheets/sheets_tools.py
  • tests/gsheets/test_read_data_validation.py
✅ Files skipped from review due to trivial changes (1)
  • core/tool_tiers.yaml

📝 Walkthrough

Walkthrough

Adds Sheets tools for setting, clearing, and reading data validation rules, registers them in the Sheets tool tier, and adds async tests covering both tool paths.

Changes

Sheets data validation tools

Layer / File(s) Summary
Manage data validation
gsheets/sheets_tools.py, core/tool_tiers.yaml
Adds the manage-data-validation helper and MCP tool for ONE_OF_LIST, ONE_OF_RANGE, and clear actions, plus tier registration.
Read data validation
gsheets/sheets_tools.py
Adds the data-validation inspection helper and MCP tool that return per-cell validation details with A1 addresses.
Tests for data validation tools
tests/gsheets/test_manage_data_validation.py, tests/gsheets/test_read_data_validation.py
Adds async coverage for request construction, validation errors, address formatting, and empty/mixed validation reads.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

enhancement

Poem

🐇 I hop through sheets with dropdown cheer,
New validation rules appear,
Set, read, or clear in flight,
Cells stay tidy, crisp, and bright,
Hop hop—your sheet feels just right!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits required template sections like Type of Change, Checklist, and Additional Notes. Add the missing template sections, especially Type of Change, Checklist (including Allow edits from maintainers), and Additional Notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding Google Sheets data-validation tools.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gsheets/sheets_tools.py`:
- Around line 2459-2494: The dropdown rule builder in the set/clear validation
flow accepts a whitespace-only source_range and only fails later when building
the ONE_OF_RANGE condition. In the function that normalizes action_normalized
and constructs dv_request/condition, reject blank source_range values up front
(after stripping) with a UserInputError before calling _parse_a1_range or
creating the userEnteredValue. Keep the existing exclusivity check for values vs
source_range, but add a validation path that treats empty or whitespace-only
source_range as invalid input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d153d14-c775-4704-ba34-5a448cf50d18

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1d145 and 6c10b40.

📒 Files selected for processing (3)
  • core/tool_tiers.yaml
  • gsheets/sheets_tools.py
  • tests/gsheets/test_manage_data_validation.py

Comment thread gsheets/sheets_tools.py
Comment on lines +2459 to +2494
if action_normalized == "set" and bool(values) == bool(source_range):
raise UserInputError(
"For action='set', provide exactly one of 'values' (a list of dropdown "
"options) or 'source_range' (an A1 range to source the options from)."
)

metadata = await asyncio.to_thread(
service.spreadsheets()
.get(
spreadsheetId=spreadsheet_id,
fields="sheets(properties(sheetId,title))",
)
.execute
)
sheets = metadata.get("sheets", [])
grid_range = _parse_a1_range(range_name, sheets)

if action_normalized == "clear":
# Omitting 'rule' from setDataValidation clears validation on the range.
dv_request = {"range": grid_range}
summary = "cleared data validation"
else:
if values:
condition = {
"type": "ONE_OF_LIST",
"values": [{"userEnteredValue": str(v)} for v in values],
}
summary = f"set a dropdown with {len(values)} option(s)"
else:
ref = source_range.strip()
if not ref.startswith("="):
ref = "=" + ref
condition = {
"type": "ONE_OF_RANGE",
"values": [{"userEnteredValue": ref}],
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject blank source_range values before building the rule.

A whitespace-only source_range passes the Lines 2459-2463 exclusivity check, gets stripped to "" on Lines 2488-2490, and is then sent as userEnteredValue: "=". That turns a local input mistake into a Sheets 400 instead of a UserInputError.

Suggested fix
 async def _manage_data_validation_impl(
     service,
     spreadsheet_id: str,
     range_name: str,
@@
 ) -> dict:
     """Internal implementation for manage_data_validation (decorator-free for tests)."""
     action_normalized = (action or "").strip().lower()
+    normalized_source_range = (
+        source_range.strip() if source_range is not None else None
+    )
+
     if action_normalized not in ("set", "clear"):
         raise UserInputError("action must be 'set' or 'clear'.")
 
-    if action_normalized == "set" and bool(values) == bool(source_range):
+    if action_normalized == "set" and bool(values) == bool(normalized_source_range):
         raise UserInputError(
             "For action='set', provide exactly one of 'values' (a list of dropdown "
             "options) or 'source_range' (an A1 range to source the options from)."
         )
@@
         if values:
             condition = {
                 "type": "ONE_OF_LIST",
                 "values": [{"userEnteredValue": str(v)} for v in values],
             }
             summary = f"set a dropdown with {len(values)} option(s)"
         else:
-            ref = source_range.strip()
+            ref = normalized_source_range
+            if ref in ("", "="):
+                raise UserInputError(
+                    "For action='set', 'source_range' must be a non-empty A1 range."
+                )
             if not ref.startswith("="):
                 ref = "=" + ref
📝 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
if action_normalized == "set" and bool(values) == bool(source_range):
raise UserInputError(
"For action='set', provide exactly one of 'values' (a list of dropdown "
"options) or 'source_range' (an A1 range to source the options from)."
)
metadata = await asyncio.to_thread(
service.spreadsheets()
.get(
spreadsheetId=spreadsheet_id,
fields="sheets(properties(sheetId,title))",
)
.execute
)
sheets = metadata.get("sheets", [])
grid_range = _parse_a1_range(range_name, sheets)
if action_normalized == "clear":
# Omitting 'rule' from setDataValidation clears validation on the range.
dv_request = {"range": grid_range}
summary = "cleared data validation"
else:
if values:
condition = {
"type": "ONE_OF_LIST",
"values": [{"userEnteredValue": str(v)} for v in values],
}
summary = f"set a dropdown with {len(values)} option(s)"
else:
ref = source_range.strip()
if not ref.startswith("="):
ref = "=" + ref
condition = {
"type": "ONE_OF_RANGE",
"values": [{"userEnteredValue": ref}],
}
action_normalized = (action or "").strip().lower()
normalized_source_range = (
source_range.strip() if source_range is not None else None
)
if action_normalized not in ("set", "clear"):
raise UserInputError("action must be 'set' or 'clear'.")
if action_normalized == "set" and bool(values) == bool(normalized_source_range):
raise UserInputError(
"For action='set', provide exactly one of 'values' (a list of dropdown "
"options) or 'source_range' (an A1 range to source the options from)."
)
metadata = await asyncio.to_thread(
service.spreadsheets()
.get(
spreadsheetId=spreadsheet_id,
fields="sheets(properties(sheetId,title))",
)
.execute
)
sheets = metadata.get("sheets", [])
grid_range = _parse_a1_range(range_name, sheets)
if action_normalized == "clear":
# Omitting 'rule' from setDataValidation clears validation on the range.
dv_request = {"range": grid_range}
summary = "cleared data validation"
else:
if values:
condition = {
"type": "ONE_OF_LIST",
"values": [{"userEnteredValue": str(v)} for v in values],
}
summary = f"set a dropdown with {len(values)} option(s)"
else:
ref = normalized_source_range
if ref in ("", "="):
raise UserInputError(
"For action='set', 'source_range' must be a non-empty A1 range."
)
if not ref.startswith("="):
ref = "=" + ref
condition = {
"type": "ONE_OF_RANGE",
"values": [{"userEnteredValue": ref}],
}
🤖 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 `@gsheets/sheets_tools.py` around lines 2459 - 2494, The dropdown rule builder
in the set/clear validation flow accepts a whitespace-only source_range and only
fails later when building the ONE_OF_RANGE condition. In the function that
normalizes action_normalized and constructs dv_request/condition, reject blank
source_range values up front (after stripping) with a UserInputError before
calling _parse_a1_range or creating the userEnteredValue. Keep the existing
exclusivity check for values vs source_range, but add a validation path that
treats empty or whitespace-only source_range as invalid input.

Adds a read-only companion to manage_data_validation. read_data_validation
inspects a range and reports the data validation rule on each cell that has one:
condition type, list options or source range, strict, and whether the dropdown
chip is shown. Uses spreadsheets.get with the dataValidation field (sheets read
scope only). Includes unit tests.
@emilio-S03 emilio-S03 changed the title feat(sheets): add manage_data_validation tool (dropdowns) feat(sheets): add data validation tools (set/clear + read dropdowns) Jun 29, 2026
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