Skip to content

Commit 40c0f4d

Browse files
authored
Handle type properly in pydantic type checking. Also improve error messages when incorrectly using generic type. (#471)
Signed-off-by: Pascal Tomecek <pascal.tomecek@cubistsystematic.com>
1 parent 20754c7 commit 40c0f4d

4 files changed

Lines changed: 52 additions & 5 deletions

File tree

csp/impl/types/pydantic_types.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ class CspTypeVarType(Generic[_T]):
3535
@classmethod
3636
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
3737
typ = _check_source_type(cls, source_type)
38+
if not typ or not typ[0].isalpha():
39+
if typ and typ[0] == "~":
40+
raise SyntaxError(
41+
f"Invalid generic type: {typ}. The generic type annotation (i.e. `~T`) is only allowed at the top level (i.e. `value: '~T'`)."
42+
)
43+
raise SyntaxError(
44+
f"Invalid generic type: {typ}. Generic types in csp must start with an alphabetic character."
45+
)
3846

3947
def _validator(v: Any, info: ValidationInfo) -> Any:
4048
# info.context should be an instance of TVarValidationContext, but we don't check for performance
@@ -55,6 +63,14 @@ class CspTypeVar(Generic[_T]):
5563
@classmethod
5664
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
5765
tvar = _check_source_type(cls, source_type)
66+
if not tvar or not tvar[0].isalpha():
67+
if tvar and tvar[0] == "~":
68+
raise SyntaxError(
69+
f"Invalid generic type: {tvar}. The generic type annotation (i.e. `~T`) is only allowed at the top level (i.e. `value: '~T'`)."
70+
)
71+
raise SyntaxError(
72+
f"Invalid generic type: {tvar}. Generic types in csp must start with an alphabetic character. "
73+
)
5874

5975
def _validator(v: Any, info: ValidationInfo) -> Any:
6076
# info.context should be an instance of TVarValidationContext, but we don't check for performance

csp/impl/types/typing_utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ class TsTypeValidator:
9999
For example, this is to make sure that:
100100
ts[List] can validate as ts[List[float]]
101101
ts[Dict[str, List[str]] won't validate as ts[Dict[str, List[float]]
102-
ts["T"], ts[TypeVar("T")], ts[List["T"]], etc are allowed
103-
ts[Optional[float]], ts[Union[float, int]], ts[Annotated[float, None]], etc are not allowed
104-
etc
102+
Notes:
103+
ts["T"], ts[TypeVar("T")], ts[List["T"]], ts[Optional[float]], ts[Union[float, int]], ts[Any] are allowed
104+
ts[Annotated[float, None]], are not allowed
105105
For validation of csp baskets, this piece becomes the bottleneck
106106
"""
107107

@@ -125,7 +125,7 @@ def __init__(self, source_type: type):
125125
self._source_is_union = CspTypingUtils.is_union_type(source_type)
126126
self._source_args = typing.get_args(source_type)
127127
self._source_adapter = None
128-
if type(source_type) in (typing.ForwardRef, typing.TypeVar):
128+
if type(source_type) in (typing.ForwardRef, typing.TypeVar) or source_type is typing.Any:
129129
pass # Will handle these separately as part of type checking
130130
elif self._source_origin is None and isinstance(self._source_type, type):
131131
# self._source_adapter = TypeAdapter(typing.Type[source_type])
@@ -162,12 +162,19 @@ def validate(self, value_type, info=None):
162162
self._last_value_type = value_type
163163
self._last_context = info.context if info is not None else None
164164

165+
if value_type is typing.Any:
166+
# https://docs.python.org/3/library/typing.html#the-any-type
167+
# "Notice that no type checking is performed when assigning a value of type Any to a more precise type."
168+
return value_type
169+
165170
# Fast path because while we could use the source adapter in the next block to validate,
166171
# it's about 10x faster to do a simple validation with issubclass, and this adds up on baskets
167172
if self._source_origin is None:
168173
# Want to allow int to be passed for float (i.e. in resolution of TVars)
169174
if self._source_type is float and value_type is int:
170175
return self._source_type
176+
if self._source_type is typing.Any:
177+
return value_type
171178
try:
172179
if issubclass(value_type, self._source_type):
173180
return value_type

csp/tests/impl/types/test_pydantic_type_resolver.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ def test_forced_tvar(self):
9191
context.resolve_tvars()
9292
self.assertDictEqual(context.tvars, {"T": float})
9393

94+
def test_bad_variable_name(self):
95+
self.assertRaises(SyntaxError, lambda: CspTypeVar[""])
96+
self.assertRaises(SyntaxError, TypeAdapter, CspTypeVar["~T"])
97+
self.assertRaises(SyntaxError, TypeAdapter, CspTypeVar["1"])
98+
_ = TypeAdapter(CspTypeVar["T1"])
99+
94100

95101
class TestPydanticTypeResolver_CspTypeVarType(TestCase):
96102
def test_one_value(self):
@@ -182,6 +188,12 @@ def test_TsType_nested(self):
182188
context.resolve_tvars()
183189
self.assertDictEqual(context.tvars, {"T": float})
184190

191+
def test_bad_variable_name(self):
192+
self.assertRaises(SyntaxError, lambda: CspTypeVarType[""])
193+
self.assertRaises(SyntaxError, TypeAdapter, CspTypeVarType["~T"])
194+
self.assertRaises(SyntaxError, TypeAdapter, CspTypeVarType["1"])
195+
_ = TypeAdapter(CspTypeVarType["T1"])
196+
185197

186198
T = TypeVar("T")
187199

csp/tests/impl/types/test_tstype.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import pytest
33
import sys
44
from pydantic import TypeAdapter
5-
from typing import Dict, ForwardRef, Generic, List, Mapping, TypeVar, Union, get_args, get_origin
5+
from typing import Any, Dict, ForwardRef, Generic, List, Mapping, TypeVar, Union, get_args, get_origin
66
from unittest import TestCase
77

88
import csp
@@ -105,6 +105,18 @@ def test_allow_null(self):
105105
ta.validate_python(csp.null_ts(float), context=context)
106106
ta.validate_python(None, context=context)
107107

108+
def test_any(self):
109+
ta = TypeAdapter(TsType[Any])
110+
ta.validate_python(csp.null_ts(float))
111+
ta.validate_python(csp.null_ts(object))
112+
ta.validate_python(csp.null_ts(List[str]))
113+
ta.validate_python(csp.null_ts(Dict[str, List[float]]))
114+
115+
# https://docs.python.org/3/library/typing.html#the-any-type
116+
# "Notice that no type checking is performed when assigning a value of type Any to a more precise type."
117+
ta = TypeAdapter(TsType[float])
118+
ta.validate_python(csp.null_ts(Any))
119+
108120

109121
class TestOutputValidation(TestCase):
110122
def test_validation(self):

0 commit comments

Comments
 (0)