Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/options.py: 68%

421 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:03 -0500

1import bisect 

2import copy 

3import inspect 

4from collections import defaultdict 

5 

6from plain.exceptions import FieldDoesNotExist 

7from plain.models.constraints import UniqueConstraint 

8from plain.models.db import connections 

9from plain.models.fields import BigAutoField 

10from plain.models.fields.proxy import OrderWrt 

11from plain.models.manager import Manager 

12from plain.models.query_utils import PathInfo 

13from plain.packages import packages 

14from plain.runtime import settings 

15from plain.utils.datastructures import ImmutableList, OrderedSet 

16from plain.utils.functional import cached_property 

17 

18PROXY_PARENTS = object() 

19 

20EMPTY_RELATION_TREE = () 

21 

22IMMUTABLE_WARNING = ( 

23 "The return type of '%s' should never be mutated. If you want to manipulate this " 

24 "list for your own use, make a copy first." 

25) 

26 

27DEFAULT_NAMES = ( 

28 "db_table", 

29 "db_table_comment", 

30 "ordering", 

31 "get_latest_by", 

32 "order_with_respect_to", 

33 "package_label", 

34 "db_tablespace", 

35 "abstract", 

36 "managed", 

37 "swappable", 

38 "auto_created", 

39 "packages", 

40 "select_on_save", 

41 "default_related_name", 

42 "required_db_features", 

43 "required_db_vendor", 

44 "base_manager_name", 

45 "default_manager_name", 

46 "indexes", 

47 "constraints", 

48) 

49 

50 

51def make_immutable_fields_list(name, data): 

52 return ImmutableList(data, warning=IMMUTABLE_WARNING % name) 

53 

54 

55class Options: 

56 FORWARD_PROPERTIES = { 

57 "fields", 

58 "many_to_many", 

59 "concrete_fields", 

60 "local_concrete_fields", 

61 "_non_pk_concrete_field_names", 

62 "_forward_fields_map", 

63 "managers", 

64 "managers_map", 

65 "base_manager", 

66 "default_manager", 

67 } 

68 REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"} 

69 

70 default_packages = packages 

71 

72 def __init__(self, meta, package_label=None): 

73 self._get_fields_cache = {} 

74 self.local_fields = [] 

75 self.local_many_to_many = [] 

76 self.private_fields = [] 

77 self.local_managers = [] 

78 self.base_manager_name = None 

79 self.default_manager_name = None 

80 self.model_name = None 

81 self.db_table = "" 

82 self.db_table_comment = "" 

83 self.ordering = [] 

84 self._ordering_clash = False 

85 self.indexes = [] 

86 self.constraints = [] 

87 self.select_on_save = False 

88 self.object_name = None 

89 self.package_label = package_label 

90 self.get_latest_by = None 

91 self.order_with_respect_to = None 

92 self.db_tablespace = settings.DEFAULT_TABLESPACE 

93 self.required_db_features = [] 

94 self.required_db_vendor = None 

95 self.meta = meta 

96 self.pk = None 

97 self.auto_field = None 

98 self.abstract = False 

99 self.managed = True 

100 # For any non-abstract class, the concrete class is the model 

101 # in the end of the proxy_for_model chain. In particular, for 

102 # concrete models, the concrete_model is always the class itself. 

103 self.concrete_model = None 

104 self.swappable = None 

105 self.parents = {} 

106 self.auto_created = False 

107 

108 # List of all lookups defined in ForeignKey 'limit_choices_to' options 

109 # from *other* models. Needed for some admin checks. Internal use only. 

110 self.related_fkey_lookups = [] 

111 

112 # A custom app registry to use, if you're making a separate model set. 

113 self.packages = self.default_packages 

114 

115 self.default_related_name = None 

116 

117 @property 

118 def label(self): 

119 return f"{self.package_label}.{self.object_name}" 

120 

121 @property 

122 def label_lower(self): 

