summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornessita <124304+nessita@users.noreply.github.com>2024-06-26 12:13:17 -0300
committerNatalia <124304+nessita@users.noreply.github.com>2024-06-26 12:14:31 -0300
commit432b069b5f194d821c12208c65e88856ac52cc28 (patch)
treea93e376df60782a986ba507bb447f2ac9e45a1f7
parent3f72c8b5a27f8cde3cab29e8b4a2f2972665cae5 (diff)
[5.1.x] Fixed #35561 -- Made *args and **kwargs parsing more strict in Model.save()/asave().
Backport of e56a32b89bb7fadffdfaa2cdf12b4863ccd5af9b from main.
-rw-r--r--django/db/models/base.py88
-rw-r--r--tests/basic/tests.py34
2 files changed, 85 insertions, 37 deletions
diff --git a/django/db/models/base.py b/django/db/models/base.py
index dcfdd6eade..97fcdb81a4 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -776,6 +776,43 @@ class Model(AltersData, metaclass=ModelBase):
return getattr(self, field_name)
return getattr(self, field.attname)
+ # RemovedInDjango60Warning: When the deprecation ends, remove completely.
+ def _parse_params(self, *args, method_name, **kwargs):
+ defaults = {
+ "force_insert": False,
+ "force_update": False,
+ "using": None,
+ "update_fields": None,
+ }
+
+ warnings.warn(
+ f"Passing positional arguments to {method_name}() is deprecated",
+ RemovedInDjango60Warning,
+ stacklevel=2,
+ )
+ total_len_args = len(args) + 1 # include self
+ max_len_args = len(defaults) + 1
+ if total_len_args > max_len_args:
+ # Recreate the proper TypeError message from Python.
+ raise TypeError(
+ f"Model.{method_name}() takes from 1 to {max_len_args} positional "
+ f"arguments but {total_len_args} were given"
+ )
+
+ def get_param(param_name, param_value, arg_index):
+ if arg_index < len(args):
+ if param_value is not defaults[param_name]:
+ # Recreate the proper TypeError message from Python.
+ raise TypeError(
+ f"Model.{method_name}() got multiple values for argument "
+ f"'{param_name}'"
+ )
+ return args[arg_index]
+
+ return param_value
+
+ return [get_param(k, v, i) for i, (k, v) in enumerate(kwargs.items())]
+
# RemovedInDjango60Warning: When the deprecation ends, replace with:
# def save(
# self, *, force_insert=False, force_update=False, using=None, update_fields=None,
@@ -798,25 +835,14 @@ class Model(AltersData, metaclass=ModelBase):
"""
# RemovedInDjango60Warning.
if args:
- warnings.warn(
- "Passing positional arguments to save() is deprecated",
- RemovedInDjango60Warning,
- stacklevel=2,
+ force_insert, force_update, using, update_fields = self._parse_params(
+ *args,
+ method_name="save",
+ force_insert=force_insert,
+ force_update=force_update,
+ using=using,
+ update_fields=update_fields,
)
- total_len_args = len(args) + 1 # include self
- if total_len_args > 5:
- # Recreate the proper TypeError message from Python.
- raise TypeError(
- "Model.save() takes from 1 to 5 positional arguments but "
- f"{total_len_args} were given"
- )
- force_insert = args[0]
- try:
- force_update = args[1]
- using = args[2]
- update_fields = args[3]
- except IndexError:
- pass
self._prepare_related_fields_for_save(operation_name="save")
@@ -885,26 +911,14 @@ class Model(AltersData, metaclass=ModelBase):
):
# RemovedInDjango60Warning.
if args:
- warnings.warn(
- "Passing positional arguments to asave() is deprecated",
- RemovedInDjango60Warning,
- stacklevel=2,
+ force_insert, force_update, using, update_fields = self._parse_params(
+ *args,
+ method_name="asave",
+ force_insert=force_insert,
+ force_update=force_update,
+ using=using,
+ update_fields=update_fields,
)
- total_len_args = len(args) + 1 # include self
- if total_len_args > 5:
- # Recreate the proper TypeError message from Python.
- raise TypeError(
- "Model.asave() takes from 1 to 5 positional arguments but "
- f"{total_len_args} were given"
- )
- force_insert = args[0]
- try:
- force_update = args[1]
- using = args[2]
- update_fields = args[3]
- except IndexError:
- pass
-
return await sync_to_async(self.save)(
force_insert=force_insert,
force_update=force_update,
diff --git a/tests/basic/tests.py b/tests/basic/tests.py
index 4e89febed2..6fb67f7e6e 100644
--- a/tests/basic/tests.py
+++ b/tests/basic/tests.py
@@ -239,6 +239,23 @@ class ModelInstanceCreationTests(TestCase):
):
a.save(False, False, None, None, None)
+ def test_save_conflicting_positional_and_named_arguments(self):
+ a = Article()
+ cases = [
+ ("force_insert", True, [42]),
+ ("force_update", None, [42, 41]),
+ ("using", "some-db", [42, 41, 40]),
+ ("update_fields", ["foo"], [42, 41, 40, 39]),
+ ]
+ for param_name, param_value, args in cases:
+ with self.subTest(param_name=param_name):
+ msg = f"Model.save() got multiple values for argument '{param_name}'"
+ with (
+ self.assertWarns(RemovedInDjango60Warning),
+ self.assertRaisesMessage(TypeError, msg),
+ ):
+ a.save(*args, **{param_name: param_value})
+
async def test_asave_deprecation(self):
a = Article(headline="original", pub_date=datetime(2014, 5, 16))
msg = "Passing positional arguments to asave() is deprecated"
@@ -275,6 +292,23 @@ class ModelInstanceCreationTests(TestCase):
):
await a.asave(False, False, None, None, None)
+ async def test_asave_conflicting_positional_and_named_arguments(self):
+ a = Article()
+ cases = [
+ ("force_insert", True, [42]),
+ ("force_update", None, [42, 41]),
+ ("using", "some-db", [42, 41, 40]),
+ ("update_fields", ["foo"], [42, 41, 40, 39]),
+ ]
+ for param_name, param_value, args in cases:
+ with self.subTest(param_name=param_name):
+ msg = f"Model.asave() got multiple values for argument '{param_name}'"
+ with (
+ self.assertWarns(RemovedInDjango60Warning),
+ self.assertRaisesMessage(TypeError, msg),
+ ):
+ await a.asave(*args, **{param_name: param_value})
+
@ignore_warnings(category=RemovedInDjango60Warning)
def test_save_positional_arguments(self):
a = Article.objects.create(headline="original", pub_date=datetime(2014, 5, 16))