1
0
mirror of https://github.com/django/django.git synced 2025-10-31 09:41:08 +00:00

Refs #33308 -- Deprecated support for passing encoded JSON string literals to JSONField & co.

JSON should be provided as literal Python objects an not in their
encoded string literal forms.
This commit is contained in:
Simon Charette
2022-11-02 22:03:05 -04:00
committed by Mariusz Felisiak
parent d3e746ace5
commit 0ff46591ac
11 changed files with 230 additions and 30 deletions

View File

@@ -1,8 +1,9 @@
import json
import warnings
from django.contrib.postgres.fields import ArrayField
from django.db.models import Aggregate, BooleanField, JSONField, TextField, Value
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
from .mixins import OrderableAggMixin
@@ -31,6 +32,14 @@ class DeprecatedConvertValueMixin:
self._default_provided = True
super().__init__(*expressions, default=default, **extra)
def resolve_expression(self, *args, **kwargs):
resolved = super().resolve_expression(*args, **kwargs)
if not self._default_provided:
resolved.empty_result_set_value = getattr(
self, "deprecation_empty_result_set_value", self.deprecation_value
)
return resolved
def convert_value(self, value, expression, connection):
if value is None and not self._default_provided:
warnings.warn(self.deprecation_msg, category=RemovedInDjango50Warning)
@@ -48,8 +57,7 @@ class ArrayAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
deprecation_msg = (
"In Django 5.0, ArrayAgg() will return None instead of an empty list "
"if there are no rows. Pass default=None to opt into the new behavior "
"and silence this warning or default=Value([]) to keep the previous "
"behavior."
"and silence this warning or default=[] to keep the previous behavior."
)
@property
@@ -87,13 +95,46 @@ class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
# RemovedInDjango50Warning
deprecation_value = "[]"
deprecation_empty_result_set_value = property(lambda self: [])
deprecation_msg = (
"In Django 5.0, JSONBAgg() will return None instead of an empty list "
"if there are no rows. Pass default=None to opt into the new behavior "
"and silence this warning or default=Value('[]') to keep the previous "
"and silence this warning or default=[] to keep the previous "
"behavior."
)
# RemovedInDjango51Warning: When the deprecation ends, remove __init__().
#
# RemovedInDjango50Warning: When the deprecation ends, replace with:
# def __init__(self, *expressions, default=None, **extra):
def __init__(self, *expressions, default=NOT_PROVIDED, **extra):
super().__init__(*expressions, default=default, **extra)
if (
isinstance(default, Value)
and isinstance(default.value, str)
and not isinstance(default.output_field, JSONField)
):
value = default.value
try:
decoded = json.loads(value)
except json.JSONDecodeError:
warnings.warn(
"Passing a Value() with an output_field that isn't a JSONField as "
"JSONBAgg(default) is deprecated. Pass default="
f"Value({value!r}, output_field=JSONField()) instead.",
stacklevel=2,
category=RemovedInDjango51Warning,
)
self.default.output_field = self.output_field
else:
self.default = Value(decoded, self.output_field)
warnings.warn(
"Passing an encoded JSON string as JSONBAgg(default) is "
f"deprecated. Pass default={decoded!r} instead.",
stacklevel=2,
category=RemovedInDjango51Warning,
)
class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
function = "STRING_AGG"
@@ -106,8 +147,7 @@ class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
deprecation_msg = (
"In Django 5.0, StringAgg() will return None instead of an empty "
"string if there are no rows. Pass default=None to opt into the new "
"behavior and silence this warning or default=Value('') to keep the "
"previous behavior."
'behavior and silence this warning or default="" to keep the previous behavior.'
)
def __init__(self, expression, delimiter, **extra):

View File

@@ -85,6 +85,8 @@ class Aggregate(Func):
return c
if hasattr(default, "resolve_expression"):
default = default.resolve_expression(query, allow_joins, reuse, summarize)
if default._output_field_or_none is None:
default.output_field = c._output_field_or_none
else:
default = Value(default, c._output_field_or_none)
c.default = None # Reset the default argument before wrapping.