123 return f"{self.package_label}.{self.model_name}" 

124 

125 @property 

126 def package_config(self): 

127 # Don't go through get_package_config to avoid triggering imports. 

128 return self.packages.package_configs.get(self.package_label) 

129 

130 def contribute_to_class(self, cls, name): 

131 from plain.models.backends.utils import truncate_name 

132 from plain.models.db import connection 

133 

134 cls._meta = self 

135 self.model = cls 

136 # First, construct the default values for these options. 

137 self.object_name = cls.__name__ 

138 self.model_name = self.object_name.lower() 

139 

140 # Store the original user-defined values for each option, 

141 # for use when serializing the model definition 

142 self.original_attrs = {} 

143 

144 # Next, apply any overridden values from 'class Meta'. 

145 if self.meta: 

146 meta_attrs = self.meta.__dict__.copy() 

147 for name in self.meta.__dict__: 

148 # Ignore any private attributes that Plain doesn't care about. 

149 # NOTE: We can't modify a dictionary's contents while looping 

150 # over it, so we loop over the *original* dictionary instead. 

151 if name.startswith("_"): 

152 del meta_attrs[name] 

153 for attr_name in DEFAULT_NAMES: 

154 if attr_name in meta_attrs: 

155 setattr(self, attr_name, meta_attrs.pop(attr_name)) 

156 self.original_attrs[attr_name] = getattr(self, attr_name) 

157 elif hasattr(self.meta, attr_name): 

158 setattr(self, attr_name, getattr(self.meta, attr_name)) 

159 self.original_attrs[attr_name] = getattr(self, attr_name) 

160 

161 # Package label/class name interpolation for names of constraints and 

162 # indexes. 

163 if not getattr(cls._meta, "abstract", False): 

164 for attr_name in {"constraints", "indexes"}: 

165 objs = getattr(self, attr_name, []) 

166 setattr(self, attr_name, self._format_names_with_class(cls, objs)) 

167 

168 # order_with_respect_and ordering are mutually exclusive. 

169 self._ordering_clash = bool(self.ordering and self.order_with_respect_to) 

170 

171 # Any leftover attributes must be invalid. 

172 if meta_attrs != {}: 

173 raise TypeError( 

174 "'class Meta' got invalid attribute(s): %s" % ",".join(meta_attrs) 

175 ) 

176 

177 del self.meta 

178 

179 # If the db_table wasn't provided, use the package_label + model_name. 

180 if not self.db_table: 

181 self.db_table = f"{self.package_label}_{self.model_name}" 

182 self.db_table = truncate_name( 

183 self.db_table, connection.ops.max_name_length() 

184 ) 

185 

186 def _format_names_with_class(self, cls, objs): 

187 """Package label/class name interpolation for object names.""" 

188 new_objs = [] 

189 for obj in objs: 

190 obj = obj.clone() 

191 obj.name = obj.name % { 

192 "package_label": cls._meta.package_label.lower(), 

193 "class": cls.__name__.lower(), 

194 } 

195 new_objs.append(obj) 

196 return new_objs 

197 

198 def _prepare(self, model): 

199 if self.order_with_respect_to: 

200 # The app registry will not be ready at this point, so we cannot 

201 # use get_field(). 

202 query = self.order_with_respect_to 

203 try: 

204 self.order_with_respect_to = next( 

205 f 

206 for f in self._get_fields(reverse=False) 

207 if f.name == query or f.attname == query 

208 ) 

209 except StopIteration: 

210 raise FieldDoesNotExist( 

211 f"{self.object_name} has no field named '{query}'" 

212 ) 

213 

214 self.ordering = ("_order",) 

215 if not any( 

216 isinstance(field, OrderWrt) for field in model._meta.local_fields 

217 ): 

218 model.add_to_class("_order", OrderWrt()) 

219 else: 

220 self.order_with_respect_to = None 

221 

222 if self.pk is None: 

223 if self.parents: 

