Coverage for tests/test_compat.py: 100%

113 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-12-04 08:29 +0000

1from typing import Any, Dict, List, Union 1abcdefg

2 

3from fastapi import FastAPI, UploadFile 1abcdefg

4from fastapi._compat import ( 1abcdefg

5 Undefined, 

6 _get_model_config, 

7 get_cached_model_fields, 

8 is_scalar_field, 

9 is_uploadfile_sequence_annotation, 

10 may_v1, 

11) 

12from fastapi._compat.shared import is_bytes_sequence_annotation 1abcdefg

13from fastapi.testclient import TestClient 1abcdefg

14from pydantic import BaseModel, ConfigDict 1abcdefg

15from pydantic.fields import FieldInfo 1abcdefg

16 

17from .utils import needs_py310, needs_py_lt_314, needs_pydanticv2 1abcdefg

18 

19 

20@needs_pydanticv2 1abcdefg

21def test_model_field_default_required(): 1abcdefg

22 from fastapi._compat import v2 156789!#

23 

24 # For coverage 

25 field_info = FieldInfo(annotation=str) 156789!#

26 field = v2.ModelField(name="foo", field_info=field_info) 156789!#

27 assert field.default is Undefined 156789!#

28 

29 

30@needs_py_lt_314 1abcdefg

31def test_v1_plain_validator_function(): 1abcdefg

32 from fastapi._compat import v1 1$%'()

33 

34 # For coverage 

35 def func(v): # pragma: no cover 1$%'()

36 return v 

37 

38 result = v1.with_info_plain_validator_function(func) 1$%'()

39 assert result == {} 1$%'()

40 

41 

42def test_is_model_field(): 1abcdefg

43 # For coverage 

44 from fastapi._compat import _is_model_field 1*+,-./:

45 

46 assert not _is_model_field(str) 1*+,-./:

47 

48 

49@needs_pydanticv2 1abcdefg

50def test_get_model_config(): 1abcdefg

51 # For coverage in Pydantic v2 

52 class Foo(BaseModel): 1TUVWXYZ

53 model_config = ConfigDict(from_attributes=True) 1TUVWXYZ

54 

55 foo = Foo() 1TUVWXYZ

56 config = _get_model_config(foo) 1TUVWXYZ

57 assert config == {"from_attributes": True} 1TUVWXYZ

58 

59 

60def test_complex(): 1abcdefg

61 app = FastAPI() 1opqrstu

62 

63 @app.post("/") 1opqrstu

64 def foo(foo: Union[str, List[int]]): 1opqrstu

65 return foo 1opqrstu

66 

67 client = TestClient(app) 1opqrstu

68 

69 response = client.post("/", json="bar") 1opqrstu

70 assert response.status_code == 200, response.text 1opqrstu

71 assert response.json() == "bar" 1opqrstu

72 

73 response2 = client.post("/", json=[1, 2]) 1opqrstu

74 assert response2.status_code == 200, response2.text 1opqrstu

75 assert response2.json() == [1, 2] 1opqrstu

76 

77 

78@needs_pydanticv2 1abcdefg

79def test_propagates_pydantic2_model_config(): 1abcdefg

80 app = FastAPI() 1hijklmn

81 

82 class Missing: 1hijklmn

83 def __bool__(self): 1hijklmn

84 return False 1hijklmn

85 

86 class EmbeddedModel(BaseModel): 1hijklmn

87 model_config = ConfigDict(arbitrary_types_allowed=True) 1hijklmn

88 value: Union[str, Missing] = Missing() 1hijklmn

89 

90 class Model(BaseModel): 1hijklmn

91 model_config = ConfigDict( 1hijklmn

92 arbitrary_types_allowed=True, 

93 ) 

94 value: Union[str, Missing] = Missing() 1hijklmn

95 embedded_model: EmbeddedModel = EmbeddedModel() 1hijklmn

96 

97 @app.post("/") 1hijklmn

98 def foo(req: Model) -> Dict[str, Union[str, None]]: 1hijklmn

99 return { 1hijklmn

100 "value": req.value or None, 

101 "embedded_value": req.embedded_model.value or None, 

102 } 

103 

104 client = TestClient(app) 1hijklmn

105 

106 response = client.post("/", json={}) 1hijklmn

107 assert response.status_code == 200, response.text 1hijklmn

108 assert response.json() == { 1hijklmn

109 "value": None, 

110 "embedded_value": None, 

111 } 

