Coverage for python/lsst/daf/butler/_compat.py: 43%

68 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-08-12 09:20 +0000

1# This file is part of pipe_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Code to support backwards compatibility.""" 

23 

24from __future__ import annotations 

25 

26__all__ = ["PYDANTIC_V2", "_BaseModelCompat"] 

27 

28import sys 

29from collections.abc import Callable 

30from typing import TYPE_CHECKING, Any, Literal 

31 

32from pydantic import BaseModel 

33from pydantic.fields import FieldInfo 

34from pydantic.version import VERSION as PYDANTIC_VERSION 

35 

36if sys.version_info >= (3, 11, 0): 36 ↛ 39line 36 didn't jump to line 39, because the condition on line 36 was never false

37 from typing import Self 

38else: 

39 from typing import TypeVar 

40 

41 Self = TypeVar("Self", bound="_BaseModelCompat") # type: ignore 

42 

43 

44PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") 

45 

46# This matches the pydantic v2 internal definition. 

47IncEx = set[int] | set[str] | dict[int, Any] | dict[str, Any] | None 

48 

49if PYDANTIC_V2: 49 ↛ 51line 49 didn't jump to line 51, because the condition on line 49 was never true

50 

51 class _BaseModelCompat(BaseModel): 

52 """Methods from pydantic v1 that we want to emulate in v2. 

53 

54 Some of these methods are provided by v2 but issue deprecation 

55 warnings. We need to decide whether we are also okay with deprecating 

56 them or want to support them without the deprecation message. 

57 """ 

58 

59 def json( 

60 self, 

61 *, 

62 include: IncEx = None, # type: ignore 

63 exclude: IncEx = None, # type: ignore 

64 by_alias: bool = False, 

65 skip_defaults: bool | None = None, 

66 exclude_unset: bool = False, 

67 exclude_defaults: bool = False, 

68 exclude_none: bool = False, 

69 encoder: Callable[[Any], Any] | None = None, 

70 models_as_dict: bool = True, 

71 **dumps_kwargs: Any, 

72 ) -> str: 

73 if dumps_kwargs: 

74 raise TypeError("dumps_kwargs no longer supported.") 

75 if encoder is not None: 

76 raise TypeError("json encoder is no longer supported.") 

77 # Can catch warnings and call BaseModel.json() directly. 

78 return self.model_dump_json( 

79 include=include, 

80 exclude=exclude, 

81 by_alias=by_alias, 

82 exclude_defaults=exclude_defaults, 

83 exclude_none=exclude_none, 

84 exclude_unset=exclude_unset, 

85 ) 

86 

87 @classmethod 

88 def parse_obj(cls, obj: Any) -> Self: 

89 # Catch warnings and call BaseModel.parse_obj directly? 

90 return cls.model_validate(obj) 

91 

92 if TYPE_CHECKING and not PYDANTIC_V2: 

93 # mypy sees the first definition of a class and ignores any 

94 # redefinition. This means that if mypy is run with pydantic v1 

95 # it will not see the classes defined in the else block below. 

96 

97 @classmethod 

98 def model_construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self: 

99 return cls() 

100 

101 @classmethod 

102 def model_validate( 

103 cls, 

104 obj: Any, 

105 *, 

106 strict: bool | None = None, 

107 from_attributes: bool | None = None, 

108 context: dict[str, Any] | None = None, 

109 ) -> Self: 

110 return cls() 

111 

112 def model_dump_json( 

113 self, 

114 *, 

115 indent: int | None = None, 

116 include: IncEx = None, 

117 exclude: IncEx = None, 

118 by_alias: bool = False, 

119 exclude_unset: bool = False, 

120 exclude_defaults: bool = False, 

121 exclude_none: bool = False, 

122 round_trip: bool = False, 

123 warnings: bool = True, 

124 ) -> str: 

125 return "" 

126 

127 @property 

128 def model_fields(self) -> dict[str, FieldInfo]: # type: ignore 

129 return {} 

130 

131 @classmethod 

