Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/migrations/serializer.py: 38%

1 import builtins

2 import collections.abc

3 import datetime

4 import decimal

5 import enum

6 import functools

7 import math

8 import os

9 import pathlib

10 import re

11 import types

12 import uuid

13

14 from plain import models

15 from plain.models.migrations.operations.base import Operation

16 from plain.models.migrations.utils import COMPILED_REGEX_TYPE, RegexObject

17 from plain.runtime.user_settings import SettingsReference

18 from plain.utils.functional import LazyObject, Promise

19

20

21 class BaseSerializer:

22 def __init__(self, value):

23 self.value = value

24

25 def serialize(self):

26 raise NotImplementedError(

27 "Subclasses of BaseSerializer must implement the serialize() method."

28 )

29

30

31 class BaseSequenceSerializer(BaseSerializer):

32 def _format(self):

33 raise NotImplementedError(

34 "Subclasses of BaseSequenceSerializer must implement the _format() method."

35 )

36

37 def serialize(self):

38 imports = set()

39 strings = []

40 for item in self.value:

41 item_string, item_imports = serializer_factory(item).serialize()

42 imports.update(item_imports)

43 strings.append(item_string)

44 value = self._format()

45 return value % (", ".join(strings)), imports

46

47

48 class BaseSimpleSerializer(BaseSerializer):

49 def serialize(self):

50 return repr(self.value), set()

51

52

53 class ChoicesSerializer(BaseSerializer):

54 def serialize(self):

55 return serializer_factory(self.value.value).serialize()

56

57

58 class DateTimeSerializer(BaseSerializer):

59 """For datetime.*, except datetime.datetime."""

60

61 def serialize(self):

62 return repr(self.value), {"import datetime"}

63

64

65 class DatetimeDatetimeSerializer(BaseSerializer):

66 """For datetime.datetime."""

67

68 def serialize(self):

69 if self.value.tzinfo is not None and self.value.tzinfo != datetime.UTC:

70 self.value = self.value.astimezone(datetime.UTC)

71 imports = ["import datetime"]

72 return repr(self.value), set(imports)

73

74

75 class DecimalSerializer(BaseSerializer):

76 def serialize(self):

77 return repr(self.value), {"from decimal import Decimal"}

78

79

80 class DeconstructableSerializer(BaseSerializer):

81 @staticmethod

82 def serialize_deconstructed(path, args, kwargs):

83 name, imports = DeconstructableSerializer._serialize_path(path)

84 strings = []

85 for arg in args:

86 arg_string, arg_imports = serializer_factory(arg).serialize()

87 strings.append(arg_string)

88 imports.update(arg_imports)

89 for kw, arg in sorted(kwargs.items()):

90 arg_string, arg_imports = serializer_factory(arg).serialize()

91 imports.update(arg_imports)

92 strings.append(f"{kw}={arg_string}")

93 return "{}({})".format(name, ", ".join(strings)), imports

94

95 @staticmethod

96 def _serialize_path(path):

97 module, name = path.rsplit(".", 1)

98 if module == "plain.models":

99 imports = {"from plain import models"}

100 name = f"models.{name}"

101 else:

102 imports = {f"import {module}"}

103 name = path

104 return name, imports

105

106 def serialize(self):

107 return self.serialize_deconstructed(*self.value.deconstruct())

108

109

110 class DictionarySerializer(BaseSerializer):

111 def serialize(self):

112 imports = set()

113 strings = []

114 for k, v in sorted(self.value.items()):

115 k_string, k_imports = serializer_factory(k).serialize()

116 v_string, v_imports = serializer_factory(v).serialize()

117 imports.update(k_imports)

118 imports.update(v_imports)

119 strings.append((k_string, v_string))

120 return "{{{}}}".format(", ".join(f"{k}: {v}" for k, v in strings)), imports

121

122

123 class EnumSerializer(BaseSerializer):

124 def serialize(self):

125 enum_class = self.value.__class__

126 module = enum_class.__module__

127 if issubclass(enum_class, enum.Flag):

128 members = list(self.value)

129 else:

130 members = (self.value,)

131 return (

132 " | ".join(

133 [

134 f"{module}.{enum_class.__qualname__}[{item.name! r}]"

135 for item in members

136 ]

137 ),

138 {f"import {module}"},

139 )

140

141

142 class FloatSerializer(BaseSimpleSerializer):

143 def serialize(self):