112 

113 response2 = client.post( 1hijklmn

114 "/", json={"value": "foo", "embedded_model": {"value": "bar"}} 

115 ) 

116 assert response2.status_code == 200, response2.text 1hijklmn

117 assert response2.json() == { 1hijklmn

118 "value": "foo", 

119 "embedded_value": "bar", 

120 } 

121 

122 

123def test_is_bytes_sequence_annotation_union(): 1abcdefg

124 # For coverage 

125 # TODO: in theory this would allow declaring types that could be lists of bytes 

126 # to be read from files and other types, but I'm not even sure it's a good idea 

127 # to support it as a first class "feature" 

128 assert is_bytes_sequence_annotation(Union[List[str], List[bytes]]) 1;=?@[]^

129 

130 

131def test_is_uploadfile_sequence_annotation(): 1abcdefg

132 # For coverage 

133 # TODO: in theory this would allow declaring types that could be lists of UploadFile 

134 # and other types, but I'm not even sure it's a good idea to support it as a first 

135 # class "feature" 

136 assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) 2_ ` { | } ~ ab

137 

138 

139@needs_pydanticv2 1abcdefg

140def test_serialize_sequence_value_with_optional_list(): 1abcdefg

141 """Test that serialize_sequence_value handles optional lists correctly.""" 

142 from fastapi._compat import v2 1ABCDEFG

143 

144 field_info = FieldInfo(annotation=Union[List[str], None]) 1ABCDEFG

145 field = v2.ModelField(name="items", field_info=field_info) 1ABCDEFG

146 result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) 1ABCDEFG

147 assert result == ["a", "b", "c"] 1ABCDEFG

148 assert isinstance(result, list) 1ABCDEFG

149 

150 

151@needs_pydanticv2 1abcdefg

152@needs_py310 1abcdefg

153def test_serialize_sequence_value_with_optional_list_pipe_union(): 1abcdefg

154 """Test that serialize_sequence_value handles optional lists correctly (with new syntax).""" 

155 from fastapi._compat import v2 1HIJKL

156 

157 field_info = FieldInfo(annotation=list[str] | None) 1HIJKL

158 field = v2.ModelField(name="items", field_info=field_info) 1HIJKL

159 result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"]) 1HIJKL

160 assert result == ["a", "b", "c"] 1HIJKL

161 assert isinstance(result, list) 1HIJKL

162 

163 

164@needs_pydanticv2 1abcdefg

165def test_serialize_sequence_value_with_none_first_in_union(): 1abcdefg

166 """Test that serialize_sequence_value handles Union[None, List[...]] correctly.""" 

167 from fastapi._compat import v2 1MNOPQRS

168 

169 field_info = FieldInfo(annotation=Union[None, List[str]]) 1MNOPQRS

170 field = v2.ModelField(name="items", field_info=field_info) 1MNOPQRS

171 result = v2.serialize_sequence_value(field=field, value=["x", "y"]) 1MNOPQRS

172 assert result == ["x", "y"] 1MNOPQRS

173 assert isinstance(result, list) 1MNOPQRS

174 

175 

176@needs_py_lt_314 1abcdefg

177def test_is_pv1_scalar_field(): 1abcdefg

178 from fastapi._compat import v1 101234

179 

180 # For coverage 

181 class Model(v1.BaseModel): 101234

182 foo: Union[str, Dict[str, Any]] 101234

183 

184 fields = v1.get_model_fields(Model) 101234

185 assert not is_scalar_field(fields[0]) 101234

186 

187 

188@needs_py_lt_314 1abcdefg

189def test_get_model_fields_cached(): 1abcdefg

190 from fastapi._compat import v1 1vwxyz

191 

192 class Model(may_v1.BaseModel): 1vwxyz

193 foo: str 1vwxyz

194 

195 non_cached_fields = v1.get_model_fields(Model) 1vwxyz

196 non_cached_fields2 = v1.get_model_fields(Model) 1vwxyz

197 cached_fields = get_cached_model_fields(Model) 1vwxyz

198 cached_fields2 = get_cached_model_fields(Model) 1vwxyz

199 for f1, f2 in zip(cached_fields, cached_fields2): 1vwxyz

200 assert f1 is f2 1vwxyz

201 

202 assert non_cached_fields is not non_cached_fields2 1vwxyz

203 assert cached_fields is cached_fields2 1vwxyz