plain.models 0.48.0__tar.gz → 0.49.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {plain_models-0.48.0 → plain_models-0.49.0}/.gitignore +1 -0
  2. {plain_models-0.48.0 → plain_models-0.49.0}/PKG-INFO +1 -1
  3. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/CHANGELOG.md +16 -0
  4. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/__init__.py +0 -3
  5. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/aggregates.py +1 -1
  6. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/compiler.py +1 -1
  7. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/operations.py +1 -1
  8. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/base.py +34 -94
  9. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/constraints.py +2 -1
  10. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/deletion.py +3 -5
  11. plain_models-0.49.0/plain/models/exceptions.py +196 -0
  12. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/expressions.py +1 -1
  13. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/__init__.py +3 -41
  14. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/related.py +15 -22
  15. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/related_descriptors.py +1 -3
  16. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/related_managers.py +1 -1
  17. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/reverse_related.py +3 -5
  18. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/forms.py +1 -1
  19. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/lookups.py +1 -1
  20. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/state.py +1 -1
  21. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/options.py +4 -16
  22. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/query.py +12 -10
  23. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/query_utils.py +2 -2
  24. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/compiler.py +6 -6
  25. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/datastructures.py +1 -1
  26. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/query.py +4 -4
  27. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/subqueries.py +3 -3
  28. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/where.py +1 -1
  29. {plain_models-0.48.0 → plain_models-0.49.0}/pyproject.toml +1 -1
  30. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_exceptions.py +1 -1
  31. plain_models-0.48.0/plain/models/exceptions.py +0 -88
  32. {plain_models-0.48.0 → plain_models-0.49.0}/LICENSE +0 -0
  33. {plain_models-0.48.0 → plain_models-0.49.0}/README.md +0 -0
  34. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/AGENTS.md +0 -0
  35. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/README.md +0 -0
  36. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/__init__.py +0 -0
  37. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/__init__.py +0 -0
  38. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/base.py +0 -0
  39. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/client.py +0 -0
  40. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/creation.py +0 -0
  41. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/features.py +0 -0
  42. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/introspection.py +0 -0
  43. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/operations.py +0 -0
  44. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/schema.py +0 -0
  45. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/base/validation.py +0 -0
  46. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/ddl_references.py +0 -0
  47. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/__init__.py +0 -0
  48. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/base.py +0 -0
  49. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/client.py +0 -0
  50. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/creation.py +0 -0
  51. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/features.py +0 -0
  52. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/introspection.py +0 -0
  53. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/operations.py +0 -0
  54. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/schema.py +0 -0
  55. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/mysql/validation.py +0 -0
  56. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/__init__.py +0 -0
  57. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/base.py +0 -0
  58. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/client.py +0 -0
  59. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/creation.py +0 -0
  60. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/features.py +0 -0
  61. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/introspection.py +0 -0
  62. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/operations.py +0 -0
  63. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/postgresql/schema.py +0 -0
  64. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  65. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  66. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/base.py +0 -0
  67. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/client.py +0 -0
  68. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/creation.py +0 -0
  69. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/features.py +0 -0
  70. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  71. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/schema.py +0 -0
  72. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backends/utils.py +0 -0
  73. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backups/__init__.py +0 -0
  74. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backups/cli.py +0 -0
  75. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backups/clients.py +0 -0
  76. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/backups/core.py +0 -0
  77. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/cli.py +0 -0
  78. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/config.py +0 -0
  79. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/connections.py +0 -0
  80. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/constants.py +0 -0
  81. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/database_url.py +0 -0
  82. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/db.py +0 -0
  83. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/default_settings.py +0 -0
  84. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/entrypoints.py +0 -0
  85. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/enums.py +0 -0
  86. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/json.py +0 -0
  87. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/mixins.py +0 -0
  88. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/fields/related_lookups.py +0 -0
  89. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/__init__.py +0 -0
  90. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/comparison.py +0 -0
  91. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/datetime.py +0 -0
  92. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/math.py +0 -0
  93. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/mixins.py +0 -0
  94. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/text.py +0 -0
  95. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/functions/window.py +0 -0
  96. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/indexes.py +0 -0
  97. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/__init__.py +0 -0
  98. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/autodetector.py +0 -0
  99. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/exceptions.py +0 -0
  100. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/executor.py +0 -0
  101. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/graph.py +0 -0
  102. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/loader.py +0 -0
  103. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/migration.py +0 -0
  104. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/operations/__init__.py +0 -0
  105. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/operations/base.py +0 -0
  106. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/operations/fields.py +0 -0
  107. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/operations/models.py +0 -0
  108. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/operations/special.py +0 -0
  109. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/optimizer.py +0 -0
  110. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/questioner.py +0 -0
  111. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/recorder.py +0 -0
  112. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/serializer.py +0 -0
  113. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/utils.py +0 -0
  114. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/migrations/writer.py +0 -0
  115. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/otel.py +0 -0
  116. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/preflight.py +0 -0
  117. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/registry.py +0 -0
  118. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/__init__.py +0 -0
  119. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/sql/constants.py +0 -0
  120. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/test/__init__.py +0 -0
  121. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/test/pytest.py +0 -0
  122. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/test/utils.py +0 -0
  123. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/transaction.py +0 -0
  124. {plain_models-0.48.0 → plain_models-0.49.0}/plain/models/utils.py +0 -0
  125. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  126. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  127. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  128. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/examples/migrations/__init__.py +0 -0
  129. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/examples/models.py +0 -0
  130. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/settings.py +0 -0
  131. {plain_models-0.48.0 → plain_models-0.49.0}/tests/app/urls.py +0 -0
  132. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_database_url.py +0 -0
  133. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_delete_behaviors.py +0 -0
  134. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_manager_assignment.py +0 -0
  135. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_models.py +0 -0
  136. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_related_descriptors.py +0 -0
  137. {plain_models-0.48.0 → plain_models-0.49.0}/tests/test_related_manager_api.py +0 -0
