mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Merge pull request #17711 from BerriAI/litellm_add_additional_drop_params_support
feat: Add nested field removal support to additional_drop_params
This commit is contained in:
commit
0d2f8ce931
6 changed files with 613 additions and 3 deletions
|
|
@ -117,6 +117,56 @@ response = litellm.completion(
|
|||
|
||||
**additional_drop_params**: List or null - Is a list of openai params you want to drop when making a call to the model.
|
||||
|
||||
### Nested Field Removal
|
||||
|
||||
Drop nested fields within complex objects using JSONPath-like notation:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="sdk" label="SDK">
|
||||
|
||||
```python
|
||||
import litellm
|
||||
|
||||
response = litellm.completion(
|
||||
model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
tools=[{
|
||||
"name": "search",
|
||||
"description": "Search files",
|
||||
"input_schema": {"type": "object", "properties": {"query": {"type": "string"}}},
|
||||
"input_examples": [{"query": "test"}] # Will be removed
|
||||
}],
|
||||
additional_drop_params=["tools[*].input_examples"] # Remove from all tools
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="proxy" label="PROXY">
|
||||
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: my-bedrock-model
|
||||
litellm_params:
|
||||
model: bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
additional_drop_params: ["tools[*].input_examples"] # Remove from all tools
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
**Supported syntax:**
|
||||
- `field` - Top-level field
|
||||
- `parent.child` - Nested object field
|
||||
- `array[*]` - All array elements
|
||||
- `array[0]` - Specific array index
|
||||
- `tools[*].input_examples` - Field in all array elements
|
||||
- `tools[0].metadata.field` - Specific index + nested field
|
||||
|
||||
**Example use cases:**
|
||||
- Remove `input_examples` from tool definitions (Claude Code + AWS Bedrock)
|
||||
- Drop provider-specific fields from nested structures
|
||||
- Clean up nested parameters before sending to LLM
|
||||
|
||||
## Specify allowed openai params in a request
|
||||
|
||||
Tell litellm to allow specific openai params in a request. Use this if you get a `litellm.UnsupportedParamsError` and want to allow a param. LiteLLM will pass the param as is to the model.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
"""
|
||||
This file contains the logic for dot notation indexing.
|
||||
Path-based navigation utilities for nested dictionaries.
|
||||
|
||||
Used by JWT Auth to get the user role from the token.
|
||||
This module provides utilities for reading and deleting values in nested
|
||||
dictionaries using dot notation and JSONPath-like array syntax.
|
||||
|
||||
Custom implementation with zero external dependencies.
|
||||
|
||||
Supported syntax:
|
||||
- "field" - top-level field
|
||||
- "parent.child" - nested field
|
||||
- "array[*]" - all array elements (wildcard)
|
||||
- "array[0]" - specific array element (index)
|
||||
- "array[*].field" - field in all array elements
|
||||
|
||||
Examples:
|
||||
>>> data = {"tools": [{"name": "t1", "input_examples": ["ex"]}]}
|
||||
>>> delete_nested_value(data, "tools[*].input_examples")
|
||||
{"tools": [{"name": "t1"}]}
|
||||
|
||||
Used by JWT Auth to get the user role from the token, and by
|
||||
additional_drop_params to remove nested fields from optional parameters.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional, TypeVar
|
||||
from typing import Any, Dict, List, Optional, TypeVar, Union
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
|
@ -57,3 +75,164 @@ def get_nested_value(
|
|||
|
||||
# Otherwise, ensure the type matches the default
|
||||
return current if isinstance(current, type(default)) else default
|
||||
|
||||
|
||||
def _parse_path_segments(path: str) -> list:
|
||||
"""
|
||||
Parse a JSONPath-like string into segments using regex.
|
||||
|
||||
Handles:
|
||||
- Dot notation: "a.b.c" → ["a", "b", "c"]
|
||||
- Array wildcards: "a[*].b" → ["a", "[*]", "b"]
|
||||
- Array indices: "a[0].b" → ["a", "[0]", "b"]
|
||||
|
||||
Args:
|
||||
path: JSONPath-like path string
|
||||
|
||||
Returns:
|
||||
List of path segments
|
||||
|
||||
Example:
|
||||
>>> _parse_path_segments("tools[*].arr[0].field")
|
||||
["tools", "[*]", "arr", "[0]", "field"]
|
||||
"""
|
||||
import re
|
||||
|
||||
# Match field names OR bracket expressions
|
||||
# Pattern: field_name (anything except . or [) | [anything_in_brackets]
|
||||
pattern = r'[^\.\[]+|\[[^\]]*\]'
|
||||
segments = re.findall(pattern, path)
|
||||
return segments
|
||||
|
||||
|
||||
def _delete_nested_value_custom(
|
||||
data: Union[Dict[str, Any], List[Any]],
|
||||
segments: list,
|
||||
segment_index: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Recursively delete a field from nested data using parsed segments.
|
||||
|
||||
Modifies data in-place (caller must deep copy first).
|
||||
|
||||
Args:
|
||||
data: Dictionary or list to modify
|
||||
segments: Parsed path segments
|
||||
segment_index: Current position in segments list
|
||||
"""
|
||||
if segment_index >= len(segments):
|
||||
return
|
||||
|
||||
segment = segments[segment_index]
|
||||
is_last = segment_index == len(segments) - 1
|
||||
|
||||
# Handle array wildcard: [*]
|
||||
if segment == "[*]":
|
||||
if isinstance(data, list):
|
||||
for item in data:
|
||||
if is_last:
|
||||
# Can't delete array elements themselves, skip
|
||||
pass
|
||||
else:
|
||||
# Only recurse if item is a dict or list (nested structure)
|
||||
if isinstance(item, (dict, list)):
|
||||
_delete_nested_value_custom(item, segments, segment_index + 1)
|
||||
return
|
||||
|
||||
# Handle array index: [0], [1], [2], etc.
|
||||
if segment.startswith("[") and segment.endswith("]"):
|
||||
try:
|
||||
index = int(segment[1:-1])
|
||||
if isinstance(data, list) and 0 <= index < len(data):
|
||||
if is_last:
|
||||
# Can't delete array elements themselves, skip
|
||||
pass
|
||||
else:
|
||||
# Only recurse if element is a dict or list (nested structure)
|
||||
element = data[index]
|
||||
if isinstance(element, (dict, list)):
|
||||
_delete_nested_value_custom(element, segments, segment_index + 1)
|
||||
except (ValueError, IndexError):
|
||||
# Invalid index, skip
|
||||
pass
|
||||
return
|
||||
|
||||
# Handle regular field navigation
|
||||
if isinstance(data, dict):
|
||||
if is_last:
|
||||
# Delete the field
|
||||
data.pop(segment, None)
|
||||
else:
|
||||
# Navigate deeper
|
||||
if segment in data:
|
||||
next_segment = segments[segment_index + 1] if segment_index + 1 < len(segments) else None
|
||||
|
||||
# If next segment is array notation, current field should be list
|
||||
if next_segment and (next_segment.startswith("[")):
|
||||
if isinstance(data[segment], list):
|
||||
_delete_nested_value_custom(data[segment], segments, segment_index + 1)
|
||||
# Otherwise navigate into dict
|
||||
elif isinstance(data[segment], dict):
|
||||
_delete_nested_value_custom(data[segment], segments, segment_index + 1)
|
||||
|
||||
|
||||
def delete_nested_value(
|
||||
data: Dict[str, Any],
|
||||
path: str,
|
||||
depth: int = 0,
|
||||
max_depth: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete a field from nested data using JSONPath notation.
|
||||
|
||||
Custom implementation - no external dependencies.
|
||||
|
||||
Supports:
|
||||
- "field" - top-level field
|
||||
- "parent.child" - nested field
|
||||
- "array[*]" - all array elements (wildcard)
|
||||
- "array[0]" - specific array element (index)
|
||||
- "array[*].field" - field in all array elements
|
||||
|
||||
Args:
|
||||
data: Dictionary to modify (creates deep copy)
|
||||
path: JSONPath-like path string
|
||||
depth: Current recursion depth (kept for API compatibility)
|
||||
max_depth: Maximum recursion depth (kept for API compatibility)
|
||||
|
||||
Returns:
|
||||
New dictionary with field removed at path
|
||||
|
||||
Example:
|
||||
>>> data = {"tools": [{"name": "t1", "input_examples": ["ex"]}]}
|
||||
>>> delete_nested_value(data, "tools[*].input_examples")
|
||||
{"tools": [{"name": "t1"}]}
|
||||
"""
|
||||
import copy
|
||||
|
||||
result = copy.deepcopy(data)
|
||||
|
||||
try:
|
||||
# Parse path into segments
|
||||
segments = _parse_path_segments(path)
|
||||
|
||||
if not segments:
|
||||
return result
|
||||
|
||||
# Delete using custom recursive implementation
|
||||
_delete_nested_value_custom(result, segments, 0)
|
||||
|
||||
except Exception:
|
||||
# Invalid path or parsing error - silently skip
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def is_nested_path(path: str) -> bool:
|
||||
"""
|
||||
Check if path requires nested handling.
|
||||
|
||||
Returns True if path contains '.' or '[' (array notation).
|
||||
"""
|
||||
return "." in path or "[" in path
|
||||
|
|
|
|||
|
|
@ -1844,6 +1844,21 @@ class BaseLLMHTTPHandler:
|
|||
},
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
# Apply additional_drop_params for nested field removal
|
||||
additional_drop_params = litellm_params.get("additional_drop_params")
|
||||
if additional_drop_params:
|
||||
from litellm.litellm_core_utils.dot_notation_indexing import (
|
||||
delete_nested_value,
|
||||
is_nested_path,
|
||||
)
|
||||
|
||||
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
|
||||
for path in nested_paths:
|
||||
anthropic_messages_optional_request_params = delete_nested_value(
|
||||
anthropic_messages_optional_request_params, path
|
||||
)
|
||||
|
||||
# Prepare request body
|
||||
request_body = anthropic_messages_provider_config.transform_anthropic_messages_request(
|
||||
model=model,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,10 @@ from litellm.litellm_core_utils.redact_messages import (
|
|||
LiteLLMLoggingObject,
|
||||
redact_message_input_output_from_logging,
|
||||
)
|
||||
from litellm.litellm_core_utils.dot_notation_indexing import (
|
||||
delete_nested_value,
|
||||
is_nested_path,
|
||||
)
|
||||
from litellm.litellm_core_utils.rules import Rules
|
||||
from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
|
||||
from litellm.litellm_core_utils.token_counter import get_modified_max_tokens
|
||||
|
|
@ -4148,6 +4152,13 @@ def get_optional_params( # noqa: PLR0915
|
|||
non_default_params=non_default_params,
|
||||
allowed_openai_params=allowed_openai_params,
|
||||
)
|
||||
|
||||
# Apply nested drops from additional_drop_params
|
||||
if additional_drop_params:
|
||||
nested_paths = [p for p in additional_drop_params if is_nested_path(p)]
|
||||
for path in nested_paths:
|
||||
optional_params = delete_nested_value(optional_params, path)
|
||||
|
||||
return optional_params
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ IGNORE_FUNCTIONS = [
|
|||
"_fix_enum_types", # max depth set.
|
||||
"_collect_argument_paths", # max depth set.
|
||||
"_split_text", # max depth set.
|
||||
"_delete_nested_value_custom", # max depth set (bounded by number of path segments).
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
354
tests/test_litellm/test_nested_drop_params.py
Normal file
354
tests/test_litellm/test_nested_drop_params.py
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
"""
|
||||
Test nested path support in additional_drop_params.
|
||||
|
||||
This tests the new JSONPath-like syntax for removing nested fields.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||
|
||||
from litellm.litellm_core_utils.dot_notation_indexing import (
|
||||
delete_nested_value,
|
||||
is_nested_path,
|
||||
)
|
||||
|
||||
|
||||
class TestIsNestedPath:
|
||||
"""Test path detection."""
|
||||
|
||||
def test_top_level_path(self):
|
||||
"""Top-level paths should return False."""
|
||||
assert is_nested_path("temperature") is False
|
||||
assert is_nested_path("response_format") is False
|
||||
|
||||
def test_nested_path_with_dot(self):
|
||||
"""Paths with dots are nested."""
|
||||
assert is_nested_path("parent.child") is True
|
||||
|
||||
def test_nested_path_with_array(self):
|
||||
"""Paths with array notation are nested."""
|
||||
assert is_nested_path("tools[*].input_examples") is True
|
||||
assert is_nested_path("tools[0].field") is True
|
||||
|
||||
|
||||
class TestDeleteNestedValue:
|
||||
"""Test the core deletion logic."""
|
||||
|
||||
def test_array_wildcard_removes_field_from_all_elements(self):
|
||||
"""Test removing a field from all array elements."""
|
||||
data = {
|
||||
"tools": [
|
||||
{"name": "tool1", "input_examples": ["ex1"]},
|
||||
{"name": "tool2", "input_examples": ["ex2"]},
|
||||
],
|
||||
"temperature": 0.7,
|
||||
}
|
||||
|
||||
result = delete_nested_value(data, "tools[*].input_examples")
|
||||
|
||||
# Verify structure preserved
|
||||
assert len(result["tools"]) == 2
|
||||
assert result["tools"][0]["name"] == "tool1"
|
||||
assert result["tools"][1]["name"] == "tool2"
|
||||
assert result["temperature"] == 0.7
|
||||
|
||||
# Verify input_examples removed
|
||||
assert "input_examples" not in result["tools"][0]
|
||||
assert "input_examples" not in result["tools"][1]
|
||||
|
||||
# Verify original unchanged (deep copy)
|
||||
assert "input_examples" in data["tools"][0]
|
||||
|
||||
def test_specific_array_index_removes_field_from_single_element(self):
|
||||
"""Test removing a field from specific array elements using [n] syntax."""
|
||||
# Test data with multiple array elements
|
||||
data = {
|
||||
"tools": [
|
||||
{"name": "t0", "input_examples": ["ex0"], "keep": "val0"},
|
||||
{"name": "t1", "input_examples": ["ex1"], "keep": "val1"},
|
||||
{"name": "t2", "input_examples": ["ex2"], "keep": "val2"},
|
||||
{"name": "t3", "input_examples": ["ex3"], "keep": "val3"},
|
||||
{"name": "t4", "input_examples": ["ex4"], "keep": "val4"},
|
||||
{"name": "t5", "input_examples": ["ex5"], "keep": "val5"},
|
||||
]
|
||||
}
|
||||
|
||||
# Test [0] - first element
|
||||
result = delete_nested_value(data, "tools[0].input_examples")
|
||||
assert "input_examples" not in result["tools"][0]
|
||||
assert "input_examples" in result["tools"][1]
|
||||
assert "input_examples" in result["tools"][2]
|
||||
|
||||
# Test [1] - second element
|
||||
result = delete_nested_value(data, "tools[1].input_examples")
|
||||
assert "input_examples" in result["tools"][0]
|
||||
assert "input_examples" not in result["tools"][1]
|
||||
assert "input_examples" in result["tools"][2]
|
||||
|
||||
# Test [2] - middle element
|
||||
result = delete_nested_value(data, "tools[2].input_examples")
|
||||
assert "input_examples" in result["tools"][0]
|
||||
assert "input_examples" not in result["tools"][2]
|
||||
assert "input_examples" in result["tools"][3]
|
||||
|
||||
# Test [5] - last element
|
||||
result = delete_nested_value(data, "tools[5].input_examples")
|
||||
assert "input_examples" in result["tools"][0]
|
||||
assert "input_examples" not in result["tools"][5]
|
||||
|
||||
# Verify other fields preserved in all cases
|
||||
assert result["tools"][0]["keep"] == "val0"
|
||||
assert result["tools"][5]["keep"] == "val5"
|
||||
|
||||
# Verify original unchanged (deep copy)
|
||||
assert "input_examples" in data["tools"][0]
|
||||
|
||||
|
||||
class TestComplexNestedPatterns:
|
||||
"""Test complex nested patterns with multiple wildcards and deep nesting."""
|
||||
|
||||
def test_multiple_jsonpath_patterns_in_list(self):
|
||||
"""Test processing multiple JSONPath patterns sequentially."""
|
||||
data = {
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool1",
|
||||
"input_examples": ["ex1"],
|
||||
"some_arr": [
|
||||
{
|
||||
"some_struct": {
|
||||
"remove_this_field": "val1",
|
||||
"keep_this": "val2",
|
||||
}
|
||||
},
|
||||
{
|
||||
"some_struct": {
|
||||
"remove_this_field": "val3",
|
||||
"keep_this": "val4",
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "tool2",
|
||||
"input_examples": ["ex2"],
|
||||
"some_arr": [
|
||||
{
|
||||
"some_struct": {
|
||||
"remove_this_field": "val5",
|
||||
"keep_this": "val6",
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
"temperature": 0.7,
|
||||
}
|
||||
|
||||
# Simulate multiple paths being processed (as in utils.py:4134-4137)
|
||||
paths = [
|
||||
"tools[*].input_examples",
|
||||
"tools[*].some_arr[*].some_struct.remove_this_field",
|
||||
]
|
||||
|
||||
result = data
|
||||
for path in paths:
|
||||
result = delete_nested_value(result, path)
|
||||
|
||||
# Verify input_examples removed from all tools
|
||||
assert "input_examples" not in result["tools"][0]
|
||||
assert "input_examples" not in result["tools"][1]
|
||||
|
||||
# Verify deeply nested field removed from all array elements
|
||||
assert (
|
||||
"remove_this_field"
|
||||
not in result["tools"][0]["some_arr"][0]["some_struct"]
|
||||
)
|
||||
assert (
|
||||
"remove_this_field"
|
||||
not in result["tools"][0]["some_arr"][1]["some_struct"]
|
||||
)
|
||||
assert (
|
||||
"remove_this_field"
|
||||
not in result["tools"][1]["some_arr"][0]["some_struct"]
|
||||
)
|
||||
|
||||
# Verify other fields preserved
|
||||
assert result["tools"][0]["some_arr"][0]["some_struct"]["keep_this"] == "val2"
|
||||
assert result["tools"][1]["some_arr"][0]["some_struct"]["keep_this"] == "val6"
|
||||
assert result["temperature"] == 0.7
|
||||
|
||||
def test_remove_entire_nested_array_field(self):
|
||||
"""Test removing entire array fields (not just array elements)."""
|
||||
data = {
|
||||
"tools": [
|
||||
{"name": "t1", "some_arr": [1, 2, 3], "other_field": "keep"},
|
||||
{"name": "t2", "some_arr": [4, 5, 6], "other_field": "keep"},
|
||||
]
|
||||
}
|
||||
|
||||
result = delete_nested_value(data, "tools[*].some_arr")
|
||||
|
||||
# Verify entire array field removed (not individual elements)
|
||||
assert "some_arr" not in result["tools"][0]
|
||||
assert "some_arr" not in result["tools"][1]
|
||||
|
||||
# Verify other fields preserved
|
||||
assert result["tools"][0]["name"] == "t1"
|
||||
assert result["tools"][0]["other_field"] == "keep"
|
||||
assert result["tools"][1]["name"] == "t2"
|
||||
assert result["tools"][1]["other_field"] == "keep"
|
||||
|
||||
def test_triple_nested_wildcards(self):
|
||||
"""Test extreme nesting: tools[*].arr1[*].arr2[*].field."""
|
||||
data = {
|
||||
"tools": [
|
||||
{
|
||||
"name": "t1",
|
||||
"arr1": [
|
||||
{
|
||||
"arr2": [
|
||||
{"field": "remove1", "keep": "yes1"},
|
||||
{"field": "remove2", "keep": "yes2"},
|
||||
]
|
||||
},
|
||||
{
|
||||
"arr2": [
|
||||
{"field": "remove3", "keep": "yes3"},
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = delete_nested_value(data, "tools[*].arr1[*].arr2[*].field")
|
||||
|
||||
# Verify deeply nested field removed from all levels
|
||||
assert "field" not in result["tools"][0]["arr1"][0]["arr2"][0]
|
||||
assert "field" not in result["tools"][0]["arr1"][0]["arr2"][1]
|
||||
assert "field" not in result["tools"][0]["arr1"][1]["arr2"][0]
|
||||
|
||||
# Verify keep field preserved at all levels
|
||||
assert result["tools"][0]["arr1"][0]["arr2"][0]["keep"] == "yes1"
|
||||
assert result["tools"][0]["arr1"][0]["arr2"][1]["keep"] == "yes2"
|
||||
assert result["tools"][0]["arr1"][1]["arr2"][0]["keep"] == "yes3"
|
||||
|
||||
def test_combination_of_simple_and_complex_paths(self):
|
||||
"""Test mixing simple nested paths with complex multi-wildcard paths."""
|
||||
data = {
|
||||
"tools": [
|
||||
{
|
||||
"name": "t1",
|
||||
"simple_nested": {"remove": "val1", "keep": "val2"},
|
||||
"complex": [{"nested": {"remove": "val3", "keep": "val4"}}],
|
||||
}
|
||||
],
|
||||
"top_level_remove": "should_go",
|
||||
"top_level_keep": "should_stay",
|
||||
}
|
||||
|
||||
# Process multiple different types of paths
|
||||
paths = [
|
||||
"tools[*].simple_nested.remove",
|
||||
"tools[*].complex[*].nested.remove",
|
||||
]
|
||||
|
||||
result = data
|
||||
for path in paths:
|
||||
result = delete_nested_value(result, path)
|
||||
|
||||
# Verify simple nested removal
|
||||
assert "remove" not in result["tools"][0]["simple_nested"]
|
||||
assert result["tools"][0]["simple_nested"]["keep"] == "val2"
|
||||
|
||||
# Verify complex nested removal
|
||||
assert "remove" not in result["tools"][0]["complex"][0]["nested"]
|
||||
assert result["tools"][0]["complex"][0]["nested"]["keep"] == "val4"
|
||||
|
||||
# Verify top-level fields unchanged
|
||||
assert result["top_level_remove"] == "should_go"
|
||||
assert result["top_level_keep"] == "should_stay"
|
||||
|
||||
def test_mixed_wildcards_and_indices_with_deep_nesting(self):
|
||||
"""Test combining [*] wildcards, [n] indices, and deep nesting in complex patterns."""
|
||||
data = {
|
||||
"tools": [
|
||||
{
|
||||
"name": "t0",
|
||||
"configs": [
|
||||
{"id": "c0", "remove_me": "val1", "keep": "yes1"},
|
||||
{"id": "c1", "remove_me": "val2", "keep": "yes2"},
|
||||
],
|
||||
"metadata": {"drop_this": "meta1", "preserve": "preserve1"},
|
||||
},
|
||||
{
|
||||
"name": "t1",
|
||||
"configs": [
|
||||
{"id": "c0", "remove_me": "val3", "keep": "yes3"},
|
||||
{"id": "c1", "remove_me": "val4", "keep": "yes4"},
|
||||
],
|
||||
"metadata": {"drop_this": "meta2", "preserve": "preserve2"},
|
||||
},
|
||||
{
|
||||
"name": "t2",
|
||||
"configs": [
|
||||
{"id": "c0", "remove_me": "val5", "keep": "yes5"},
|
||||
],
|
||||
"metadata": {"drop_this": "meta3", "preserve": "preserve3"},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
# Simulate processing multiple complex paths
|
||||
paths = [
|
||||
"tools[*].configs[1].remove_me", # Wildcard + specific index [1] + nested
|
||||
"tools[1].metadata.drop_this", # Specific index + nested
|
||||
"tools[*].configs[*].id", # Double wildcard + nested
|
||||
]
|
||||
|
||||
result = data
|
||||
for path in paths:
|
||||
result = delete_nested_value(result, path)
|
||||
|
||||
# Verify: tools[*].configs[1].remove_me removed from second config of all tools (that have one)
|
||||
assert "remove_me" in result["tools"][0]["configs"][0] # First config untouched
|
||||
assert (
|
||||
"remove_me" not in result["tools"][0]["configs"][1]
|
||||
) # Second config removed
|
||||
assert "remove_me" in result["tools"][1]["configs"][0] # First config untouched
|
||||
assert (
|
||||
"remove_me" not in result["tools"][1]["configs"][1]
|
||||
) # Second config removed
|
||||
assert (
|
||||
"remove_me" in result["tools"][2]["configs"][0]
|
||||
) # Only has [0], unaffected
|
||||
|
||||
# Verify: tools[1].metadata.drop_this removed only from second tool
|
||||
assert "drop_this" in result["tools"][0]["metadata"]
|
||||
assert "drop_this" not in result["tools"][1]["metadata"]
|
||||
assert "drop_this" in result["tools"][2]["metadata"]
|
||||
|
||||
# Verify: tools[*].configs[*].id removed from all configs in all tools
|
||||
assert "id" not in result["tools"][0]["configs"][0]
|
||||
assert "id" not in result["tools"][0]["configs"][1]
|
||||
assert "id" not in result["tools"][1]["configs"][0]
|
||||
assert "id" not in result["tools"][1]["configs"][1]
|
||||
assert "id" not in result["tools"][2]["configs"][0]
|
||||
|
||||
# Verify: other fields preserved
|
||||
assert result["tools"][0]["configs"][0]["keep"] == "yes1"
|
||||
assert result["tools"][1]["configs"][1]["keep"] == "yes4"
|
||||
assert result["tools"][0]["metadata"]["preserve"] == "preserve1"
|
||||
assert result["tools"][1]["metadata"]["preserve"] == "preserve2"
|
||||
assert result["tools"][2]["name"] == "t2"
|
||||
|
||||
# Verify original unchanged
|
||||
assert "remove_me" in data["tools"][0]["configs"][1]
|
||||
|
||||
|
||||
# Phase 1 tests - validates core functionality and complex patterns
|
||||
Loading…
Add table
Reference in a new issue