224 # Promote the first parent link in lieu of adding yet another 

225 # field. 

226 field = next(iter(self.parents.values())) 

227 # Look for a local field with the same name as the 

228 # first parent link. If a local field has already been 

229 # created, use it instead of promoting the parent 

230 already_created = [ 

231 fld for fld in self.local_fields if fld.name == field.name 

232 ] 

233 if already_created: 

234 field = already_created[0] 

235 field.primary_key = True 

236 self.setup_pk(field) 

237 else: 

238 auto = BigAutoField(primary_key=True, auto_created=True) 

239 model.add_to_class("id", auto) 

240 

241 def add_manager(self, manager): 

242 self.local_managers.append(manager) 

243 self._expire_cache() 

244 

245 def add_field(self, field, private=False): 

246 # Insert the given field in the order in which it was created, using 

247 # the "creation_counter" attribute of the field. 

248 # Move many-to-many related fields from self.fields into 

249 # self.many_to_many. 

250 if private: 

251 self.private_fields.append(field) 

252 elif field.is_relation and field.many_to_many: 

253 bisect.insort(self.local_many_to_many, field) 

254 else: 

255 bisect.insort(self.local_fields, field) 

256 self.setup_pk(field) 

257 

258 # If the field being added is a relation to another known field, 

259 # expire the cache on this field and the forward cache on the field 

260 # being referenced, because there will be new relationships in the 

261 # cache. Otherwise, expire the cache of references *to* this field. 

262 # The mechanism for getting at the related model is slightly odd - 

263 # ideally, we'd just ask for field.related_model. However, related_model 

264 # is a cached property, and all the models haven't been loaded yet, so 

265 # we need to make sure we don't cache a string reference. 

266 if ( 

267 field.is_relation 

268 and hasattr(field.remote_field, "model") 

269 and field.remote_field.model 

270 ): 

271 try: 

272 field.remote_field.model._meta._expire_cache(forward=False) 

273 except AttributeError: 

274 pass 

275 self._expire_cache() 

276 else: 

277 self._expire_cache(reverse=False) 

278 

279 def setup_pk(self, field): 

280 if not self.pk and field.primary_key: 

281 self.pk = field 

282 

283 def __repr__(self): 

284 return "<Options for %s>" % self.object_name 

285 

286 def __str__(self): 

287 return self.label_lower 

288 

289 def can_migrate(self, connection): 

290 """ 

291 Return True if the model can/should be migrated on the `connection`. 

292 `connection` can be either a real connection or a connection alias. 

293 """ 

294 if self.swapped or not self.managed: 

295 return False 

296 if isinstance(connection, str): 

297 connection = connections[connection] 

298 if self.required_db_vendor: 

299 return self.required_db_vendor == connection.vendor 

300 if self.required_db_features: 

301 return all( 

302 getattr(connection.features, feat, False) 

303 for feat in self.required_db_features 

304 ) 

305 return True 

306 

307 @property 

308 def swapped(self): 

309 """ 

310 Has this model been swapped out for another? If so, return the model 

311 name of the replacement; otherwise, return None. 

312 

313 For historical reasons, model name lookups using get_model() are 

314 case insensitive, so we make sure we are case insensitive here. 

315 """ 

316 if self.swappable: 

317 swapped_for = getattr(settings, self.swappable, None) 

318 if swapped_for: 

319 try: 

320 swapped_label, swapped_object = swapped_for.split(".") 

321 except ValueError: 

322 # setting not in the format package_label.model_name 

323 # raising ImproperlyConfigured here causes problems with 

324 # test cleanup code - instead it is raised in get_user_model 

325 # or as part of validation. 

326 return swapped_for 

327 

328 if f"{swapped_label}.{swapped_object.lower()}" != self.label_lower: 

329 return swapped_for 

330 return None 

331 

332 @cached_property 

333 def managers(self): 

334 managers = [] 

335 seen_managers = set() 

