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.
- {plain_models-0.47.0 → plain_models-0.49.0}/.gitignore +1 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/PKG-INFO +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/CHANGELOG.md +27 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/__init__.py +0 -3
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/aggregates.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/creation.py +1 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/compiler.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/operations.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/base.py +34 -94
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/cli.py +66 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/constraints.py +2 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/deletion.py +3 -5
- plain_models-0.49.0/plain/models/exceptions.py +196 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/expressions.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/__init__.py +3 -41
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related.py +15 -22
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_descriptors.py +1 -3
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_managers.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/reverse_related.py +3 -5
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/forms.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/lookups.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/executor.py +29 -27
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/state.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/options.py +4 -16
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/query.py +12 -10
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/query_utils.py +2 -2
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/compiler.py +6 -6
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/datastructures.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/query.py +4 -4
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/subqueries.py +3 -3
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/where.py +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/pyproject.toml +1 -1
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_exceptions.py +1 -1
- plain_models-0.47.0/plain/models/exceptions.py +0 -88
- {plain_models-0.47.0 → plain_models-0.49.0}/LICENSE +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/README.md +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/AGENTS.md +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/README.md +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/schema.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backends/utils.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/cli.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/clients.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/backups/core.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/config.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/connections.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/constants.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/database_url.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/db.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/default_settings.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/entrypoints.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/enums.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/json.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/fields/related_lookups.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/math.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/text.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/functions/window.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/indexes.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/autodetector.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/loader.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/migration.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/models.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/operations/special.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/recorder.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/serializer.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/otel.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/preflight.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/registry.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/sql/constants.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/pytest.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/test/utils.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/transaction.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/plain/models/utils.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/examples/models.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/settings.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/app/urls.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_database_url.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_delete_behaviors.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_manager_assignment.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_models.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_related_descriptors.py +0 -0
- {plain_models-0.47.0 → plain_models-0.49.0}/tests/test_related_manager_api.py +0 -0
@@ -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.
|
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
198
|
-
|
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.
|
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
|
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
|
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
|
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
|
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
|
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
|