@@ -15,3 +15,4 @@ plain*/tests/.plain
15
15
  .plain
16
16
 
17
17
  .vscode
18
+ /.claude
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.48.0
3
+ Version: 0.49.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,5 +1,21 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.49.0](https://github.com/dropseed/plain/releases/plain-models@0.49.0) (2025-09-29)
4
+
5
+ ### What's changed
6
+
7
+ - Model exceptions (`FieldDoesNotExist`, `FieldError`, `ObjectDoesNotExist`, `MultipleObjectsReturned`, `EmptyResultSet`, `FullResultSet`) have been moved from `plain.exceptions` to `plain.models.exceptions` ([1c02564](https://github.com/dropseed/plain/commit/1c02564561))
8
+ - The `get_FOO_display()` methods for fields with choices have been replaced with a single `get_field_display(field_name)` method ([e796e71](https://github.com/dropseed/plain/commit/e796e71e02))
9
+ - The `get_next_by_*` and `get_previous_by_*` methods for date fields have been removed ([3a5b8a8](https://github.com/dropseed/plain/commit/3a5b8a89d1))
10
+ - The `id` primary key field is now defined directly on the Model base class instead of being added dynamically via Options ([e164dc7](https://github.com/dropseed/plain/commit/e164dc7982))
11
+ - Model `DoesNotExist` and `MultipleObjectsReturned` exceptions now use descriptors for better performance ([8f54ea3](https://github.com/dropseed/plain/commit/8f54ea3a62))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - Update imports for model exceptions from `plain.exceptions` to `plain.models.exceptions` (e.g., `from plain.exceptions import ObjectDoesNotExist` becomes `from plain.models.exceptions import ObjectDoesNotExist`)
16
+ - Replace any usage of `instance.get_FOO_display()` with `instance.get_field_display("FOO")` where FOO is the field name
17
+ - Remove any usage of `get_next_by_*` and `get_previous_by_*` methods - use QuerySet ordering instead (e.g., `Model.query.filter(date__gt=obj.date).order_by("date").first()`)
18
+
3
19
  ## [0.48.0](https://github.com/dropseed/plain/releases/plain-models@0.48.0) (2025-09-26)
4
20
 
5
21
  ### What's changed
@@ -1,5 +1,3 @@
1
- from plain.exceptions import ObjectDoesNotExist
2
-
3
1
  from . import (
4
2
  preflight, # noqa
5
3
  )
@@ -78,7 +76,6 @@ from .fields.reverse_related import ( # isort:skip
78
76
 
79
77
  __all__ = aggregates_all + constraints_all + enums_all + fields_all + indexes_all
80
78
  __all__ += [
81
- "ObjectDoesNotExist",
82
79
  "CASCADE",
83
80
  "DO_NOTHING",
84
81
  "PROTECT",
@@ -2,7 +2,7 @@
2
2
  Classes to represent the definitions of aggregate functions.
3
3
  """
4
4
 
5
- from plain.exceptions import FieldError, FullResultSet
5
+ from plain.models.exceptions import FieldError, FullResultSet
6
6
  from plain.models.expressions import Case, Func, Star, Value, When
7
7
  from plain.models.fields import IntegerField
8
8
  from plain.models.functions.comparison import Coalesce
@@ -1,4 +1,4 @@
1
- from plain.exceptions import FieldError, FullResultSet
1
+ from plain.models.exceptions import FieldError, FullResultSet
2
2
  from plain.models.expressions import Col
3
3
  from plain.models.sql import compiler
4
4
 
@@ -4,10 +4,10 @@ import uuid
4
4
  from functools import cached_property, lru_cache
5
5
 
6
6
  from plain import models
7
- from plain.exceptions import FieldError
8
7
  from plain.models.backends.base.operations import BaseDatabaseOperations
9
8
  from plain.models.constants import OnConflict
10
9
  from plain.models.db import DatabaseError, NotSupportedError
10
+ from plain.models.exceptions import FieldError
11
11
  from plain.models.expressions import Col
12
12
  from plain.utils import timezone
13
13
  from plain.utils.dateparse import parse_date, parse_datetime, parse_time
@@ -4,13 +4,7 @@ import warnings
4
4
  from itertools import chain
5
5
 
6
6
  import plain.runtime
7
- from plain.exceptions import (
8
- NON_FIELD_ERRORS,
9
- FieldDoesNotExist,
10
- MultipleObjectsReturned,
11
- ObjectDoesNotExist,
12
- ValidationError,
13
- )
7
+ from plain.exceptions import NON_FIELD_ERRORS, ValidationError
14
8
  from plain.models import models_registry, transaction
15
9
  from plain.models.constants import LOOKUP_SEP
16
10
  from plain.models.constraints import CheckConstraint, UniqueConstraint
@@ -20,8 +14,13 @@ from plain.models.db import (
20
14
  db_connection,
21
15
  )
22
16
  from plain.models.deletion import Collector
17
+ from plain.models.exceptions import (
18
+ DoesNotExistDescriptor,
19
+ FieldDoesNotExist,
20
+ MultipleObjectsReturnedDescriptor,
21
+ )
23
22
  from plain.models.expressions import RawSQL, Value
24
- from plain.models.fields import NOT_PROVIDED
23
+ from plain.models.fields import NOT_PROVIDED, PrimaryKeyField
25
24
  from plain.models.fields.reverse_related import ForeignObjectRel
26
25
  from plain.models.options import Options
27
26
  from plain.models.query import F, Q, QuerySet
@@ -42,11 +41,6 @@ class Deferred:
42
41
  DEFERRED = Deferred()
43
42
 
44
43
 
45
- def _has_contribute_to_class(value):
46
- # Only call contribute_to_class() if it's bound.
47
- return not inspect.isclass(value) and hasattr(value, "contribute_to_class")
48
-
49
-
50
44
  class ModelBase(type):
51
45
  """Metaclass for all models."""
52
46
 
@@ -71,7 +65,6 @@ class ModelBase(type):
71
65
  new_class = super().__new__(cls, name, bases, attrs, **kwargs)
72
66
 
73
67
  new_class._setup_meta()
74
- new_class._add_exceptions()
75
68
 
76
69
  # Now go back over all the attrs on this class see if they have a contribute_to_class() method.
77
70
  # Attributes with contribute_to_class are fields and meta options.
@@ -79,7 +72,9 @@ class ModelBase(type):
79
72
  if attr_name.startswith("_"):
80
73
  continue
81
74
 
82
- if _has_contribute_to_class(attr_value):
75
+ if not inspect.isclass(attr_value) and hasattr(
76
+ attr_value, "contribute_to_class"
77
+ ):
83
78
  if attr_name not in attrs:
84
79
  # If the field came from an inherited class/mixin,
85
80
  # we need to make a copy of it to avoid altering the
@@ -87,25 +82,17 @@ class ModelBase(type):
87
82
  field = copy.deepcopy(attr_value)
88
83
  else:
89
84
  field = attr_value
90
- new_class.add_to_class(attr_name, field)
91
-
92
- new_class._meta.concrete_model = new_class
93
-
94
- # Copy indexes so that index names are unique when models extend another class.
95
- new_class._meta.indexes = [
96
- copy.deepcopy(idx) for idx in new_class._meta.indexes
97
- ]
85
+ field.contribute_to_class(new_class, attr_name)
98
86
 
99
- new_class._prepare()
87
+ # Set the name of _meta.indexes. This can't be done in
88
+ # Options.contribute_to_class() because fields haven't been added to
89
+ # the model at that point.
90
+ for index in new_class._meta.indexes:
91
+ if not index.name:
92
+ index.set_name_with_model(new_class)
100
93
 
101
94
  return new_class
102
95
 
103
- def add_to_class(cls, name, value):
104
- if _has_contribute_to_class(value):
105
- value.contribute_to_class(cls, name)
106
- else:
107
- setattr(cls, name, value)
108
-
109
96
  def _setup_meta(cls):
110
97
  name = cls.__name__
111
98
  module = cls.__module__
@@ -127,45 +114,7 @@ class ModelBase(type):
127
114
  else:
128
115
  package_label = package_config.package_label
129
116
 
130
- cls.add_to_class("_meta", Options(meta, package_label))
131
-
132
- def _add_exceptions(cls):
133
- cls.DoesNotExist = type(
134
- "DoesNotExist",
135
- (ObjectDoesNotExist,),
136
- {
137
- "__module__": cls.__module__,
138
- "__qualname__": f"{cls.__qualname__}.DoesNotExist",
139
- },
140
- )
141
-
142
- cls.MultipleObjectsReturned = type(
143
- "MultipleObjectsReturned",
144
- (MultipleObjectsReturned,),
145
- {
146
- "__module__": cls.__module__,
147
- "__qualname__": f"{cls.__qualname__}.MultipleObjectsReturned",
148
- },
149
- )
150
-
151
- def _prepare(cls):
152
- """Create some methods once self._meta has been populated."""
153
- opts = cls._meta
154
- opts._prepare(cls)
155
-
156
- # Give the class a docstring -- its definition.
157
- if cls.__doc__ is None:
158
- cls.__doc__ = "{}({})".format(
159
- cls.__name__,
160
- ", ".join(f.name for f in opts.fields),
161
- )
162
-
163
- # Set the name of _meta.indexes. This can't be done in
164
- # Options.contribute_to_class() because fields haven't been added to
165
- # the model at that point.
166
- for index in cls._meta.indexes:
167
- if not index.name:
168
- index.set_name_with_model(cls)
117
+ Options(meta, package_label).contribute_to_class(cls, "_meta")
169
118
 
170
119
  @property
171
120
  def query(cls) -> QuerySet:
@@ -194,8 +143,13 @@ class ModelState:
194
143
 
195
144
  class Model(metaclass=ModelBase):
196
145
  _meta: Options
197
- DoesNotExist: type[ObjectDoesNotExist]
198
- MultipleObjectsReturned: type[MultipleObjectsReturned]
146
+
147
+ # Use descriptors for exception classes instead of metaclass generation
148
+ DoesNotExist = DoesNotExistDescriptor()
149
+ MultipleObjectsReturned = MultipleObjectsReturnedDescriptor()
150
+
151
+ # Every model gets an automatic id field
152
+ id = PrimaryKeyField()
199
153
 
200
154
  def __init__(self, *args, **kwargs):
201
155
  # Alias some things as locals to avoid repeat global lookups
@@ -327,7 +281,7 @@ class Model(metaclass=ModelBase):
327
281
  def __eq__(self, other):
328
282
  if not isinstance(other, Model):
329
283
  return NotImplemented
330
- if self._meta.concrete_model != other._meta.concrete_model:
284
+ if self.__class__ != other.__class__:
331
285
  return False
332
286
  my_id = self.id
333
287
  if my_id is None:
@@ -711,34 +665,20 @@ class Model(metaclass=ModelBase):
711
665
  collector.collect([self])
712
666
  return collector.delete()
713
667
 
714
- def _get_FIELD_display(self, field):
668
+ def get_field_display(self, field):
669
+ """Get the display value for a field, especially useful for fields with choices."""
715
670
  value = getattr(self, field.attname)
671
+
672
+ # If field has no choices, just return the value as string
673
+ if not hasattr(field, "flatchoices") or not field.flatchoices:
674
+ return force_str(value, strings_only=True)
675
+
676
+ # For fields with choices, look up the display value
716
677
  choices_dict = dict(make_hashable(field.flatchoices))
717
- # force_str() to coerce lazy strings.
718
678
  return force_str(
719
679
  choices_dict.get(make_hashable(value), value), strings_only=True
720
680
  )
721
681
 
722
- def _get_next_or_previous_by_FIELD(self, field, is_next, **kwargs):
723
- if not self.id:
724
- raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
725
- op = "gt" if is_next else "lt"
726
- order = "" if is_next else "-"
727
- param = getattr(self, field.attname)
728
- q = Q.create([(field.name, param), (f"id__{op}", self.id)], connector=Q.AND)
729
- q = Q.create([q, (f"{field.name}__{op}", param)], connector=Q.OR)
730
- qs = (
731
- self.__class__.query.filter(**kwargs)
732
- .filter(q)
733
- .order_by(f"{order}{field.name}", f"{order}id")
734
- )
735
- try:
736
- return qs[0]
737
- except IndexError:
738
- raise self.DoesNotExist(
739
- f"{self.__class__._meta.object_name} matching query does not exist."
740
- )
741
-
742
682
  def _get_field_value_map(self, meta, exclude=None):
743
683
  if exclude is None:
744
684
  exclude = set()
@@ -1,7 +1,8 @@
1
1
  from enum import Enum
2
2
  from types import NoneType
3
3
 
4
- from plain.exceptions import FieldError, ValidationError
4
+ from plain.exceptions import ValidationError
5
+ from plain.models.exceptions import FieldError
5
6
  from plain.models.expressions import Exists, ExpressionList, F, OrderBy
6
7
  from plain.models.indexes import IndexExpression
7
8
  from plain.models.lookups import Exact
@@ -139,9 +139,7 @@ class Collector:
139
139
  def add_dependency(self, model, dependency, reverse_dependency=False):
140
140
  if reverse_dependency:
141
141
  model, dependency = dependency, model
142
- self.dependencies[model._meta.concrete_model].add(
143
- dependency._meta.concrete_model
144
- )
142
+ self.dependencies[model].add(dependency)
145
143
  self.data.setdefault(dependency, self.data.default_factory())
146
144
 
147
145
  def add_field_update(self, field, value, objs):
@@ -363,10 +361,10 @@ class Collector:
363
361
  for model in models:
364
362
  if model in sorted_models:
365
363
  continue
366
- dependencies = self.dependencies.get(model._meta.concrete_model)
364
+ dependencies = self.dependencies.get(model)
367
365
  if not (dependencies and dependencies.difference(concrete_models)):
368
366
  sorted_models.append(model)
369
- concrete_models.add(model._meta.concrete_model)
367
+ concrete_models.add(model)
370
368
  found = True
371
369
  if not found:
372
370
  return
@@ -0,0 +1,196 @@
1
+ from typing import Any
2
+
3
+ # MARK: Database Query Exceptions
4
+
5
+
6
+ class EmptyResultSet(Exception):
7
+ """A database query predicate is impossible."""
8
+
9
+ pass
10
+
11
+
12
+ class FullResultSet(Exception):
13
+ """A database query predicate is matches everything."""
14
+
15
+ pass
16
+
17
+
18
+ # MARK: Model and Field Errors
19
+
20
+
21
+ class FieldDoesNotExist(Exception):
22
+ """The requested model field does not exist"""
23
+
24
+ pass
25
+
26
+
27
+ class FieldError(Exception):
28
+ """Some kind of problem with a model field."""
29
+
30
+ pass
31
+
32
+
33
+ class ObjectDoesNotExist(Exception):
34
+ """The requested object does not exist"""
35
+
36
+ pass
37
+
38
+
39
+ class MultipleObjectsReturned(Exception):
40
+ """The query returned multiple objects when only one was expected."""
41
+
42
+ pass
43
+
44
+
45
+ # MARK: Model Exception Descriptors
46
+
47
+
48
+ class DoesNotExistDescriptor:
49
+ """Descriptor that creates a unique DoesNotExist exception class per model."""
50
+
51
+ def __init__(self) -> None:
52
+ self._exceptions_by_class: dict[type, type[ObjectDoesNotExist]] = {}
53
+
54
+ def __get__(self, instance: Any, owner: type | None) -> type[ObjectDoesNotExist]:
55
+ if owner is None:
56
+ return ObjectDoesNotExist # Return base class as fallback
57
+
58
+ # Create a unique exception class for this model if we haven't already
59
+ if owner not in self._exceptions_by_class:
60
+ exc_class = type(
61
+ "DoesNotExist",
62
+ (ObjectDoesNotExist,),
63
+ {
64
+ "__module__": owner.__module__,
65
+ "__qualname__": f"{owner.__qualname__}.DoesNotExist",
66
+ },
67
+ )
68
+ self._exceptions_by_class[owner] = exc_class
69
+
70
+ return self._exceptions_by_class[owner]
71
+
72
+ def __set__(self, instance: Any, value: Any) -> None:
73
+ raise AttributeError("Cannot set DoesNotExist")
74
+
75
+
76
+ class MultipleObjectsReturnedDescriptor:
77
+ """Descriptor that creates a unique MultipleObjectsReturned exception class per model."""
78
+
79
+ def __init__(self) -> None:
80
+ self._exceptions_by_class: dict[type, type[MultipleObjectsReturned]] = {}
81
+
82
+ def __get__(
83
+ self, instance: Any, owner: type | None
84
+ ) -> type[MultipleObjectsReturned]:
85
+ if owner is None:
86
+ return MultipleObjectsReturned # Return base class as fallback
87
+
88
+ # Create a unique exception class for this model if we haven't already
89
+ if owner not in self._exceptions_by_class:
90
+ exc_class = type(
91
+ "MultipleObjectsReturned",
92
+ (MultipleObjectsReturned,),
93
+ {
94
+ "__module__": owner.__module__,
95
+ "__qualname__": f"{owner.__qualname__}.MultipleObjectsReturned",
96
+ },
97
+ )
98
+ self._exceptions_by_class[owner] = exc_class
99
+
100
+ return self._exceptions_by_class[owner]
101
+
102
+ def __set__(self, instance: Any, value: Any) -> None:
103
+ raise AttributeError("Cannot set MultipleObjectsReturned")
104
+
105
+
106
+ # MARK: Database Exceptions (PEP-249)
107
+
108
+
109
+ class Error(Exception):
110
+ pass
111
+
112
+
113
+ class InterfaceError(Error):
114
+ pass
115
+
116
+
117
+ class DatabaseError(Error):
118
+ pass
119
+
120
+
121
+ class DataError(DatabaseError):
122
+ pass
123
+
124
+
125
+ class OperationalError(DatabaseError):
126
+ pass
127
+
128
+
129
+ class IntegrityError(DatabaseError):
130
+ pass
131
+
132
+
133
+ class InternalError(DatabaseError):
134
+ pass
135
+
136
+
137
+ class ProgrammingError(DatabaseError):
138
+ pass
139
+
140
+
141
+ class NotSupportedError(DatabaseError):
142
+ pass
143
+
144
+
145
+ class ConnectionDoesNotExist(Exception):
146
+ pass
147
+
148
+
149
+ class DatabaseErrorWrapper:
150
+ """
151
+ Context manager and decorator that reraises backend-specific database
152
+ exceptions using Plain's common wrappers.
153
+ """
154
+
155
+ def __init__(self, wrapper):
156
+ """
157
+ wrapper is a database wrapper.
158
+
159
+ It must have a Database attribute defining PEP-249 exceptions.
160
+ """
161
+ self.wrapper = wrapper
162
+
163
+ def __enter__(self):
164
+ pass
165
+
166
+ def __exit__(self, exc_type, exc_value, traceback):
167
+ if exc_type is None:
168
+ return
169
+ for plain_exc_type in (
170
+ DataError,
171
+ OperationalError,
172
+ IntegrityError,
173
+ InternalError,
174
+ ProgrammingError,
175
+ NotSupportedError,
176
+ DatabaseError,
177
+ InterfaceError,
178
+ Error,
179
+ ):
180
+ db_exc_type = getattr(self.wrapper.Database, plain_exc_type.__name__)
181
+ if issubclass(exc_type, db_exc_type):
182
+ plain_exc_value = plain_exc_type(*exc_value.args)
183
+ # Only set the 'errors_occurred' flag for errors that may make
184
+ # the connection unusable.
185
+ if plain_exc_type not in (DataError, IntegrityError):
186
+ self.wrapper.errors_occurred = True
187
+ raise plain_exc_value.with_traceback(traceback) from exc_value
188
+
189
+ def __call__(self, func):
190
+ # Note that we are intentionally not using @wraps here for performance
191
+ # reasons. Refs #21109.
192
+ def inner(*args, **kwargs):
193
+ with self:
194
+ return func(*args, **kwargs)
195
+
196
+ return inner
@@ -8,7 +8,6 @@ from functools import cached_property
8
8
  from types import NoneType
9
9
  from uuid import UUID
10
10
 
11
- from plain.exceptions import EmptyResultSet, FieldError, FullResultSet
12
11
  from plain.models import fields
13
12
  from plain.models.constants import LOOKUP_SEP
14
13
  from plain.models.db import (
@@ -16,6 +15,7 @@ from plain.models.db import (
16
15
  NotSupportedError,
17
16
  db_connection,
18
17
  )
18
+ from plain.models.exceptions import EmptyResultSet, FieldError, FullResultSet
19
19
  from plain.models.query_utils import Q
20
20
  from plain.utils.deconstruct import deconstructible
21
21
  from plain.utils.hashable import make_hashable
@@ -7,7 +7,7 @@ import operator
7
7
  import uuid
8
8
  import warnings
9
9
  from base64 import b64decode, b64encode
10
- from functools import cached_property, partialmethod, total_ordering
10
+ from functools import cached_property, total_ordering
11
11
 
12
12
  from plain import exceptions, validators
13
13
  from plain.models.constants import LOOKUP_SEP
@@ -759,29 +759,15 @@ class Field(RegisterLookupMixin):
759
759
  self.attname, self.column = self.get_attname_column()
760
760
  self.concrete = self.column is not None
761
761
 
762
- def contribute_to_class(self, cls, name, private_only=False):
762
+ def contribute_to_class(self, cls, name):
763
763
  """
764
764
  Register the field with the model class it belongs to.
765
-
766
- If private_only is True, create a separate instance of this field
767
- for every subclass of cls, even if cls is not an abstract model.
768
765
  """
769
766
  self.set_attributes_from_name(name)
770
767
  self.model = cls
771
- cls._meta.add_field(self, private=private_only)
768
+ cls._meta.add_field(self)
772
769
  if self.column:
773
770
  setattr(cls, self.attname, self.descriptor_class(self))
774
- if self.choices is not None:
775
- # Don't override a get_FOO_display() method defined explicitly on
776
- # this class, but don't check methods derived from inheritance, to
777
- # allow overriding inherited choices. For more complex inheritance
778
- # structures users should override contribute_to_class().
779
- if f"get_{self.name}_display" not in cls.__dict__:
780
- setattr(
781
- cls,
782
- f"get_{self.name}_display",
783
- partialmethod(cls._get_FIELD_display, field=self),
784
- )
785
771
 
786
772
  def get_attname(self):
787
773
  return self.name
@@ -1200,24 +1186,6 @@ class DateField(DateTimeCheckMixin, Field):
1200
1186
  else:
1201
1187
  return super().pre_save(model_instance, add)
1202
1188
 
1203
- def contribute_to_class(self, cls, name, **kwargs):
1204
- super().contribute_to_class(cls, name, **kwargs)
1205
- if not self.allow_null:
1206
- setattr(
1207
- cls,
1208
- f"get_next_by_{self.name}",
1209
- partialmethod(
1210
- cls._get_next_or_previous_by_FIELD, field=self, is_next=True
1211
- ),
1212
- )
1213
- setattr(
1214
- cls,
1215
- f"get_previous_by_{self.name}",
1216
- partialmethod(
1217
- cls._get_next_or_previous_by_FIELD, field=self, is_next=False
1218
- ),
1219
- )
1220
-
1221
1189
  def get_prep_value(self, value):
1222
1190
  value = super().get_prep_value(value)
1223
1191
  return self.to_python(value)
@@ -1319,9 +1287,6 @@ class DateTimeField(DateField):
1319
1287
  else:
1320
1288
  return super().pre_save(model_instance, add)
1321
1289
 
1322
- # contribute_to_class is inherited from DateField, it registers
1323
- # get_next_by_FOO and get_prev_by_FOO
1324
-
1325
1290
  def get_prep_value(self, value):
1326
1291
  value = super().get_prep_value(value)
1327
1292
  value = self.to_python(value)
@@ -2145,9 +2110,6 @@ class PrimaryKeyField(BigIntegerField):
2145
2110
  value = connection.ops.validate_autopk_value(value)
2146
2111
  return value
2147
2112
 
2148
- def contribute_to_class(self, cls, name, **kwargs):
2149
- super().contribute_to_class(cls, name, **kwargs)
2150
-
2151
2113
  def get_internal_type(self):
2152
2114
  return "PrimaryKeyField"
2153
2115