336 bases = (b for b in self.model.mro() if hasattr(b, "_meta")) 

337 for depth, base in enumerate(bases): 

338 for manager in base._meta.local_managers: 

339 if manager.name in seen_managers: 

340 continue 

341 

342 manager = copy.copy(manager) 

343 manager.model = self.model 

344 seen_managers.add(manager.name) 

345 managers.append((depth, manager.creation_counter, manager)) 

346 

347 return make_immutable_fields_list( 

348 "managers", 

349 (m[2] for m in sorted(managers)), 

350 ) 

351 

352 @cached_property 

353 def managers_map(self): 

354 return {manager.name: manager for manager in self.managers} 

355 

356 @cached_property 

357 def base_manager(self): 

358 base_manager_name = self.base_manager_name 

359 if not base_manager_name: 

360 # Get the first parent's base_manager_name if there's one. 

361 for parent in self.model.mro()[1:]: 

362 if hasattr(parent, "_meta"): 

363 if parent._base_manager.name != "_base_manager": 

364 base_manager_name = parent._base_manager.name 

365 break 

366 

367 if base_manager_name: 

368 try: 

369 return self.managers_map[base_manager_name] 

370 except KeyError: 

371 raise ValueError( 

372 f"{self.object_name} has no manager named {base_manager_name!r}" 

373 ) 

374 

375 manager = Manager() 

376 manager.name = "_base_manager" 

377 manager.model = self.model 

378 manager.auto_created = True 

379 return manager 

380 

381 @cached_property 

382 def default_manager(self): 

383 default_manager_name = self.default_manager_name 

384 if not default_manager_name and not self.local_managers: 

385 # Get the first parent's default_manager_name if there's one. 

386 for parent in self.model.mro()[1:]: 

387 if hasattr(parent, "_meta"): 

388 default_manager_name = parent._meta.default_manager_name 

389 break 

390 

391 if default_manager_name: 

392 try: 

393 return self.managers_map[default_manager_name] 

394 except KeyError: 

395 raise ValueError( 

396 f"{self.object_name} has no manager named {default_manager_name!r}" 

397 ) 

398 

399 if self.managers: 

400 return self.managers[0] 

401 

402 @cached_property 

403 def fields(self): 

404 """ 

405 Return a list of all forward fields on the model and its parents, 

406 excluding ManyToManyFields. 

407 

408 Private API intended only to be used by Plain itself; get_fields() 

409 combined with filtering of field properties is the public API for 

410 obtaining this field list. 

411 """ 

412 

413 # For legacy reasons, the fields property should only contain forward 

414 # fields that are not private or with a m2m cardinality. Therefore we 

415 # pass these three filters as filters to the generator. 

416 # The third lambda is a longwinded way of checking f.related_model - we don't 

417 # use that property directly because related_model is a cached property, 

418 # and all the models may not have been loaded yet; we don't want to cache 

419 # the string reference to the related_model. 

420 def is_not_an_m2m_field(f): 

421 return not (f.is_relation and f.many_to_many) 

422 

423 def is_not_a_generic_relation(f): 

424 return not (f.is_relation and f.one_to_many) 

425 

426 def is_not_a_generic_foreign_key(f): 

427 return not ( 

428 f.is_relation 

429 and f.many_to_one 

430 and not (hasattr(f.remote_field, "model") and f.remote_field.model) 

431 ) 

432 

433 return make_immutable_fields_list( 

434 "fields", 

435 ( 

436 f 

437 for f in self._get_fields(reverse=False) 

438 if is_not_an_m2m_field(f) 

439 and is_not_a_generic_relation(f) 

440 and is_not_a_generic_foreign_key(f) 

441 ), 

442 ) 

443 

444 @cached_property 

445 def concrete_fields(self): 

446 """ 

447 Return a list of all concrete fields on the model and its parents. 

448 

449 Private API intended only to be used by Plain itself; get_fields() 

450 combined with filtering of field properties is the public API for 

451 obtaining this field list. 

452 """ 