144 if math.isnan(self.value) or math.isinf(self.value):

145 return f'float("{self.value}")', set()

146 return super().serialize()

147

148

149 class FrozensetSerializer(BaseSequenceSerializer):

150 def _format(self):

151 return "frozenset([%s])"

152

153

154 class FunctionTypeSerializer(BaseSerializer):

155 def serialize(self):

156 if getattr(self.value, "__self__", None) and isinstance(

157 self.value.__self__, type

158 ):

159 klass = self.value.__self__

160 module = klass.__module__

161 return f"{module}.{klass.__name__}.{self.value.__name__}", {

162 f"import {module}"

163 }

164 # Further error checking

165 if self.value.__name__ == "<lambda>":

166 raise ValueError("Cannot serialize function: lambda")

167 if self.value.__module__ is None:

168 raise ValueError(f"Cannot serialize function {self.value! r}: No module")

169

170 module_name = self.value.__module__

171

172 if "<" not in self.value.__qualname__: # Qualname can include <locals>

173 return f"{module_name}.{self.value.__qualname__}", {

174 f"import {self.value.__module__}"

175 }

176

177 raise ValueError(

178 f"Could not find function {self.value.__name__} in {module_name}.\n"

179 )

180

181

182 class FunctoolsPartialSerializer(BaseSerializer):

183 def serialize(self):

184 # Serialize functools.partial() arguments

185 func_string, func_imports = serializer_factory(self.value.func).serialize()

186 args_string, args_imports = serializer_factory(self.value.args).serialize()

187 keywords_string, keywords_imports = serializer_factory(

188 self.value.keywords

189 ).serialize()

190 # Add any imports needed by arguments

191 imports = {"import functools", *func_imports, *args_imports, *keywords_imports}

192 return (

193 f"functools.{self.value.__class__.__name__}({func_string}, *{args_string}, **{keywords_string})",

194 imports,

195 )

196

197

198 class IterableSerializer(BaseSerializer):

199 def serialize(self):

200 imports = set()

201 strings = []

202 for item in self.value:

203 item_string, item_imports = serializer_factory(item).serialize()

204 imports.update(item_imports)

205 strings.append(item_string)

206 # When len(strings)==0, the empty iterable should be serialized as

207 # "()", not "(,)" because (,) is invalid Python syntax.

208 value = "(%s)" if len(strings) != 1 else "(%s,)"

209 return value % (", ".join(strings)), imports

210

211

212 class ModelFieldSerializer(DeconstructableSerializer):

213 def serialize(self):

214 attr_name, path, args, kwargs = self.value.deconstruct()

215 return self.serialize_deconstructed(path, args, kwargs)

216

217

218 class ModelManagerSerializer(DeconstructableSerializer):

219 def serialize(self):

220 as_manager, manager_path, qs_path, args, kwargs = self.value.deconstruct()

221 if as_manager:

222 name, imports = self._serialize_path(qs_path)

223 return f"{name}.as_manager()", imports

224 else:

225 return self.serialize_deconstructed(manager_path, args, kwargs)

226

227

228 class OperationSerializer(BaseSerializer):

229 def serialize(self):

230 from plain.models.migrations.writer import OperationWriter

231

232 string, imports = OperationWriter(self.value, indentation=0).serialize()

233 # Nested operation, trailing comma is handled in upper OperationWriter._write()

234 return string.rstrip(","), imports

235

236

237 class PathLikeSerializer(BaseSerializer):

238 def serialize(self):

239 return repr(os.fspath(self.value)), {}

240

241

242 class PathSerializer(BaseSerializer):

243 def serialize(self):

244 # Convert concrete paths to pure paths to avoid issues with migrations

245 # generated on one platform being used on a different platform.

246 prefix = "Pure" if isinstance(self.value, pathlib.Path) else ""

247 return f"pathlib.{prefix}{self.value! r}", {"import pathlib"}

248

249

250 class RegexSerializer(BaseSerializer):

251 def serialize(self):

252 regex_pattern, pattern_imports = serializer_factory(

253 self.value.pattern

254 ).serialize()

255 # Turn off default implicit flags (e.g. re.U) because regexes with the

256 # same implicit and explicit flags aren't equal.

257 flags = self.value.flags ^ re.compile("").flags

258 regex_flags, flag_imports = serializer_factory(flags).serialize()

259 imports = {"import re", *pattern_imports, *flag_imports}