132 def model_rebuild( 

133 cls, 

134 *, 

135 force: bool = False, 

136 raise_errors: bool = True, 

137 _parent_namespace_depth: int = 2, 

138 _types_namespace: dict[str, Any] | None = None, 

139 ) -> bool | None: 

140 return None 

141 

142 def model_dump( 

143 self, 

144 *, 

145 mode: Literal["json", "python"] | str = "python", 

146 include: IncEx = None, 

147 exclude: IncEx = None, 

148 by_alias: bool = False, 

149 exclude_unset: bool = False, 

150 exclude_defaults: bool = False, 

151 exclude_none: bool = False, 

152 round_trip: bool = False, 

153 warnings: bool = True, 

154 ) -> dict[str, Any]: 

155 return {} 

156 

157 @classmethod 

158 def model_validate_json( 

159 cls, 

160 json_data: str | bytes | bytearray, 

161 *, 

162 strict: bool | None = None, 

163 context: dict[str, Any] | None = None, 

164 ) -> Self: 

165 return cls() 

166 

167else: 

168 from astropy.utils.decorators import classproperty 

169 

170 class _BaseModelCompat(BaseModel): # type:ignore[no-redef] 

171 """Methods from pydantic v2 that can be used in pydantic v1.""" 

172 

173 @classmethod 

174 def model_validate( 

175 cls, 

176 obj: Any, 

177 *, 

178 strict: bool | None = None, 

179 from_attributes: bool | None = None, 

180 context: dict[str, Any] | None = None, 

181 ) -> Self: 

182 return cls.parse_obj(obj) 

183 

184 def model_dump_json( 

185 self, 

186 *, 

187 indent: int | None = None, 

188 include: IncEx = None, 

189 exclude: IncEx = None, 

190 by_alias: bool = False, 

191 exclude_unset: bool = False, 

192 exclude_defaults: bool = False, 

193 exclude_none: bool = False, 

194 round_trip: bool = False, 

195 warnings: bool = True, 

196 ) -> str: 

197 return self.json( 

198 include=include, # type: ignore 

199 exclude=exclude, # type: ignore 

200 by_alias=by_alias, 

201 exclude_unset=exclude_unset, 

202 exclude_defaults=exclude_defaults, 

203 exclude_none=exclude_none, 

204 ) 

205 

206 @classmethod # type: ignore 

207 def model_construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self: 

208 # BaseModel.construct() is very close to what we previously 

209 # implemented manually in each direct() method but does have one 

210 # extra loop in it to fill in defaults and handle aliases. 

211 return cls.construct(_fields_set=_fields_set, **values) 

212 

213 @classmethod 

214 @classproperty 

215 def model_fields(cls) -> dict[str, FieldInfo]: # type: ignore 

216 return cls.__fields__ # type: ignore 

217 

218 @classmethod 

219 def model_rebuild( 

220 cls, 

221 *, 

222 force: bool = False, 

223 raise_errors: bool = True, 

224 _parent_namespace_depth: int = 2, 

225 _types_namespace: dict[str, Any] | None = None, 

226 ) -> bool | None: 

227 return cls.update_forward_refs() 

228 

229 def model_dump( 

230 self, 

231 *, 

232 mode: Literal["json", "python"] | str = "python", 

233 include: IncEx = None, 

234 exclude: IncEx = None, 

235 by_alias: bool = False, 

236 exclude_unset: bool = False, 

237 exclude_defaults: bool = False, 

238 exclude_none: bool = False, 

239 round_trip: bool = False, 

240 warnings: bool = True, 

241 ) -> dict[str, Any]: 

242 # Need to decide whether to warn if the mode parameter is given. 

243 return self.dict( 

244 include=include, # type: ignore 

245 exclude=exclude, # type: ignore 

246 by_alias=by_alias, 

247 exclude_unset=exclude_unset, 

248 exclude_defaults=exclude_defaults, 

249 exclude_none=exclude_none, 

250 ) 

251 

252 @classmethod 

253 def model_validate_json( 

254 cls, 

255 json_data: str | bytes | bytearray, 

256 *, 

257 strict: bool | None = None, 

258 context: dict[str, Any] | None = None, 

259 ) -> Self: 

260 return cls.parse_raw(json_data)