453 return make_immutable_fields_list( 

454 "concrete_fields", (f for f in self.fields if f.concrete) 

455 ) 

456 

457 @cached_property 

458 def local_concrete_fields(self): 

459 """ 

460 Return a list of all concrete fields on the model. 

461 

462 Private API intended only to be used by Plain itself; get_fields() 

463 combined with filtering of field properties is the public API for 

464 obtaining this field list. 

465 """ 

466 return make_immutable_fields_list( 

467 "local_concrete_fields", (f for f in self.local_fields if f.concrete) 

468 ) 

469 

470 @cached_property 

471 def many_to_many(self): 

472 """ 

473 Return a list of all many to many fields on the model and its parents. 

474 

475 Private API intended only to be used by Plain itself; get_fields() 

476 combined with filtering of field properties is the public API for 

477 obtaining this list. 

478 """ 

479 return make_immutable_fields_list( 

480 "many_to_many", 

481 ( 

482 f 

483 for f in self._get_fields(reverse=False) 

484 if f.is_relation and f.many_to_many 

485 ), 

486 ) 

487 

488 @cached_property 

489 def related_objects(self): 

490 """ 

491 Return all related objects pointing to the current model. The related 

492 objects can come from a one-to-one, one-to-many, or many-to-many field 

493 relation type. 

494 

495 Private API intended only to be used by Plain itself; get_fields() 

496 combined with filtering of field properties is the public API for 

497 obtaining this field list. 

498 """ 

499 all_related_fields = self._get_fields( 

500 forward=False, reverse=True, include_hidden=True 

501 ) 

502 return make_immutable_fields_list( 

503 "related_objects", 

504 ( 

505 obj 

506 for obj in all_related_fields 

507 if not obj.hidden or obj.field.many_to_many 

508 ), 

509 ) 

510 

511 @cached_property 

512 def _forward_fields_map(self): 

513 res = {} 

514 fields = self._get_fields(reverse=False) 

515 for field in fields: 

516 res[field.name] = field 

517 # Due to the way Plain's internals work, get_field() should also 

518 # be able to fetch a field by attname. In the case of a concrete 

519 # field with relation, includes the *_id name too 

520 try: 

521 res[field.attname] = field 

522 except AttributeError: 

523 pass 

524 return res 

525 

526 @cached_property 

527 def fields_map(self): 

528 res = {} 

529 fields = self._get_fields(forward=False, include_hidden=True) 

530 for field in fields: 

531 res[field.name] = field 

532 # Due to the way Plain's internals work, get_field() should also 

533 # be able to fetch a field by attname. In the case of a concrete 

534 # field with relation, includes the *_id name too 

535 try: 

536 res[field.attname] = field 

537 except AttributeError: 

538 pass 

539 return res 

540 

541 def get_field(self, field_name): 

542 """ 

543 Return a field instance given the name of a forward or reverse field. 

544 """ 

545 try: 

546 # In order to avoid premature loading of the relation tree 

547 # (expensive) we prefer checking if the field is a forward field. 

548 return self._forward_fields_map[field_name] 

549 except KeyError: 

550 # If the app registry is not ready, reverse fields are 

551 # unavailable, therefore we throw a FieldDoesNotExist exception. 

552 if not self.packages.models_ready: 

553 raise FieldDoesNotExist( 

554 "{} has no field named '{}'. The app cache isn't ready yet, " 

555 "so if this is an auto-created related field, it won't " 

556 "be available yet.".format(self.object_name, field_name) 

557 ) 

558 

559 try: 

560 # Retrieve field instance by name from cached or just-computed 

561 # field map. 

562 return self.fields_map[field_name] 

563 except KeyError: 

564 raise FieldDoesNotExist( 

565 f"{self.object_name} has no field named '{field_name}'" 

566 ) 

567 

