plain.models 0.47.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.47.0 → plain_models-0.49.0}/.gitignore +1 -0
  2. {plain_models-0.47.0 → plain_models-0.49.0}/PKG-INFO +1 -1
  3. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/CHANGELOG.md +27 -0
  4. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/__init__.py +0 -3
  5. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/aggregates.py +1 -1
  6. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/creation.py +1 -0
  7. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/compiler.py +1 -1
  8. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/operations.py +1 -1
  9. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/base.py +34 -94
  10. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/cli.py +66 -0
  11. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/constraints.py +2 -1
  12. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/deletion.py +3 -5
  13. plain_models-0.49.0/plain/models/exceptions.py +196 -0
  14. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/expressions.py +1 -1
  15. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/__init__.py +3 -41
  16. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related.py +15 -22
  17. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_descriptors.py +1 -3
  18. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_managers.py +1 -1
  19. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/reverse_related.py +3 -5
  20. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/forms.py +1 -1
  21. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/lookups.py +1 -1
  22. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/executor.py +29 -27
  23. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/state.py +1 -1
  24. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/options.py +4 -16
  25. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/query.py +12 -10
  26. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/query_utils.py +2 -2
  27. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/compiler.py +6 -6
  28. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/datastructures.py +1 -1
  29. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/query.py +4 -4
  30. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/subqueries.py +3 -3
  31. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/where.py +1 -1
  32. {plain_models-0.47.0 → plain_models-0.49.0}/pyproject.toml +1 -1
  33. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_exceptions.py +1 -1
  34. plain_models-0.47.0/plain/models/exceptions.py +0 -88
  35. {plain_models-0.47.0 → plain_models-0.49.0}/LICENSE +0 -0
  36. {plain_models-0.47.0 → plain_models-0.49.0}/README.md +0 -0
  37. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/AGENTS.md +0 -0
  38. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/README.md +0 -0
  39. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/__init__.py +0 -0
  40. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/__init__.py +0 -0
  41. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/base.py +0 -0
  42. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/client.py +0 -0
  43. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/features.py +0 -0
  44. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/introspection.py +0 -0
  45. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/operations.py +0 -0
  46. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/schema.py +0 -0
  47. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/validation.py +0 -0
  48. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/ddl_references.py +0 -0
  49. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/__init__.py +0 -0
  50. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/base.py +0 -0
  51. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/client.py +0 -0
  52. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/creation.py +0 -0
  53. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/features.py +0 -0
  54. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/introspection.py +0 -0
  55. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/operations.py +0 -0
  56. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/schema.py +0 -0
  57. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/validation.py +0 -0
  58. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/__init__.py +0 -0
  59. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/base.py +0 -0
  60. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/client.py +0 -0
  61. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/creation.py +0 -0
  62. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/features.py +0 -0
  63. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/introspection.py +0 -0
  64. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/operations.py +0 -0
  65. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/schema.py +0 -0
  66. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  67. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  68. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/base.py +0 -0
  69. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/client.py +0 -0
  70. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/creation.py +0 -0
  71. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/features.py +0 -0
  72. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  73. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/schema.py +0 -0
  74. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/utils.py +0 -0
  75. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/__init__.py +0 -0
  76. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/cli.py +0 -0
  77. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/clients.py +0 -0
  78. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/core.py +0 -0
  79. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/config.py +0 -0
  80. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/connections.py +0 -0
  81. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/constants.py +0 -0
  82. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/database_url.py +0 -0
  83. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/db.py +0 -0
  84. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/default_settings.py +0 -0
  85. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/entrypoints.py +0 -0
  86. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/enums.py +0 -0
  87. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/json.py +0 -0
  88. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/mixins.py +0 -0
  89. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_lookups.py +0 -0
  90. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/__init__.py +0 -0
  91. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/comparison.py +0 -0
  92. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/datetime.py +0 -0
  93. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/math.py +0 -0
  94. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/mixins.py +0 -0
  95. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/text.py +0 -0
  96. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/window.py +0 -0
  97. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/indexes.py +0 -0
  98. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/__init__.py +0 -0
  99. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/autodetector.py +0 -0
  100. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/exceptions.py +0 -0
  101. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/graph.py +0 -0
  102. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/loader.py +0 -0
  103. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/migration.py +0 -0
  104. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/__init__.py +0 -0
  105. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/base.py +0 -0
  106. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/fields.py +0 -0
  107. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/models.py +0 -0
  108. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/special.py +0 -0
  109. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/optimizer.py +0 -0
  110. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/questioner.py +0 -0
  111. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/recorder.py +0 -0
  112. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/serializer.py +0 -0
  113. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/utils.py +0 -0
  114. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/writer.py +0 -0
  115. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/otel.py +0 -0
  116. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/preflight.py +0 -0
  117. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/registry.py +0 -0
  118. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/__init__.py +0 -0
  119. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/constants.py +0 -0
  120. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/__init__.py +0 -0
  121. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/pytest.py +0 -0
  122. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/utils.py +0 -0
  123. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/transaction.py +0 -0
  124. {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/utils.py +0 -0
  125. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  126. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  127. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  128. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/__init__.py +0 -0
  129. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/models.py +0 -0
  130. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/settings.py +0 -0
  131. {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/urls.py +0 -0
  132. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_database_url.py +0 -0
  133. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_delete_behaviors.py +0 -0
  134. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_manager_assignment.py +0 -0
  135. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_models.py +0 -0
  136. {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_related_descriptors.py +0 -0
  137. {plain_models-0.47.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.47.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,32 @@
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
+
19
+ ## [0.48.0](https://github.com/dropseed/plain/releases/plain-models@0.48.0) (2025-09-26)
20
+
21
+ ### What's changed
22
+
23
+ - Migrations now run in a single transaction by default for databases that support transactional DDL, providing all-or-nothing migration batches for better safety and consistency ([6d0c105](https://github.com/dropseed/plain/commit/6d0c105fa9))
24
+ - Added `--atomic-batch/--no-atomic-batch` options to `plain migrate` to explicitly control whether migrations are run in a single transaction ([6d0c105](https://github.com/dropseed/plain/commit/6d0c105fa9))
25
+
26
+ ### Upgrade instructions
27
+
28
+ - No changes required
29
+
3
30
  ## [0.47.0](https://github.com/dropseed/plain/releases/plain-models@0.47.0) (2025-09-25)
4
31
 
5
32
  ### 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
@@ -59,6 +59,7 @@ class BaseDatabaseCreation:
59
59
  prune=False,
60
60
  no_input=True,
61
61
  verbosity=max(verbosity - 1, 0),
62
+ atomic_batch=False, # No need for atomic batch when creating test database
62
63
  )
63
64
 
64
65
  # Ensure a connection for the side effect of initializing the test database.
@@ -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()
@@ -362,6 +362,11 @@ def makemigrations(package_labels, dry_run, empty, no_input, name, check, verbos
362
362
  default=1,
363
363
  help="Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output",
364
364
  )
365
+ @click.option(
366
+ "--atomic-batch/--no-atomic-batch",
367
+ default=None,
368
+ help="Run migrations in a single transaction (auto-detected by default)",
369
+ )
365
370
  def migrate(
366
371
  package_label,
367
372
  migration_name,
@@ -372,6 +377,7 @@ def migrate(
372
377
  prune,
373
378
  no_input,
374
379
  verbosity,
380
+ atomic_batch,
375
381
  ):
376
382
  """Updates database schema. Manages both packages with migrations and those without."""
377
383
 
@@ -575,6 +581,65 @@ def migrate(
575
581
  # pprint(sql)
576
582
 
577
583
  if migration_plan:
584
+ # Determine whether to use atomic batch
585
+ use_atomic_batch = False
586
+ if len(migration_plan) > 1:
587
+ # Check database capabilities
588
+ can_rollback_ddl = db_connection.features.can_rollback_ddl
589
+
590
+ # Check if all migrations support atomic
591
+ non_atomic_migrations = [m for m in migration_plan if not m.atomic]
592
+
593
+ if atomic_batch is True:
594
+ # User explicitly requested atomic batch
595
+ if not can_rollback_ddl:
596
+ raise click.UsageError(
597
+ f"--atomic-batch not supported on {db_connection.vendor}. "
598
+ "Remove the flag or use a database that supports transactional DDL."
599
+ )
600
+ if non_atomic_migrations:
601
+ names = ", ".join(
602
+ f"{m.package_label}.{m.name}" for m in non_atomic_migrations[:3]
603
+ )
604
+ if len(non_atomic_migrations) > 3:
605
+ names += f", and {len(non_atomic_migrations) - 3} more"
606
+ raise click.UsageError(
607
+ f"--atomic-batch requested but these migrations have atomic=False: {names}"
608
+ )
609
+ use_atomic_batch = True
610
+ if verbosity >= 1:
611
+ click.echo(
612
+ f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
613
+ )
614
+ elif atomic_batch is False:
615
+ # User explicitly disabled atomic batch
616
+ use_atomic_batch = False
617
+ if verbosity >= 1:
618
+ click.echo(f" Running {len(migration_plan)} migrations separately")
619
+ else:
620
+ # Auto-detect (atomic_batch is None)
621
+ if can_rollback_ddl and not non_atomic_migrations:
622
+ use_atomic_batch = True
623
+ if verbosity >= 1:
624
+ click.echo(
625
+ f" Running {len(migration_plan)} migrations in atomic batch (all-or-nothing)"
626
+ )
627
+ else:
628
+ use_atomic_batch = False
629
+ if verbosity >= 1:
630
+ if not can_rollback_ddl:
631
+ click.echo(
632
+ f" Running {len(migration_plan)} migrations separately ({db_connection.vendor} doesn't support batch transactions)"
633
+ )
634
+ elif non_atomic_migrations:
635
+ click.echo(
636
+ f" Running {len(migration_plan)} migrations separately (some migrations have atomic=False)"
637
+ )
638
+ else:
639
+ click.echo(
640
+ f" Running {len(migration_plan)} migrations separately"
641
+ )
642
+
578
643
  if backup or (backup is None and settings.DEBUG):
579
644
  backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
580
645
  click.secho(
@@ -599,6 +664,7 @@ def migrate(
599
664
  plan=migration_plan,
600
665
  state=pre_migrate_state.clone(),
601
666
  fake=fake,
667
+ atomic_batch=use_atomic_batch,
602
668
  )
603
669
  # post_migrate signals have access to all models. Ensure that all models
604
670
  # are reloaded in case any are delayed.
@@ -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