View File

@@ -921,6 +921,8 @@ class Field(RegisterLookupMixin):
def get_db_prep_save(self, value, connection):
"""Return field's value prepared for saving into a database."""
if hasattr(value, "as_sql"):
return value
return self.get_db_prep_value(value, connection=connection, prepared=False)
def has_default(self):
@@ -1715,6 +1717,8 @@ class DecimalField(Field):
def get_db_prep_value(self, value, connection, prepared=False):
if not prepared:
value = self.get_prep_value(value)
if hasattr(value, "as_sql"):
return value
return connection.ops.adapt_decimalfield_value(
value, self.max_digits, self.decimal_places
)

View File

@@ -1,9 +1,10 @@
import json
import warnings
from django import forms
from django.core import checks, exceptions
from django.db import NotSupportedError, connections, router
from django.db.models import lookups
from django.db.models import expressions, lookups
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields import TextField
from django.db.models.lookups import (
@@ -11,6 +12,7 @@ from django.db.models.lookups import (
PostgresOperatorLookup,
Transform,
)
from django.utils.deprecation import RemovedInDjango51Warning
from django.utils.translation import gettext_lazy as _
from . import Field
@@ -97,7 +99,32 @@ class JSONField(CheckFieldDefaultMixin, Field):
return "JSONField"
def get_db_prep_value(self, value, connection, prepared=False):
if hasattr(value, "as_sql"):
# RemovedInDjango51Warning: When the deprecation ends, replace with:
# if (
# isinstance(value, expressions.Value)
# and isinstance(value.output_field, JSONField)
# ):
# value = value.value
# elif hasattr(value, "as_sql"): ...
if isinstance(value, expressions.Value):
if isinstance(value.value, str) and not isinstance(
value.output_field, JSONField
):
try:
value = json.loads(value.value, cls=self.decoder)
except json.JSONDecodeError:
value = value.value
else:
warnings.warn(
"Providing an encoded JSON string via Value() is deprecated. "
f"Use Value({value!r}, output_field=JSONField()) instead.",
category=RemovedInDjango51Warning,
)
elif isinstance(value.output_field, JSONField):
value = value.value
else:
return value
elif hasattr(value, "as_sql"):
return value
return connection.ops.adapt_json_value(value, self.encoder)

View File

@@ -1637,9 +1637,7 @@ class SQLInsertCompiler(SQLCompiler):
"Window expressions are not allowed in this query (%s=%r)."
% (field.name, value)
)
else:
value = field.get_db_prep_save(value, connection=self.connection)
return value
return field.get_db_prep_save(value, connection=self.connection)
def pre_save_val(self, field, obj):
"""
@@ -1893,18 +1891,14 @@ class SQLUpdateCompiler(SQLCompiler):
)
elif hasattr(val, "prepare_database_save"):
if field.remote_field:
val = field.get_db_prep_save(
val.prepare_database_save(field),
connection=self.connection,
)
val = val.prepare_database_save(field)
else:
raise TypeError(
"Tried to update field %s with a model instance, %r. "
"Use a value compatible with %s."
% (field, val, field.__class__.__name__)
)
else:
val = field.get_db_prep_save(val, connection=self.connection)
val = field.get_db_prep_save(val, connection=self.connection)
# Getting the placeholder for the field.
if hasattr(field, "get_placeholder"):

View File

@@ -522,9 +522,9 @@ class Query(BaseExpression):
result = compiler.execute_sql(SINGLE)
if result is None:
result = empty_set_result
converters = compiler.get_converters(outer_query.annotation_select.values())
result = next(compiler.apply_converters((result,), converters))
else:
converters = compiler.get_converters(outer_query.annotation_select.values())
result = next(compiler.apply_converters((result,), converters))
return dict(zip(outer_query.annotation_select, result))