260 args = [regex_pattern]

261 if flags:

262 args.append(regex_flags)

263 return "re.compile({})".format(", ".join(args)), imports

264

265

266 class SequenceSerializer(BaseSequenceSerializer):

267 def _format(self):

268 return "[%s]"

269

270

271 class SetSerializer(BaseSequenceSerializer):

272 def _format(self):

273 # Serialize as a set literal except when value is empty because {}

274 # is an empty dict.

275 return "{%s}" if self.value else "set(%s)"

276

277

278 class SettingsReferenceSerializer(BaseSerializer):

279 def serialize(self):

280 return f"settings.{self.value.setting_name}", {

281 "from plain.runtime import settings"

282 }

283

284

285 class TupleSerializer(BaseSequenceSerializer):

286 def _format(self):

287 # When len(value)==0, the empty tuple should be serialized as "()",

288 # not "(,)" because (,) is invalid Python syntax.

289 return "(%s)" if len(self.value) != 1 else "(%s,)"

290

291

292 class TypeSerializer(BaseSerializer):

293 def serialize(self):

294 special_cases = [

295 (models.Model, "models.Model", ["from plain import models"]),

296 (types.NoneType, "types.NoneType", ["import types"]),

297 ]

298 for case, string, imports in special_cases:

299 if case is self.value:

300 return string, set(imports)

301 if hasattr(self.value, "__module__"):

302 module = self.value.__module__

303 if module == builtins.__name__:

304 return self.value.__name__, set()

305 else:

306 return f"{module}.{self.value.__qualname__}", {f"import {module}"}

307

308

309 class UUIDSerializer(BaseSerializer):

310 def serialize(self):

311 return f"uuid.{repr(self.value)}", {"import uuid"}

312

313

314 class Serializer:

315 _registry = {

316 # Some of these are order-dependent.

317 frozenset: FrozensetSerializer,

318 list: SequenceSerializer,

319 set: SetSerializer,

320 tuple: TupleSerializer,

321 dict: DictionarySerializer,

322 models.Choices: ChoicesSerializer,

323 enum.Enum: EnumSerializer,

324 datetime.datetime: DatetimeDatetimeSerializer,

325 (datetime.date, datetime.timedelta, datetime.time): DateTimeSerializer,

326 SettingsReference: SettingsReferenceSerializer,

327 float: FloatSerializer,

328 (bool, int, types.NoneType, bytes, str, range): BaseSimpleSerializer,

329 decimal.Decimal: DecimalSerializer,

330 (functools.partial, functools.partialmethod): FunctoolsPartialSerializer,

331 (

332 types.FunctionType,

333 types.BuiltinFunctionType,

334 types.MethodType,

335 ): FunctionTypeSerializer,

336 collections.abc.Iterable: IterableSerializer,

337 (COMPILED_REGEX_TYPE, RegexObject): RegexSerializer,

338 uuid.UUID: UUIDSerializer,

339 pathlib.PurePath: PathSerializer,

340 os.PathLike: PathLikeSerializer,

341 }

342

343 @classmethod

344 def register(cls, type_, serializer):

345 if not issubclass(serializer, BaseSerializer):

346 raise ValueError(

347 f"'{serializer.__name__}' must inherit from 'BaseSerializer'."

348 )

349 cls._registry[type_] = serializer

350

351 @classmethod

352 def unregister(cls, type_):

353 cls._registry.pop(type_)

354

355

356 def serializer_factory(value):

357 if isinstance(value, Promise):

358 value = str(value)

359 elif isinstance(value, LazyObject):

360 # The unwrapped value is returned as the first item of the arguments

361 # tuple.

362 value = value.__reduce__()[1][0]

363

364 if isinstance(value, models.Field):

365 return ModelFieldSerializer(value)

366 if isinstance(value, models.manager.BaseManager):

367 return ModelManagerSerializer(value)

368 if isinstance(value, Operation):

369 return OperationSerializer(value)

370 if isinstance(value, type):

371 return TypeSerializer(value)

372 # Anything that knows how to deconstruct itself.

373 if hasattr(value, "deconstruct"):

374 return DeconstructableSerializer(value)

375 for type_, serializer_cls in Serializer._registry.items():

376 if isinstance(value, type_):

377 return serializer_cls(value)

378 raise ValueError(

379 f"Cannot serialize: {value! r}\nThere are some values Plain cannot serialize into "

380 "migration files."

381 )