568 def get_base_chain(self, model): 

569 """ 

570 Return a list of parent classes leading to `model` (ordered from 

571 closest to most distant ancestor). This has to handle the case where 

572 `model` is a grandparent or even more distant relation. 

573 """ 

574 if not self.parents: 

575 return [] 

576 if model in self.parents: 

577 return [model] 

578 for parent in self.parents: 

579 res = parent._meta.get_base_chain(model) 

580 if res: 

581 res.insert(0, parent) 

582 return res 

583 return [] 

584 

585 def get_parent_list(self): 

586 """ 

587 Return all the ancestors of this model as a list ordered by MRO. 

588 Useful for determining if something is an ancestor, regardless of lineage. 

589 """ 

590 result = OrderedSet(self.parents) 

591 for parent in self.parents: 

592 for ancestor in parent._meta.get_parent_list(): 

593 result.add(ancestor) 

594 return list(result) 

595 

596 def get_ancestor_link(self, ancestor): 

597 """ 

598 Return the field on the current model which points to the given 

599 "ancestor". This is possible an indirect link (a pointer to a parent 

600 model, which points, eventually, to the ancestor). Used when 

601 constructing table joins for model inheritance. 

602 

603 Return None if the model isn't an ancestor of this one. 

604 """ 

605 if ancestor in self.parents: 

606 return self.parents[ancestor] 

607 for parent in self.parents: 

608 # Tries to get a link field from the immediate parent 

609 parent_link = parent._meta.get_ancestor_link(ancestor) 

610 if parent_link: 

611 # In case of a proxied model, the first link 

612 # of the chain to the ancestor is that parent 

613 # links 

614 return self.parents[parent] or parent_link 

615 

616 def get_path_to_parent(self, parent): 

617 """ 

618 Return a list of PathInfos containing the path from the current 

619 model to the parent model, or an empty list if parent is not a 

620 parent of the current model. 

621 """ 

622 if self.model is parent: 

623 return [] 

624 # Skip the chain of proxy to the concrete proxied model. 

625 proxied_model = self.concrete_model 

626 path = [] 

627 opts = self 

628 for int_model in self.get_base_chain(parent): 

629 if int_model is proxied_model: 

630 opts = int_model._meta 

631 else: 

632 final_field = opts.parents[int_model] 

633 targets = (final_field.remote_field.get_related_field(),) 

634 opts = int_model._meta 

635 path.append( 

636 PathInfo( 

637 from_opts=final_field.model._meta, 

638 to_opts=opts, 

639 target_fields=targets, 

640 join_field=final_field, 

641 m2m=False, 

642 direct=True, 

643 filtered_relation=None, 

644 ) 

645 ) 

646 return path 

647 

648 def get_path_from_parent(self, parent): 

649 """ 

650 Return a list of PathInfos containing the path from the parent 

651 model to the current model, or an empty list if parent is not a 

652 parent of the current model. 

653 """ 

654 if self.model is parent: 

655 return [] 

656 model = self.concrete_model 

657 # Get a reversed base chain including both the current and parent 

658 # models. 

659 chain = model._meta.get_base_chain(parent) 

660 chain.reverse() 

661 chain.append(model) 

662 # Construct a list of the PathInfos between models in chain. 

663 path = [] 

664 for i, ancestor in enumerate(chain[:-1]): 

665 child = chain[i + 1] 

666 link = child._meta.get_ancestor_link(ancestor) 

667 path.extend(link.reverse_path_infos) 

668 return path 

669 

670 def _populate_directed_relation_graph(self): 

671 """ 

672 This method is used by each model to find its reverse objects. As this 

673 method is very expensive and is accessed frequently (it looks up every 

674 field in a model, in every app), it is computed on first access and then 

675 is set as a property on every model. 

676 """ 

677 related_objects_graph = defaultdict(list) 

678 

679 all_models = self.packages.get_models(include_auto_created=True) 

680 for model in all_models: 

