chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""Tests for parameter aliasing using Pydantic AliasChoices.
|
||||
|
||||
P1-1.5 uses Pydantic's AliasChoices with Field(validation_alias=...) to accept
|
||||
both snake_case and camelCase parameter names at the FastMCP validation layer.
|
||||
"""
|
||||
import pytest
|
||||
from pydantic import AliasChoices, BaseModel, Field
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
class TestAliasChoicesPattern:
|
||||
"""Tests demonstrating the AliasChoices pattern for parameter aliasing."""
|
||||
|
||||
def test_alias_choices_accepts_snake_case(self):
|
||||
"""AliasChoices accepts snake_case parameter names."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
search_term: Annotated[
|
||||
str,
|
||||
Field(validation_alias=AliasChoices("search_term", "searchTerm"))
|
||||
]
|
||||
|
||||
m = TestModel.model_validate({"search_term": "test"})
|
||||
assert m.search_term == "test"
|
||||
|
||||
def test_alias_choices_accepts_camel_case(self):
|
||||
"""AliasChoices accepts camelCase parameter names."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
search_term: Annotated[
|
||||
str,
|
||||
Field(validation_alias=AliasChoices("search_term", "searchTerm"))
|
||||
]
|
||||
|
||||
m = TestModel.model_validate({"searchTerm": "test"})
|
||||
assert m.search_term == "test"
|
||||
|
||||
def test_snake_case_takes_precedence(self):
|
||||
"""When both are provided, the first alias choice wins."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
search_term: Annotated[
|
||||
str,
|
||||
Field(validation_alias=AliasChoices("search_term", "searchTerm"))
|
||||
]
|
||||
|
||||
# First matching alias wins
|
||||
m = TestModel.model_validate({"search_term": "snake", "searchTerm": "camel"})
|
||||
assert m.search_term == "snake"
|
||||
|
||||
def test_alias_choices_with_default_value(self):
|
||||
"""AliasChoices works with optional parameters that have defaults."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
search_method: Annotated[
|
||||
str,
|
||||
Field(
|
||||
default="by_name",
|
||||
validation_alias=AliasChoices("search_method", "searchMethod")
|
||||
)
|
||||
]
|
||||
|
||||
# Default is used when not provided
|
||||
m1 = TestModel.model_validate({})
|
||||
assert m1.search_method == "by_name"
|
||||
|
||||
# snake_case overrides default
|
||||
m2 = TestModel.model_validate({"search_method": "by_tag"})
|
||||
assert m2.search_method == "by_tag"
|
||||
|
||||
# camelCase overrides default
|
||||
m3 = TestModel.model_validate({"searchMethod": "by_id"})
|
||||
assert m3.search_method == "by_id"
|
||||
|
||||
def test_alias_choices_with_optional_none(self):
|
||||
"""AliasChoices works with Optional parameters defaulting to None."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
page_size: Annotated[
|
||||
int | None,
|
||||
Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("page_size", "pageSize")
|
||||
)
|
||||
]
|
||||
|
||||
# None default
|
||||
m1 = TestModel.model_validate({})
|
||||
assert m1.page_size is None
|
||||
|
||||
# snake_case
|
||||
m2 = TestModel.model_validate({"page_size": 50})
|
||||
assert m2.page_size == 50
|
||||
|
||||
# camelCase
|
||||
m3 = TestModel.model_validate({"pageSize": 100})
|
||||
assert m3.page_size == 100
|
||||
|
||||
def test_alias_choices_with_bool_coercion(self):
|
||||
"""AliasChoices works with boolean parameters."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
include_inactive: Annotated[
|
||||
bool | str | None,
|
||||
Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("include_inactive", "includeInactive")
|
||||
)
|
||||
]
|
||||
|
||||
# camelCase with bool
|
||||
m1 = TestModel.model_validate({"includeInactive": True})
|
||||
assert m1.include_inactive is True
|
||||
|
||||
# snake_case with string (common from JSON)
|
||||
m2 = TestModel.model_validate({"include_inactive": "true"})
|
||||
assert m2.include_inactive == "true" # Note: string coercion happens in tool
|
||||
|
||||
def test_alias_choices_multiple_params(self):
|
||||
"""Multiple parameters can each have AliasChoices."""
|
||||
|
||||
class TestModel(BaseModel):
|
||||
search_term: Annotated[
|
||||
str,
|
||||
Field(validation_alias=AliasChoices("search_term", "searchTerm"))
|
||||
]
|
||||
search_method: Annotated[
|
||||
str,
|
||||
Field(
|
||||
default="by_name",
|
||||
validation_alias=AliasChoices("search_method", "searchMethod")
|
||||
)
|
||||
]
|
||||
page_size: Annotated[
|
||||
int | None,
|
||||
Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("page_size", "pageSize")
|
||||
)
|
||||
]
|
||||
|
||||
# Mix of snake_case and camelCase
|
||||
m = TestModel.model_validate({
|
||||
"searchTerm": "Player",
|
||||
"search_method": "by_tag",
|
||||
"pageSize": 25
|
||||
})
|
||||
|
||||
assert m.search_term == "Player"
|
||||
assert m.search_method == "by_tag"
|
||||
assert m.page_size == 25
|
||||
Reference in New Issue
Block a user