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

68 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-27 09:44 +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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

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

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

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

18# (at your option) any later version. 

19# 

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

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

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

23# GNU General Public License for more details. 

24# 

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

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

27 

28"""Code to support backwards compatibility.""" 

29 

30from __future__ import annotations 

31 

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

33 

34import sys 

35from collections.abc import Callable 

36from typing import TYPE_CHECKING, Any, Literal 

37 

38from pydantic import BaseModel 

39from pydantic.fields import FieldInfo 

40from pydantic.version import VERSION as PYDANTIC_VERSION 

41 

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

43 from typing import Self 

44else: 

45 from typing import TypeVar 

46 

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

48 

49 

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

51 

52# This matches the pydantic v2 internal definition. 

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

54 

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

56 

57 class _BaseModelCompat(BaseModel): 

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

59 

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

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

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

63 """ 

64 

65 def json( 

66 self, 

67 *, 

68 include: IncEx = None, # type: ignore 

69 exclude: IncEx = None, # type: ignore 

70 by_alias: bool = False, 

71 skip_defaults: bool | None = None, 

72 exclude_unset: bool = False, 

73 exclude_defaults: bool = False, 

74 exclude_none: bool = False, 

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

76 models_as_dict: bool = True, 

77 **dumps_kwargs: Any, 

78 ) -> str: 

79 if dumps_kwargs: 

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

81 if encoder is not None: 

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

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

84 return self.model_dump_json( 

85 include=include, 

86 exclude=exclude, 

87 by_alias=by_alias, 

88 exclude_defaults=exclude_defaults, 

89 exclude_none=exclude_none, 

90 exclude_unset=exclude_unset, 

91 ) 

92 

93 @classmethod 

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

95 # Catch warnings and call BaseModel.parse_obj directly? 

96 return cls.model_validate(obj) 

97 

98 if TYPE_CHECKING and not PYDANTIC_V2: 

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

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

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

102 

103 @classmethod 

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

105 return cls() 

106 

107 @classmethod 

108 def model_validate( 

109 cls, 

110 obj: Any, 

111 *, 

112 strict: bool | None = None, 

113 from_attributes: bool | None = None, 

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

115 ) -> Self: 

116 return cls() 

117 

118 def model_dump_json( 

119 self, 

120 *, 

121 indent: int | None = None, 

122 include: IncEx = None, 

123 exclude: IncEx = None, 

124 by_alias: bool = False, 

125 exclude_unset: bool = False, 

126 exclude_defaults: bool = False, 

127 exclude_none: bool = False, 

128 round_trip: bool = False, 

129 warnings: bool = True, 

130 ) -> str: 

131 return "" 

132 

133 @property 

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

135 return {} 

136 

137 @classmethod 

138 def model_rebuild( 

139 cls, 

140 *, 

141 force: bool = False, 

142 raise_errors: bool = True, 

143 _parent_namespace_depth: int = 2, 

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

145 ) -> bool | None: 

146 return None 

147 

148 def model_dump( 

149 self, 

150 *, 

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

152 include: IncEx = None, 

153 exclude: IncEx = None, 

154 by_alias: bool = False, 

155 exclude_unset: bool = False, 

156 exclude_defaults: bool = False, 

157 exclude_none: bool = False, 

158 round_trip: bool = False, 

159 warnings: bool = True, 

160 ) -> dict[str, Any]: 

161 return {} 

162 

163 @classmethod 

164 def model_validate_json( 

165 cls, 

166 json_data: str | bytes | bytearray, 

167 *, 

168 strict: bool | None = None, 

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

170 ) -> Self: 

171 return cls() 

172 

173else: 

174 from astropy.utils.decorators import classproperty 

175 

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

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

178 

179 @classmethod 

180 def model_validate( 

181 cls, 

182 obj: Any, 

183 *, 

184 strict: bool | None = None, 

185 from_attributes: bool | None = None, 

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

187 ) -> Self: 

188 return cls.parse_obj(obj) 

189 

190 def model_dump_json( 

191 self, 

192 *, 

193 indent: int | None = None, 

194 include: IncEx = None, 

195 exclude: IncEx = None, 

196 by_alias: bool = False, 

197 exclude_unset: bool = False, 

198 exclude_defaults: bool = False, 

199 exclude_none: bool = False, 

200 round_trip: bool = False, 

201 warnings: bool = True, 

202 ) -> str: 

203 return self.json( 

204 include=include, # type: ignore 

205 exclude=exclude, # type: ignore 

206 by_alias=by_alias, 

207 exclude_unset=exclude_unset, 

208 exclude_defaults=exclude_defaults, 

209 exclude_none=exclude_none, 

210 ) 

211 

212 @classmethod # type: ignore 

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

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

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

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

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

218 

219 @classmethod 

220 @classproperty 

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

222 return cls.__fields__ # type: ignore 

223 

224 @classmethod 

225 def model_rebuild( 

226 cls, 

227 *, 

228 force: bool = False, 

229 raise_errors: bool = True, 

230 _parent_namespace_depth: int = 2, 

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

232 ) -> bool | None: 

233 return cls.update_forward_refs() 

234 

235 def model_dump( 

236 self, 

237 *, 

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

239 include: IncEx = None, 

240 exclude: IncEx = None, 

241 by_alias: bool = False, 

242 exclude_unset: bool = False, 

243 exclude_defaults: bool = False, 

244 exclude_none: bool = False, 

245 round_trip: bool = False, 

246 warnings: bool = True, 

247 ) -> dict[str, Any]: 

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

249 return self.dict( 

250 include=include, # type: ignore 

251 exclude=exclude, # type: ignore 

252 by_alias=by_alias, 

253 exclude_unset=exclude_unset, 

254 exclude_defaults=exclude_defaults, 

255 exclude_none=exclude_none, 

256 ) 

257 

258 @classmethod 

259 def model_validate_json( 

260 cls, 

261 json_data: str | bytes | bytearray, 

262 *, 

263 strict: bool | None = None, 

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

265 ) -> Self: 

266 return cls.parse_raw(json_data)