681 opts = model._meta 

682 # Abstract model's fields are copied to child models, hence we will 

683 # see the fields from the child models. 

684 if opts.abstract: 

685 continue 

686 fields_with_relations = ( 

687 f 

688 for f in opts._get_fields(reverse=False, include_parents=False) 

689 if f.is_relation and f.related_model is not None 

690 ) 

691 for f in fields_with_relations: 

692 if not isinstance(f.remote_field.model, str): 

693 remote_label = f.remote_field.model._meta.concrete_model._meta.label 

694 related_objects_graph[remote_label].append(f) 

695 

696 for model in all_models: 

697 # Set the relation_tree using the internal __dict__. In this way 

698 # we avoid calling the cached property. In attribute lookup, 

699 # __dict__ takes precedence over a data descriptor (such as 

700 # @cached_property). This means that the _meta._relation_tree is 

701 # only called if related_objects is not in __dict__. 

702 related_objects = related_objects_graph[ 

703 model._meta.concrete_model._meta.label 

704 ] 

705 model._meta.__dict__["_relation_tree"] = related_objects 

706 # It seems it is possible that self is not in all_models, so guard 

707 # against that with default for get(). 

708 return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE) 

709 

710 @cached_property 

711 def _relation_tree(self): 

712 return self._populate_directed_relation_graph() 

713 

714 def _expire_cache(self, forward=True, reverse=True): 

715 # This method is usually called by packages.cache_clear(), when the 

716 # registry is finalized, or when a new field is added. 

717 if forward: 

718 for cache_key in self.FORWARD_PROPERTIES: 

719 if cache_key in self.__dict__: 

720 delattr(self, cache_key) 

721 if reverse and not self.abstract: 

722 for cache_key in self.REVERSE_PROPERTIES: 

723 if cache_key in self.__dict__: 

724 delattr(self, cache_key) 

725 self._get_fields_cache = {} 

726 

727 def get_fields(self, include_parents=True, include_hidden=False): 

728 """ 

729 Return a list of fields associated to the model. By default, include 

730 forward and reverse fields, fields derived from inheritance, but not 

731 hidden fields. The returned fields can be changed using the parameters: 

732 

733 - include_parents: include fields derived from inheritance 

734 - include_hidden: include fields that have a related_name that 

735 starts with a "+" 

736 """ 

737 if include_parents is False: 

738 include_parents = PROXY_PARENTS 

739 return self._get_fields( 

740 include_parents=include_parents, include_hidden=include_hidden 

741 ) 

742 

743 def _get_fields( 

744 self, 

745 forward=True, 

746 reverse=True, 

747 include_parents=True, 

748 include_hidden=False, 

749 seen_models=None, 

750 ): 

751 """ 

752 Internal helper function to return fields of the model. 

753 * If forward=True, then fields defined on this model are returned. 

754 * If reverse=True, then relations pointing to this model are returned. 

755 * If include_hidden=True, then fields with is_hidden=True are returned. 

756 * The include_parents argument toggles if fields from parent models 

757 should be included. It has three values: True, False, and 

758 PROXY_PARENTS. When set to PROXY_PARENTS, the call will return all 

759 fields defined for the current model or any of its parents in the 

760 parent chain to the model's concrete model. 

761 """ 

762 if include_parents not in (True, False, PROXY_PARENTS): 

763 raise TypeError(f"Invalid argument for include_parents: {include_parents}") 

764 # This helper function is used to allow recursion in ``get_fields()`` 

765 # implementation and to provide a fast way for Plain's internals to 

766 # access specific subsets of fields. 

767 

768 # We must keep track of which models we have already seen. Otherwise we 

769 # could include the same field multiple times from different models. 

770 topmost_call = seen_models is None 

771 if topmost_call: 

772 seen_models = set() 

773 seen_models.add(self.model) 

774 

775 # Creates a cache key composed of all arguments 

