plain.models 0.44.0__py3-none-any.whl → 0.46.0__py3-none-any.whl

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.
@@ -23,7 +23,7 @@ class JSONField(CheckFieldDefaultMixin, Field):
23
23
  default_error_messages = {
24
24
  "invalid": "Value must be valid JSON.",
25
25
  }
26
- _default_hint = ("dict", "{}")
26
+ _default_fix = ("dict", "{}")
27
27
 
28
28
  def __init__(
29
29
  self,
@@ -40,31 +40,31 @@ class JSONField(CheckFieldDefaultMixin, Field):
40
40
  self.decoder = decoder
41
41
  super().__init__(**kwargs)
42
42
 
43
- def check(self, **kwargs):
44
- errors = super().check(**kwargs)
45
- database = kwargs.get("database", False)
46
- errors.extend(self._check_supported(database))
43
+ def preflight(self, **kwargs):
44
+ errors = super().preflight(**kwargs)
45
+ errors.extend(self._check_supported())
47
46
  return errors
48
47
 
49
- def _check_supported(self, database):
48
+ def _check_supported(self):
50
49
  errors = []
51
- if database:
52
- if (
53
- self.model._meta.required_db_vendor
54
- and self.model._meta.required_db_vendor != db_connection.vendor
55
- ):
56
- return errors
57
- if not (
58
- "supports_json_field" in self.model._meta.required_db_features
59
- or db_connection.features.supports_json_field
60
- ):
61
- errors.append(
62
- preflight.Error(
63
- f"{db_connection.display_name} does not support JSONFields.",
64
- obj=self.model,
65
- id="fields.E180",
66
- )
50
+
51
+ if (
52
+ self.model._meta.required_db_vendor
53
+ and self.model._meta.required_db_vendor != db_connection.vendor
54
+ ):
55
+ return errors
56
+
57
+ if not (
58
+ "supports_json_field" in self.model._meta.required_db_features
59
+ or db_connection.features.supports_json_field
60
+ ):
61
+ errors.append(
62
+ preflight.PreflightResult(
63
+ fix=f"{db_connection.display_name} does not support JSONFields. Consider using a TextField with JSON serialization or upgrade to a database that supports JSON fields.",
64
+ obj=self.model,
65
+ id="fields.json_field_unsupported",
67
66
  )
67
+ )
68
68
  return errors
69
69
 
70
70
  def deconstruct(self):
@@ -1,4 +1,4 @@
1
- from plain import preflight
1
+ from plain.preflight import PreflightResult
2
2
 
3
3
  NOT_PROVIDED = object()
4
4
 
@@ -29,7 +29,7 @@ class FieldCacheMixin:
29
29
 
30
30
 
31
31
  class CheckFieldDefaultMixin:
32
- _default_hint = ("<valid default>", "<invalid default>")
32
+ _default_fix = ("<valid default>", "<invalid default>")
33
33
 
34
34
  def _check_default(self):
35
35
  if (
@@ -38,21 +38,22 @@ class CheckFieldDefaultMixin:
38
38
  and not callable(self.default)
39
39
  ):
40
40
  return [
41
- preflight.Warning(
42
- f"{self.__class__.__name__} default should be a callable instead of an instance "
43
- "so that it's not shared between all field instances.",
44
- hint=(
41
+ PreflightResult(
42
+ fix=(
43
+ f"{self.__class__.__name__} default should be a callable instead of an instance "
44
+ "so that it's not shared between all field instances. "
45
45
  "Use a callable instead, e.g., use `{}` instead of "
46
- "`{}`.".format(*self._default_hint)
46
+ "`{}`.".format(*self._default_fix)
47
47
  ),
48
48
  obj=self,
49
- id="fields.E010",
49
+ id="fields.invalid_choice_mixin_default",
50
+ warning=True,
50
51
  )
51
52
  ]
52
53
  else:
53
54
  return []
54
55
 
55
- def check(self, **kwargs):
56
- errors = super().check(**kwargs)
56
+ def preflight(self, **kwargs):
57
+ errors = super().preflight(**kwargs)
57
58
  errors.extend(self._check_default())
58
59
  return errors
@@ -1,10 +1,11 @@
1
1
  from functools import cached_property, partial
2
2
 
3
- from plain import exceptions, preflight
3
+ from plain import exceptions
4
4
  from plain.models.constants import LOOKUP_SEP
5
5
  from plain.models.deletion import SET_DEFAULT, SET_NULL
6
6
  from plain.models.query_utils import PathInfo, Q
7
7
  from plain.models.utils import make_model_tuple
8
+ from plain.preflight import PreflightResult
8
9
  from plain.runtime import SettingsReference
9
10
 
10
11
  from ..registry import models_registry
@@ -106,9 +107,9 @@ class RelatedField(FieldCacheMixin, Field):
106
107
  models_registry.check_ready()
107
108
  return self.remote_field.model
108
109
 
109
- def check(self, **kwargs):
110
+ def preflight(self, **kwargs):
110
111
  return [
111
- *super().check(**kwargs),
112
+ *super().preflight(**kwargs),
112
113
  *self._check_related_name_is_valid(),
113
114
  *self._check_related_query_name_is_valid(),
114
115
  *self._check_relation_model_exists(),
@@ -126,11 +127,10 @@ class RelatedField(FieldCacheMixin, Field):
126
127
  )
127
128
  if not is_valid_id:
128
129
  return [
129
- preflight.Error(
130
- f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model._meta.object_name}.{self.name}",
131
- hint="Related name must be a valid Python identifier.",
130
+ PreflightResult(
131
+ fix=f"The name '{self.remote_field.related_name}' is invalid related_name for field {self.model._meta.object_name}.{self.name}. Related name must be a valid Python identifier.",
132
132
  obj=self,
133
- id="fields.E306",
133
+ id="fields.invalid_related_name",
134
134
  )
135
135
  ]
136
136
  return []
@@ -142,26 +142,26 @@ class RelatedField(FieldCacheMixin, Field):
142
142
  errors = []
143
143
  if rel_query_name.endswith("_"):
144
144
  errors.append(
145
- preflight.Error(
146
- f"Reverse query name '{rel_query_name}' must not end with an underscore.",
147
- hint=(
145
+ PreflightResult(
146
+ fix=(
147
+ f"Reverse query name '{rel_query_name}' must not end with an underscore. "
148
148
  "Add or change a related_name or related_query_name "
149
149
  "argument for this field."
150
150
  ),
151
151
  obj=self,
152
- id="fields.E308",
152
+ id="fields.related_field_accessor_clash",
153
153
  )
154
154
  )
155
155
  if LOOKUP_SEP in rel_query_name:
156
156
  errors.append(
157
- preflight.Error(
158
- f"Reverse query name '{rel_query_name}' must not contain '{LOOKUP_SEP}'.",
159
- hint=(
157
+ PreflightResult(
158
+ fix=(
159
+ f"Reverse query name '{rel_query_name}' must not contain '{LOOKUP_SEP}'. "
160
160
  "Add or change a related_name or related_query_name "
161
161
  "argument for this field."
162
162
  ),
163
163
  obj=self,
164
- id="fields.E309",
164
+ id="fields.related_field_query_name_clash",
165
165
  )
166
166
  )
167
167
  return errors
@@ -178,11 +178,13 @@ class RelatedField(FieldCacheMixin, Field):
178
178
  )
179
179
  if rel_is_missing and rel_is_string:
180
180
  return [
181
- preflight.Error(
182
- f"Field defines a relation with model '{model_name}', which is either "
183
- "not installed, or is abstract.",
181
+ PreflightResult(
182
+ fix=(
183
+ f"Field defines a relation with model '{model_name}', which is either "
184
+ "not installed, or is abstract. Ensure the model is installed and not abstract."
185
+ ),
184
186
  obj=self,
185
- id="fields.E300",
187
+ id="fields.related_model_not_installed",
186
188
  )
187
189
  ]
188
190
  return []
@@ -230,29 +232,29 @@ class RelatedField(FieldCacheMixin, Field):
230
232
  clash_name = f"{rel_opts.label}.{clash_field.name}"
231
233
  if not rel_is_hidden and clash_field.name == rel_name:
232
234
  errors.append(
233
- preflight.Error(
234
- f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
235
- f"for '{field_name}' clashes with field name "
236
- f"'{clash_name}'.",
237
- hint=(
235
+ PreflightResult(
236
+ fix=(
237
+ f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
238
+ f"for '{field_name}' clashes with field name "
239
+ f"'{clash_name}'. "
238
240
  f"Rename field '{clash_name}', or add/change a related_name "
239
241
  f"argument to the definition for field '{field_name}'."
240
242
  ),
241
243
  obj=self,
242
- id="fields.E302",
244
+ id="fields.related_accessor_clash_field",
243
245
  )
244
246
  )
245
247
 
246
248
  if clash_field.name == rel_query_name:
247
249
  errors.append(
248
- preflight.Error(
249
- f"Reverse query name for '{field_name}' clashes with field name '{clash_name}'.",
250
- hint=(
250
+ PreflightResult(
251
+ fix=(
252
+ f"Reverse query name for '{field_name}' clashes with field name '{clash_name}'. "
251
253
  f"Rename field '{clash_name}', or add/change a related_name "
252
254
  f"argument to the definition for field '{field_name}'."
253
255
  ),
254
256
  obj=self,
255
- id="fields.E303",
257
+ id="fields.related_accessor_clash_manager",
256
258
  )
257
259
  )
258
260
 
@@ -267,30 +269,30 @@ class RelatedField(FieldCacheMixin, Field):
267
269
  )
268
270
  if not rel_is_hidden and clash_field.get_accessor_name() == rel_name:
269
271
  errors.append(
270
- preflight.Error(
271
- f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
272
- f"for '{field_name}' clashes with reverse accessor for "
273
- f"'{clash_name}'.",
274
- hint=(
272
+ PreflightResult(
273
+ fix=(
274
+ f"Reverse accessor '{rel_opts.object_name}.{rel_name}' "
275
+ f"for '{field_name}' clashes with reverse accessor for "
276
+ f"'{clash_name}'. "
275
277
  "Add or change a related_name argument "
276
278
  f"to the definition for '{field_name}' or '{clash_name}'."
277
279
  ),
278
280
  obj=self,
279
- id="fields.E304",
281
+ id="fields.related_name_clash",
280
282
  )
281
283
  )
282
284
 
283
285
  if clash_field.get_accessor_name() == rel_query_name:
284
286
  errors.append(
285
- preflight.Error(
286
- f"Reverse query name for '{field_name}' clashes with reverse query name "
287
- f"for '{clash_name}'.",
288
- hint=(
287
+ PreflightResult(
288
+ fix=(
289
+ f"Reverse query name for '{field_name}' clashes with reverse query name "
290
+ f"for '{clash_name}'. "
289
291
  "Add or change a related_name argument "
290
292
  f"to the definition for '{field_name}' or '{clash_name}'."
291
293
  ),
292
294
  obj=self,
293
- id="fields.E305",
295
+ id="fields.related_query_name_clash",
294
296
  )
295
297
  )
296
298
 
@@ -600,9 +602,9 @@ class ForeignKey(RelatedField):
600
602
  self.remote_field.limit_choices_to
601
603
  )
602
604
 
603
- def check(self, **kwargs):
605
+ def preflight(self, **kwargs):
604
606
  return [
605
- *super().check(**kwargs),
607
+ *super().preflight(**kwargs),
606
608
  *self._check_on_delete(),
607
609
  ]
608
610
 
@@ -610,23 +612,24 @@ class ForeignKey(RelatedField):
610
612
  on_delete = getattr(self.remote_field, "on_delete", None)
611
613
  if on_delete == SET_NULL and not self.allow_null:
612
614
  return [
613
- preflight.Error(
614
- "Field specifies on_delete=SET_NULL, but cannot be null.",
615
- hint=(
616
- "Set allow_null=True argument on the field, or change the on_delete "
617
- "rule."
615
+ PreflightResult(
616
+ fix=(
617
+ "Field specifies on_delete=SET_NULL, but cannot be null. "
618
+ "Set allow_null=True argument on the field, or change the on_delete rule."
618
619
  ),
619
620
  obj=self,
620
- id="fields.E320",
621
+ id="fields.foreign_key_null_constraint_violation",
621
622
  )
622
623
  ]
623
624
  elif on_delete == SET_DEFAULT and not self.has_default():
624
625
  return [
625
- preflight.Error(
626
- "Field specifies on_delete=SET_DEFAULT, but has no default value.",
627
- hint="Set a default value, or change the on_delete rule.",
626
+ PreflightResult(
627
+ fix=(
628
+ "Field specifies on_delete=SET_DEFAULT, but has no default value. "
629
+ "Set a default value, or change the on_delete rule."
630
+ ),
628
631
  obj=self,
629
- id="fields.E321",
632
+ id="fields.foreign_key_set_default_no_default",
630
633
  )
631
634
  ]
632
635
  else:
@@ -834,9 +837,9 @@ class ManyToManyField(RelatedField):
834
837
  **kwargs,
835
838
  )
836
839
 
837
- def check(self, **kwargs):
840
+ def preflight(self, **kwargs):
838
841
  return [
839
- *super().check(**kwargs),
842
+ *super().preflight(**kwargs),
840
843
  *self._check_relationship_model(**kwargs),
841
844
  *self._check_ignored_options(**kwargs),
842
845
  *self._check_table_uniqueness(**kwargs),
@@ -847,36 +850,43 @@ class ManyToManyField(RelatedField):
847
850
 
848
851
  if self.has_null_arg:
849
852
  warnings.append(
850
- preflight.Warning(
851
- "null has no effect on ManyToManyField.",
853
+ PreflightResult(
854
+ fix="The 'null' option has no effect on ManyToManyField. Remove the 'null' argument.",
852
855
  obj=self,
853
- id="fields.W340",
856
+ id="fields.m2m_null_has_no_effect",
857
+ warning=True,
854
858
  )
855
859
  )
856
860
 
857
861
  if self._validators:
858
862
  warnings.append(
859
- preflight.Warning(
860
- "ManyToManyField does not support validators.",
863
+ PreflightResult(
864
+ fix="ManyToManyField does not support validators. Remove validators from this field.",
861
865
  obj=self,
862
- id="fields.W341",
866
+ id="fields.m2m_validators_not_supported",
867
+ warning=True,
863
868
  )
864
869
  )
865
870
  if self.remote_field.symmetrical and self._related_name:
866
871
  warnings.append(
867
- preflight.Warning(
868
- "related_name has no effect on ManyToManyField "
869
- 'with a symmetrical relationship, e.g. to "self".',
872
+ PreflightResult(
873
+ fix=(
874
+ "The 'related_name' argument has no effect on ManyToManyField "
875
+ 'with a symmetrical relationship, e.g. to "self". '
876
+ "Remove the 'related_name' argument or set symmetrical=False."
877
+ ),
870
878
  obj=self,
871
- id="fields.W345",
879
+ id="fields.m2m_related_name_no_effect_symmetrical",
880
+ warning=True,
872
881
  )
873
882
  )
874
883
  if self.db_comment:
875
884
  warnings.append(
876
- preflight.Warning(
877
- "db_comment has no effect on ManyToManyField.",
885
+ PreflightResult(
886
+ fix="The 'db_comment' option has no effect on ManyToManyField. Remove the 'db_comment' argument.",
878
887
  obj=self,
879
- id="fields.W346",
888
+ id="fields.m2m_db_comment_has_no_effect",
889
+ warning=True,
880
890
  )
881
891
  )
882
892
 
@@ -893,11 +903,14 @@ class ManyToManyField(RelatedField):
893
903
  if self.remote_field.through not in self.opts.models_registry.get_models():
894
904
  # The relationship model is not installed.
895
905
  errors.append(
896
- preflight.Error(
897
- "Field specifies a many-to-many relation through model "
898
- f"'{qualified_model_name}', which has not been installed.",
906
+ PreflightResult(
907
+ fix=(
908
+ "Field specifies a many-to-many relation through model "
909
+ f"'{qualified_model_name}', which has not been installed. "
910
+ "Ensure the through model is properly defined and installed."
911
+ ),
899
912
  obj=self,
900
- id="fields.E331",
913
+ id="fields.m2m_through_model_not_installed",
901
914
  )
902
915
  )
903
916
 
@@ -925,18 +938,16 @@ class ManyToManyField(RelatedField):
925
938
 
926
939
  if seen_self > 2 and not self.remote_field.through_fields:
927
940
  errors.append(
928
- preflight.Error(
929
- "The model is used as an intermediate model by "
930
- f"'{self}', but it has more than two foreign keys "
931
- f"to '{from_model_name}', which is ambiguous. You must specify "
932
- "which two foreign keys Plain should use via the "
933
- "through_fields keyword argument.",
934
- hint=(
941
+ PreflightResult(
942
+ fix=(
943
+ "The model is used as an intermediate model by "
944
+ f"'{self}', but it has more than two foreign keys "
945
+ f"to '{from_model_name}', which is ambiguous. "
935
946
  "Use through_fields to specify which two foreign keys "
936
947
  "Plain should use."
937
948
  ),
938
949
  obj=self.remote_field.through,
939
- id="fields.E333",
950
+ id="fields.m2m_through_model_ambiguous_fks",
940
951
  )
941
952
  )
942
953
 
@@ -953,47 +964,48 @@ class ManyToManyField(RelatedField):
953
964
 
954
965
  if seen_from > 1 and not self.remote_field.through_fields:
955
966
  errors.append(
956
- preflight.Error(
957
- (
967
+ PreflightResult(
968
+ fix=(
958
969
  "The model is used as an intermediate model by "
959
970
  f"'{self}', but it has more than one foreign key "
960
971
  f"from '{from_model_name}', which is ambiguous. You must specify "
961
972
  "which foreign key Plain should use via the "
962
- "through_fields keyword argument."
963
- ),
964
- hint=(
973
+ "through_fields keyword argument. "
965
974
  "If you want to create a recursive relationship, "
966
975
  f'use ManyToManyField("{RECURSIVE_RELATIONSHIP_CONSTANT}", through="{relationship_model_name}").'
967
976
  ),
968
977
  obj=self,
969
- id="fields.E334",
978
+ id="fields.m2m_through_model_invalid_recursive_from",
970
979
  )
971
980
  )
972
981
 
973
982
  if seen_to > 1 and not self.remote_field.through_fields:
974
983
  errors.append(
975
- preflight.Error(
976
- "The model is used as an intermediate model by "
977
- f"'{self}', but it has more than one foreign key "
978
- f"to '{to_model_name}', which is ambiguous. You must specify "
979
- "which foreign key Plain should use via the "
980
- "through_fields keyword argument.",
981
- hint=(
984
+ PreflightResult(
985
+ fix=(
986
+ "The model is used as an intermediate model by "
987
+ f"'{self}', but it has more than one foreign key "
988
+ f"to '{to_model_name}', which is ambiguous. You must specify "
989
+ "which foreign key Plain should use via the "
990
+ "through_fields keyword argument. "
982
991
  "If you want to create a recursive relationship, "
983
992
  f'use ManyToManyField("{RECURSIVE_RELATIONSHIP_CONSTANT}", through="{relationship_model_name}").'
984
993
  ),
985
994
  obj=self,
986
- id="fields.E335",
995
+ id="fields.m2m_through_model_invalid_recursive_to",
987
996
  )
988
997
  )
989
998
 
990
999
  if seen_from == 0 or seen_to == 0:
991
1000
  errors.append(
992
- preflight.Error(
993
- "The model is used as an intermediate model by "
994
- f"'{self}', but it does not have a foreign key to '{from_model_name}' or '{to_model_name}'.",
1001
+ PreflightResult(
1002
+ fix=(
1003
+ "The model is used as an intermediate model by "
1004
+ f"'{self}', but it does not have a foreign key to '{from_model_name}' or '{to_model_name}'. "
1005
+ "Add the required foreign keys to the through model."
1006
+ ),
995
1007
  obj=self.remote_field.through,
996
- id="fields.E336",
1008
+ id="fields.m2m_through_model_missing_fk",
997
1009
  )
998
1010
  )
999
1011
 
@@ -1007,16 +1019,16 @@ class ManyToManyField(RelatedField):
1007
1019
  and self.remote_field.through_fields[1]
1008
1020
  ):
1009
1021
  errors.append(
1010
- preflight.Error(
1011
- "Field specifies 'through_fields' but does not provide "
1012
- "the names of the two link fields that should be used "
1013
- f"for the relation through model '{qualified_model_name}'.",
1014
- hint=(
1022
+ PreflightResult(
1023
+ fix=(
1024
+ "Field specifies 'through_fields' but does not provide "
1025
+ "the names of the two link fields that should be used "
1026
+ f"for the relation through model '{qualified_model_name}'. "
1015
1027
  "Make sure you specify 'through_fields' as "
1016
- "through_fields=('field1', 'field2')"
1028
+ "through_fields=('field1', 'field2')."
1017
1029
  ),
1018
1030
  obj=self,
1019
- id="fields.E337",
1031
+ id="fields.m2m_through_fields_wrong_length",
1020
1032
  )
1021
1033
  )
1022
1034
 
@@ -1051,7 +1063,7 @@ class ManyToManyField(RelatedField):
1051
1063
  ):
1052
1064
  possible_field_names.append(f.name)
1053
1065
  if possible_field_names:
1054
- hint = (
1066
+ fix = (
1055
1067
  "Did you mean one of the following foreign keys to '{}': "
1056
1068
  "{}?".format(
1057
1069
  related_model._meta.object_name,
@@ -1059,17 +1071,16 @@ class ManyToManyField(RelatedField):
1059
1071
  )
1060
1072
  )
1061
1073
  else:
1062
- hint = None
1074
+ fix = ""
1063
1075
 
1064
1076
  try:
1065
1077
  field = through._meta.get_field(field_name)
1066
1078
  except exceptions.FieldDoesNotExist:
1067
1079
  errors.append(
1068
- preflight.Error(
1069
- f"The intermediary model '{qualified_model_name}' has no field '{field_name}'.",
1070
- hint=hint,
1080
+ PreflightResult(
1081
+ fix=f"The intermediary model '{qualified_model_name}' has no field '{field_name}'. {fix}",
1071
1082
  obj=self,
1072
- id="fields.E338",
1083
+ id="fields.m2m_through_field_not_found",
1073
1084
  )
1074
1085
  )
1075
1086
  else:
@@ -1079,11 +1090,10 @@ class ManyToManyField(RelatedField):
1079
1090
  == related_model
1080
1091
  ):
1081
1092
  errors.append(
1082
- preflight.Error(
1083
- f"'{through._meta.object_name}.{field_name}' is not a foreign key to '{related_model._meta.object_name}'.",
1084
- hint=hint,
1093
+ PreflightResult(
1094
+ fix=f"'{through._meta.object_name}.{field_name}' is not a foreign key to '{related_model._meta.object_name}'. {fix}",
1085
1095
  obj=self,
1086
- id="fields.E339",
1096
+ id="fields.m2m_through_field_not_fk_to_model",
1087
1097
  )
1088
1098
  )
1089
1099
 
@@ -1108,12 +1118,14 @@ class ManyToManyField(RelatedField):
1108
1118
  ):
1109
1119
  clashing_obj = model._meta.label
1110
1120
  return [
1111
- preflight.Error(
1112
- f"The field's intermediary table '{m2m_db_table}' clashes with the "
1113
- f"table name of '{clashing_obj}'.",
1121
+ PreflightResult(
1122
+ fix=(
1123
+ f"The field's intermediary table '{m2m_db_table}' clashes with the "
1124
+ f"table name of '{clashing_obj}'. "
1125
+ "Change the through model's db_table or use a different model."
1126
+ ),
1114
1127
  obj=self,
1115
- hint=None,
1116
- id="fields.E340",
1128
+ id="fields.m2m_table_name_clash",
1117
1129
  )
1118
1130
  ]
1119
1131
  return []
@@ -521,7 +521,7 @@ class StateModelsRegistry(ModelsRegistry):
521
521
  from plain.models.preflight import _check_lazy_references
522
522
 
523
523
  if errors := _check_lazy_references(self, packages_registry):
524
- raise ValueError("\n".join(error.msg for error in errors))
524
+ raise ValueError("\n".join(error.message for error in errors))
525
525
 
526
526
  @contextmanager
527
527
  def bulk_update(self):