plain.models
Model your data and store it in a database.
# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField
class User(models.Model):
email = models.EmailField()
password = PasswordField()
is_admin = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.email
Create, update, and delete instances of your models:
from .models import User
# Create a new user
user = User.objects.create(
email="[email protected]",
password="password",
)
# Update a user
user.email = "[email protected]"
user.save()
# Delete a user
user.delete()
# Query for users
admin_users = User.objects.filter(is_admin=True)
Installation
# app/settings.py
INSTALLED_PACKAGES = [
...
"plain.models",
]
To connect to a database, you can provide a DATABASE_URL
environment variable.
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
Or you can manually define the DATABASES
setting.
# app/settings.py
DATABASES = {
"default": {
"ENGINE": "plain.models.backends.postgresql",
"NAME": "dbname",
"USER": "user",
"PASSWORD": "password",
"HOST": "localhost",
"PORT": "5432",
}
}
Multiple backends are supported, including Postgres, MySQL, and SQLite.
Querying
Migrations
Fields
Validation
Indexes and constraints
Managers
Forms
1import bisect
2import copy
3import inspect
4from collections import defaultdict
5
6from plain.exceptions import FieldDoesNotExist
7from plain.models import models_registry
8from plain.models.constraints import UniqueConstraint
9from plain.models.db import connections
10from plain.models.fields import BigAutoField
11from plain.models.manager import Manager
12from plain.utils.datastructures import ImmutableList
13from plain.utils.functional import cached_property
14
15PROXY_PARENTS = object()
16
17EMPTY_RELATION_TREE = ()
18
19IMMUTABLE_WARNING = (
20 "The return type of '%s' should never be mutated. If you want to manipulate this "
21 "list for your own use, make a copy first."
22)
23
24DEFAULT_NAMES = (
25 "db_table",
26 "db_table_comment",
27 "ordering",
28 "get_latest_by",
29 "package_label",
30 "models_registry",
31 "default_related_name",
32 "required_db_features",
33 "required_db_vendor",
34 "base_manager_name",
35 "default_manager_name",
36 "indexes",
37 "constraints",
38)
39
40
41def make_immutable_fields_list(name, data):
42 return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
43
44
45class Options:
46 FORWARD_PROPERTIES = {
47 "fields",
48 "many_to_many",
49 "concrete_fields",
50 "local_concrete_fields",
51 "_non_pk_concrete_field_names",
52 "_forward_fields_map",
53 "managers",
54 "managers_map",
55 "base_manager",
56 "default_manager",
57 }
58 REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
59
60 default_models_registry = models_registry
61
62 def __init__(self, meta, package_label=None):
63 self._get_fields_cache = {}
64 self.local_fields = []
65 self.local_many_to_many = []
66 self.local_managers = []
67 self.base_manager_name = None
68 self.default_manager_name = None
69 self.model_name = None
70 self.db_table = ""
71 self.db_table_comment = ""
72 self.ordering = []
73 self.indexes = []
74 self.constraints = []
75 self.object_name = None
76 self.package_label = package_label
77 self.get_latest_by = None
78 self.required_db_features = []
79 self.required_db_vendor = None
80 self.meta = meta
81 self.pk = None
82 self.auto_field = None
83
84 # For any non-abstract class, the concrete class is the model
85 # in the end of the proxy_for_model chain. In particular, for
86 # concrete models, the concrete_model is always the class itself.
87 self.concrete_model = None
88
89 # List of all lookups defined in ForeignKey 'limit_choices_to' options
90 # from *other* models. Needed for some admin checks. Internal use only.
91 self.related_fkey_lookups = []
92
93 # A custom app registry to use, if you're making a separate model set.
94 self.models_registry = self.default_models_registry
95
96 self.default_related_name = None
97
98 @property
99 def label(self):
100 return f"{self.package_label}.{self.object_name}"
101
102 @property
103 def label_lower(self):
104 return f"{self.package_label}.{self.model_name}"
105
106 def contribute_to_class(self, cls, name):
107 from plain.models.backends.utils import truncate_name
108 from plain.models.db import connection
109
110 cls._meta = self
111 self.model = cls
112 # First, construct the default values for these options.
113 self.object_name = cls.__name__
114 self.model_name = self.object_name.lower()
115
116 # Store the original user-defined values for each option,
117 # for use when serializing the model definition
118 self.original_attrs = {}
119
120 # Next, apply any overridden values from 'class Meta'.
121 if self.meta:
122 meta_attrs = self.meta.__dict__.copy()
123 for name in self.meta.__dict__:
124 # Ignore any private attributes that Plain doesn't care about.
125 # NOTE: We can't modify a dictionary's contents while looping
126 # over it, so we loop over the *original* dictionary instead.
127 if name.startswith("_"):
128 del meta_attrs[name]
129 for attr_name in DEFAULT_NAMES:
130 if attr_name in meta_attrs:
131 setattr(self, attr_name, meta_attrs.pop(attr_name))
132 self.original_attrs[attr_name] = getattr(self, attr_name)
133 elif hasattr(self.meta, attr_name):
134 setattr(self, attr_name, getattr(self.meta, attr_name))
135 self.original_attrs[attr_name] = getattr(self, attr_name)
136
137 # Package label/class name interpolation for names of constraints and
138 # indexes.
139 for attr_name in {"constraints", "indexes"}:
140 objs = getattr(self, attr_name, [])
141 setattr(self, attr_name, self._format_names_with_class(cls, objs))
142
143 # Any leftover attributes must be invalid.
144 if meta_attrs != {}:
145 raise TypeError(
146 "'class Meta' got invalid attribute(s): {}".format(
147 ",".join(meta_attrs)
148 )
149 )
150
151 del self.meta
152
153 # If the db_table wasn't provided, use the package_label + model_name.
154 if not self.db_table:
155 self.db_table = f"{self.package_label}_{self.model_name}"
156 self.db_table = truncate_name(
157 self.db_table, connection.ops.max_name_length()
158 )
159
160 def _format_names_with_class(self, cls, objs):
161 """Package label/class name interpolation for object names."""
162 new_objs = []
163 for obj in objs:
164 obj = obj.clone()
165 obj.name = obj.name % {
166 "package_label": cls._meta.package_label.lower(),
167 "class": cls.__name__.lower(),
168 }
169 new_objs.append(obj)
170 return new_objs
171
172 def _prepare(self, model):
173 if self.pk is None:
174 auto = BigAutoField(primary_key=True, auto_created=True)
175 model.add_to_class("id", auto)
176
177 def add_manager(self, manager):
178 self.local_managers.append(manager)
179 self._expire_cache()
180
181 def add_field(self, field, private=False):
182 # Insert the given field in the order in which it was created, using
183 # the "creation_counter" attribute of the field.
184 # Move many-to-many related fields from self.fields into
185 # self.many_to_many.
186 if field.is_relation and field.many_to_many:
187 bisect.insort(self.local_many_to_many, field)
188 else:
189 bisect.insort(self.local_fields, field)
190 self.setup_pk(field)
191
192 # If the field being added is a relation to another known field,
193 # expire the cache on this field and the forward cache on the field
194 # being referenced, because there will be new relationships in the
195 # cache. Otherwise, expire the cache of references *to* this field.
196 # The mechanism for getting at the related model is slightly odd -
197 # ideally, we'd just ask for field.related_model. However, related_model
198 # is a cached property, and all the models haven't been loaded yet, so
199 # we need to make sure we don't cache a string reference.
200 if (
201 field.is_relation
202 and hasattr(field.remote_field, "model")
203 and field.remote_field.model
204 ):
205 try:
206 field.remote_field.model._meta._expire_cache(forward=False)
207 except AttributeError:
208 pass
209 self._expire_cache()
210 else:
211 self._expire_cache(reverse=False)
212
213 def setup_pk(self, field):
214 if not self.pk and field.primary_key:
215 self.pk = field
216
217 def __repr__(self):
218 return f"<Options for {self.object_name}>"
219
220 def __str__(self):
221 return self.label_lower
222
223 def can_migrate(self, connection):
224 """
225 Return True if the model can/should be migrated on the `connection`.
226 `connection` can be either a real connection or a connection alias.
227 """
228 if isinstance(connection, str):
229 connection = connections[connection]
230 if self.required_db_vendor:
231 return self.required_db_vendor == connection.vendor
232 if self.required_db_features:
233 return all(
234 getattr(connection.features, feat, False)
235 for feat in self.required_db_features
236 )
237 return True
238
239 @cached_property
240 def managers(self):
241 managers = []
242 seen_managers = set()
243 bases = (b for b in self.model.mro() if hasattr(b, "_meta"))
244 for depth, base in enumerate(bases):
245 for manager in base._meta.local_managers:
246 if manager.name in seen_managers:
247 continue
248
249 manager = copy.copy(manager)
250 manager.model = self.model
251 seen_managers.add(manager.name)
252 managers.append((depth, manager.creation_counter, manager))
253
254 return make_immutable_fields_list(
255 "managers",
256 (m[2] for m in sorted(managers)),
257 )
258
259 @cached_property
260 def managers_map(self):
261 return {manager.name: manager for manager in self.managers}
262
263 @cached_property
264 def base_manager(self):
265 base_manager_name = self.base_manager_name
266 if not base_manager_name:
267 # Get the first parent's base_manager_name if there's one.
268 for parent in self.model.mro()[1:]:
269 if hasattr(parent, "_meta"):
270 if parent._base_manager.name != "_base_manager":
271 base_manager_name = parent._base_manager.name
272 break
273
274 if base_manager_name:
275 try:
276 return self.managers_map[base_manager_name]
277 except KeyError:
278 raise ValueError(
279 f"{self.object_name} has no manager named {base_manager_name!r}"
280 )
281
282 manager = Manager()
283 manager.name = "_base_manager"
284 manager.model = self.model
285 manager.auto_created = True
286 return manager
287
288 @cached_property
289 def default_manager(self):
290 default_manager_name = self.default_manager_name
291 if not default_manager_name and not self.local_managers:
292 # Get the first parent's default_manager_name if there's one.
293 for parent in self.model.mro()[1:]:
294 if hasattr(parent, "_meta"):
295 default_manager_name = parent._meta.default_manager_name
296 break
297
298 if default_manager_name:
299 try:
300 return self.managers_map[default_manager_name]
301 except KeyError:
302 raise ValueError(
303 f"{self.object_name} has no manager named {default_manager_name!r}"
304 )
305
306 if self.managers:
307 return self.managers[0]
308
309 @cached_property
310 def fields(self):
311 """
312 Return a list of all forward fields on the model and its parents,
313 excluding ManyToManyFields.
314
315 Private API intended only to be used by Plain itself; get_fields()
316 combined with filtering of field properties is the public API for
317 obtaining this field list.
318 """
319
320 # For legacy reasons, the fields property should only contain forward
321 # fields that are not private or with a m2m cardinality. Therefore we
322 # pass these three filters as filters to the generator.
323 # The third lambda is a longwinded way of checking f.related_model - we don't
324 # use that property directly because related_model is a cached property,
325 # and all the models may not have been loaded yet; we don't want to cache
326 # the string reference to the related_model.
327 def is_not_an_m2m_field(f):
328 return not (f.is_relation and f.many_to_many)
329
330 def is_not_a_generic_relation(f):
331 return not (f.is_relation and f.one_to_many)
332
333 def is_not_a_generic_foreign_key(f):
334 return not (
335 f.is_relation
336 and f.many_to_one
337 and not (hasattr(f.remote_field, "model") and f.remote_field.model)
338 )
339
340 return make_immutable_fields_list(
341 "fields",
342 (
343 f
344 for f in self._get_fields(reverse=False)
345 if is_not_an_m2m_field(f)
346 and is_not_a_generic_relation(f)
347 and is_not_a_generic_foreign_key(f)
348 ),
349 )
350
351 @cached_property
352 def concrete_fields(self):
353 """
354 Return a list of all concrete fields on the model and its parents.
355
356 Private API intended only to be used by Plain itself; get_fields()
357 combined with filtering of field properties is the public API for
358 obtaining this field list.
359 """
360 return make_immutable_fields_list(
361 "concrete_fields", (f for f in self.fields if f.concrete)
362 )
363
364 @cached_property
365 def local_concrete_fields(self):
366 """
367 Return a list of all concrete fields on the model.
368
369 Private API intended only to be used by Plain itself; get_fields()
370 combined with filtering of field properties is the public API for
371 obtaining this field list.
372 """
373 return make_immutable_fields_list(
374 "local_concrete_fields", (f for f in self.local_fields if f.concrete)
375 )
376
377 @cached_property
378 def many_to_many(self):
379 """
380 Return a list of all many to many fields on the model and its parents.
381
382 Private API intended only to be used by Plain itself; get_fields()
383 combined with filtering of field properties is the public API for
384 obtaining this list.
385 """
386 return make_immutable_fields_list(
387 "many_to_many",
388 (
389 f
390 for f in self._get_fields(reverse=False)
391 if f.is_relation and f.many_to_many
392 ),
393 )
394
395 @cached_property
396 def related_objects(self):
397 """
398 Return all related objects pointing to the current model. The related
399 objects can come from a one-to-one, one-to-many, or many-to-many field
400 relation type.
401
402 Private API intended only to be used by Plain itself; get_fields()
403 combined with filtering of field properties is the public API for
404 obtaining this field list.
405 """
406 all_related_fields = self._get_fields(
407 forward=False, reverse=True, include_hidden=True
408 )
409 return make_immutable_fields_list(
410 "related_objects",
411 (
412 obj
413 for obj in all_related_fields
414 if not obj.hidden or obj.field.many_to_many
415 ),
416 )
417
418 @cached_property
419 def _forward_fields_map(self):
420 res = {}
421 fields = self._get_fields(reverse=False)
422 for field in fields:
423 res[field.name] = field
424 # Due to the way Plain's internals work, get_field() should also
425 # be able to fetch a field by attname. In the case of a concrete
426 # field with relation, includes the *_id name too
427 try:
428 res[field.attname] = field
429 except AttributeError:
430 pass
431 return res
432
433 @cached_property
434 def fields_map(self):
435 res = {}
436 fields = self._get_fields(forward=False, include_hidden=True)
437 for field in fields:
438 res[field.name] = field
439 # Due to the way Plain's internals work, get_field() should also
440 # be able to fetch a field by attname. In the case of a concrete
441 # field with relation, includes the *_id name too
442 try:
443 res[field.attname] = field
444 except AttributeError:
445 pass
446 return res
447
448 def get_field(self, field_name):
449 """
450 Return a field instance given the name of a forward or reverse field.
451 """
452 try:
453 # In order to avoid premature loading of the relation tree
454 # (expensive) we prefer checking if the field is a forward field.
455 return self._forward_fields_map[field_name]
456 except KeyError:
457 # If the app registry is not ready, reverse fields are
458 # unavailable, therefore we throw a FieldDoesNotExist exception.
459 if not self.models_registry.ready:
460 raise FieldDoesNotExist(
461 f"{self.object_name} has no field named '{field_name}'. The app cache isn't ready yet, "
462 "so if this is an auto-created related field, it won't "
463 "be available yet."
464 )
465
466 try:
467 # Retrieve field instance by name from cached or just-computed
468 # field map.
469 return self.fields_map[field_name]
470 except KeyError:
471 raise FieldDoesNotExist(
472 f"{self.object_name} has no field named '{field_name}'"
473 )
474
475 def _populate_directed_relation_graph(self):
476 """
477 This method is used by each model to find its reverse objects. As this
478 method is very expensive and is accessed frequently (it looks up every
479 field in a model, in every app), it is computed on first access and then
480 is set as a property on every model.
481 """
482 related_objects_graph = defaultdict(list)
483
484 all_models = self.models_registry.get_models()
485 for model in all_models:
486 opts = model._meta
487
488 fields_with_relations = (
489 f
490 for f in opts._get_fields(reverse=False)
491 if f.is_relation and f.related_model is not None
492 )
493 for f in fields_with_relations:
494 if not isinstance(f.remote_field.model, str):
495 remote_label = f.remote_field.model._meta.concrete_model._meta.label
496 related_objects_graph[remote_label].append(f)
497
498 for model in all_models:
499 # Set the relation_tree using the internal __dict__. In this way
500 # we avoid calling the cached property. In attribute lookup,
501 # __dict__ takes precedence over a data descriptor (such as
502 # @cached_property). This means that the _meta._relation_tree is
503 # only called if related_objects is not in __dict__.
504 related_objects = related_objects_graph[
505 model._meta.concrete_model._meta.label
506 ]
507 model._meta.__dict__["_relation_tree"] = related_objects
508 # It seems it is possible that self is not in all_models, so guard
509 # against that with default for get().
510 return self.__dict__.get("_relation_tree", EMPTY_RELATION_TREE)
511
512 @cached_property
513 def _relation_tree(self):
514 return self._populate_directed_relation_graph()
515
516 def _expire_cache(self, forward=True, reverse=True):
517 # This method is usually called by packages.cache_clear(), when the
518 # registry is finalized, or when a new field is added.
519 if forward:
520 for cache_key in self.FORWARD_PROPERTIES:
521 if cache_key in self.__dict__:
522 delattr(self, cache_key)
523 if reverse:
524 for cache_key in self.REVERSE_PROPERTIES:
525 if cache_key in self.__dict__:
526 delattr(self, cache_key)
527 self._get_fields_cache = {}
528
529 def get_fields(self, include_hidden=False):
530 """
531 Return a list of fields associated to the model. By default, include
532 forward and reverse fields, fields derived from inheritance, but not
533 hidden fields. The returned fields can be changed using the parameters:
534
535 - include_hidden: include fields that have a related_name that
536 starts with a "+"
537 """
538 return self._get_fields(include_hidden=include_hidden)
539
540 def _get_fields(
541 self,
542 forward=True,
543 reverse=True,
544 include_hidden=False,
545 seen_models=None,
546 ):
547 """
548 Internal helper function to return fields of the model.
549 * If forward=True, then fields defined on this model are returned.
550 * If reverse=True, then relations pointing to this model are returned.
551 * If include_hidden=True, then fields with is_hidden=True are returned.
552 """
553
554 # This helper function is used to allow recursion in ``get_fields()``
555 # implementation and to provide a fast way for Plain's internals to
556 # access specific subsets of fields.
557
558 # We must keep track of which models we have already seen. Otherwise we
559 # could include the same field multiple times from different models.
560 topmost_call = seen_models is None
561 if topmost_call:
562 seen_models = set()
563 seen_models.add(self.model)
564
565 # Creates a cache key composed of all arguments
566 cache_key = (forward, reverse, include_hidden, topmost_call)
567
568 try:
569 # In order to avoid list manipulation. Always return a shallow copy
570 # of the results.
571 return self._get_fields_cache[cache_key]
572 except KeyError:
573 pass
574
575 fields = []
576
577 if reverse:
578 # Tree is computed once and cached until the app cache is expired.
579 # It is composed of a list of fields pointing to the current model
580 # from other models.
581 all_fields = self._relation_tree
582 for field in all_fields:
583 # If hidden fields should be included or the relation is not
584 # intentionally hidden, add to the fields dict.
585 if include_hidden or not field.remote_field.hidden:
586 fields.append(field.remote_field)
587
588 if forward:
589 fields += self.local_fields
590 fields += self.local_many_to_many
591
592 # In order to avoid list manipulation. Always
593 # return a shallow copy of the results
594 fields = make_immutable_fields_list("get_fields()", fields)
595
596 # Store result into cache for later access
597 self._get_fields_cache[cache_key] = fields
598 return fields
599
600 @cached_property
601 def total_unique_constraints(self):
602 """
603 Return a list of total unique constraints. Useful for determining set
604 of fields guaranteed to be unique for all rows.
605 """
606 return [
607 constraint
608 for constraint in self.constraints
609 if (
610 isinstance(constraint, UniqueConstraint)
611 and constraint.condition is None
612 and not constraint.contains_expressions
613 )
614 ]
615
616 @cached_property
617 def _property_names(self):
618 """Return a set of the names of the properties defined on the model."""
619 names = []
620 for name in dir(self.model):
621 attr = inspect.getattr_static(self.model, name)
622 if isinstance(attr, property):
623 names.append(name)
624 return frozenset(names)
625
626 @cached_property
627 def _non_pk_concrete_field_names(self):
628 """
629 Return a set of the non-pk concrete field names defined on the model.
630 """
631 names = []
632 for field in self.concrete_fields:
633 if not field.primary_key:
634 names.append(field.name)
635 if field.name != field.attname:
636 names.append(field.attname)
637 return frozenset(names)
638
639 @cached_property
640 def db_returning_fields(self):
641 """
642 Private API intended only to be used by Plain itself.
643 Fields to be returned after a database insert.
644 """
645 return [
646 field
647 for field in self._get_fields(forward=True, reverse=False)
648 if getattr(field, "db_returning", False)
649 ]