776 cache_key = (forward, reverse, include_parents, include_hidden, topmost_call) 

777 

778 try: 

779 # In order to avoid list manipulation. Always return a shallow copy 

780 # of the results. 

781 return self._get_fields_cache[cache_key] 

782 except KeyError: 

783 pass 

784 

785 fields = [] 

786 # Recursively call _get_fields() on each parent, with the same 

787 # options provided in this call. 

788 if include_parents is not False: 

789 for parent in self.parents: 

790 # In diamond inheritance it is possible that we see the same 

791 # model from two different routes. In that case, avoid adding 

792 # fields from the same parent again. 

793 if parent in seen_models: 

794 continue 

795 if ( 

796 parent._meta.concrete_model != self.concrete_model 

797 and include_parents == PROXY_PARENTS 

798 ): 

799 continue 

800 for obj in parent._meta._get_fields( 

801 forward=forward, 

802 reverse=reverse, 

803 include_parents=include_parents, 

804 include_hidden=include_hidden, 

805 seen_models=seen_models, 

806 ): 

807 if ( 

808 not getattr(obj, "parent_link", False) 

809 or obj.model == self.concrete_model 

810 ): 

811 fields.append(obj) 

812 if reverse: 

813 # Tree is computed once and cached until the app cache is expired. 

814 # It is composed of a list of fields pointing to the current model 

815 # from other models. 

816 all_fields = self._relation_tree 

817 for field in all_fields: 

818 # If hidden fields should be included or the relation is not 

819 # intentionally hidden, add to the fields dict. 

820 if include_hidden or not field.remote_field.hidden: 

821 fields.append(field.remote_field) 

822 

823 if forward: 

824 fields += self.local_fields 

825 fields += self.local_many_to_many 

826 # Private fields are recopied to each child model, and they get a 

827 # different model as field.model in each child. Hence we have to 

828 # add the private fields separately from the topmost call. If we 

829 # did this recursively similar to local_fields, we would get field 

830 # instances with field.model != self.model. 

831 if topmost_call: 

832 fields += self.private_fields 

833 

834 # In order to avoid list manipulation. Always 

835 # return a shallow copy of the results 

836 fields = make_immutable_fields_list("get_fields()", fields) 

837 

838 # Store result into cache for later access 

839 self._get_fields_cache[cache_key] = fields 

840 return fields 

841 

842 @cached_property 

843 def total_unique_constraints(self): 

844 """ 

845 Return a list of total unique constraints. Useful for determining set 

846 of fields guaranteed to be unique for all rows. 

847 """ 

848 return [ 

849 constraint 

850 for constraint in self.constraints 

851 if ( 

852 isinstance(constraint, UniqueConstraint) 

853 and constraint.condition is None 

854 and not constraint.contains_expressions 

855 ) 

856 ] 

857 

858 @cached_property 

859 def _property_names(self): 

860 """Return a set of the names of the properties defined on the model.""" 

861 names = [] 

862 for name in dir(self.model): 

863 attr = inspect.getattr_static(self.model, name) 

864 if isinstance(attr, property): 

865 names.append(name) 

866 return frozenset(names) 

867 

868 @cached_property 

869 def _non_pk_concrete_field_names(self): 

870 """ 

871 Return a set of the non-pk concrete field names defined on the model. 

872 """ 

873 names = [] 

874 for field in self.concrete_fields: 

875 if not field.primary_key: 

876 names.append(field.name) 

877 if field.name != field.attname: 

878 names.append(field.attname) 

879 return frozenset(names) 

880 

881 @cached_property 

882 def db_returning_fields(self): 

883 """ 

884 Private API intended only to be used by Plain itself. 

885 Fields to be returned after a database insert. 

886 """ 

887 return [ 

888 field 

889 for field in self._get_fields( 

890 forward=True, reverse=False, include_parents=PROXY_PARENTS 

891 ) 

892 if getattr(field, "db_returning", False) 

893 ]