Removed SubfieldBase per deprecation timeline.
This commit is contained in:
parent
4fd264b6f1
commit
08ab262649
|
@ -12,7 +12,6 @@ from django.db.models.expressions import ( # NOQA
|
|||
from django.db.models.fields import * # NOQA
|
||||
from django.db.models.fields.files import FileField, ImageField # NOQA
|
||||
from django.db.models.fields.proxy import OrderWrt # NOQA
|
||||
from django.db.models.fields.subclassing import SubfieldBase # NOQA
|
||||
from django.db.models.lookups import Lookup, Transform # NOQA
|
||||
from django.db.models.manager import Manager # NOQA
|
||||
from django.db.models.query import Q, Prefetch, QuerySet # NOQA
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
"""
|
||||
Convenience routines for creating non-trivial Field subclasses, as well as
|
||||
backwards compatibility utilities.
|
||||
|
||||
Add SubfieldBase as the metaclass for your Field subclass, implement
|
||||
to_python() and the other necessary methods and everything will work
|
||||
seamlessly.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from django.utils.deprecation import RemovedInDjango110Warning
|
||||
|
||||
|
||||
class SubfieldBase(type):
|
||||
"""
|
||||
A metaclass for custom Field subclasses. This ensures the model's attribute
|
||||
has the descriptor protocol attached to it.
|
||||
"""
|
||||
def __new__(cls, name, bases, attrs):
|
||||
warnings.warn("SubfieldBase has been deprecated. Use Field.from_db_value instead.",
|
||||
RemovedInDjango110Warning)
|
||||
|
||||
new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs)
|
||||
new_class.contribute_to_class = make_contrib(
|
||||
new_class, attrs.get('contribute_to_class')
|
||||
)
|
||||
return new_class
|
||||
|
||||
|
||||
class Creator(object):
|
||||
"""
|
||||
A placeholder class that provides a way to set the attribute on the model.
|
||||
"""
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
return obj.__dict__[self.field.name]
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.__dict__[self.field.name] = self.field.to_python(value)
|
||||
|
||||
|
||||
def make_contrib(superclass, func=None):
|
||||
"""
|
||||
Returns a suitable contribute_to_class() method for the Field subclass.
|
||||
|
||||
If 'func' is passed in, it is the existing contribute_to_class() method on
|
||||
the subclass and it is called before anything else. It is assumed in this
|
||||
case that the existing contribute_to_class() calls all the necessary
|
||||
superclass methods.
|
||||
"""
|
||||
def contribute_to_class(self, cls, name, **kwargs):
|
||||
if func:
|
||||
func(self, cls, name, **kwargs)
|
||||
else:
|
||||
super(superclass, self).contribute_to_class(cls, name, **kwargs)
|
||||
setattr(cls, self.name, Creator(self))
|
||||
|
||||
return contribute_to_class
|
|
@ -428,13 +428,6 @@ get out of the way.
|
|||
Converting values to Python objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionchanged:: 1.8
|
||||
|
||||
Historically, Django provided a metaclass called ``SubfieldBase`` which
|
||||
always called :meth:`~Field.to_python` on assignment. This did not play
|
||||
nicely with custom database transformations, aggregation, or values
|
||||
queries, so it has been replaced with :meth:`~Field.from_db_value`.
|
||||
|
||||
If your custom :class:`~Field` class deals with data structures that are more
|
||||
complex than strings, dates, integers, or floats, then you may need to override
|
||||
:meth:`~Field.from_db_value` and :meth:`~Field.to_python`.
|
||||
|
|
|
@ -1,94 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import warnings
|
||||
|
||||
from django.db import models
|
||||
from django.utils import six
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.deprecation import RemovedInDjango110Warning
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
|
||||
# Catch warning about subfieldbase -- remove in Django 1.10
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
'SubfieldBase has been deprecated. Use Field.from_db_value instead.',
|
||||
RemovedInDjango110Warning
|
||||
)
|
||||
|
||||
|
||||
@deconstructible
|
||||
@python_2_unicode_compatible
|
||||
class Small(object):
|
||||
"""
|
||||
A simple class to show that non-trivial Python objects can be used as
|
||||
attributes.
|
||||
"""
|
||||
def __init__(self, first, second):
|
||||
self.first, self.second = first, second
|
||||
|
||||
def __str__(self):
|
||||
return '%s%s' % (force_text(self.first), force_text(self.second))
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.first == other.first and self.second == other.second
|
||||
return False
|
||||
|
||||
|
||||
class SmallField(six.with_metaclass(models.SubfieldBase, models.Field)):
|
||||
"""
|
||||
Turns the "Small" class into a Django field. Because of the similarities
|
||||
with normal character fields and the fact that Small.__unicode__ does
|
||||
something sensible, we don't need to implement a lot here.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = 2
|
||||
super(SmallField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, Small):
|
||||
return value
|
||||
return Small(value[0], value[1])
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
return six.text_type(value)
|
||||
|
||||
def get_prep_lookup(self, lookup_type, value):
|
||||
if lookup_type == 'exact':
|
||||
return force_text(value)
|
||||
if lookup_type == 'in':
|
||||
return [force_text(v) for v in value]
|
||||
if lookup_type == 'isnull':
|
||||
return []
|
||||
raise TypeError('Invalid lookup type: %r' % lookup_type)
|
||||
|
||||
|
||||
class SmallerField(SmallField):
|
||||
pass
|
||||
|
||||
|
||||
class JSONField(six.with_metaclass(models.SubfieldBase, models.TextField)):
|
||||
|
||||
description = ("JSONField automatically serializes and deserializes values to "
|
||||
"and from JSON.")
|
||||
|
||||
def to_python(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if isinstance(value, six.string_types):
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
if value is None:
|
||||
return None
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
class CustomTypedField(models.TextField):
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
"""
|
||||
Tests for field subclassing.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
|
||||
from .fields import JSONField, Small, SmallerField, SmallField
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class MyModel(models.Model):
|
||||
name = models.CharField(max_length=10)
|
||||
data = SmallField('small field')
|
||||
|
||||
def __str__(self):
|
||||
return force_text(self.name)
|
||||
|
||||
|
||||
class OtherModel(models.Model):
|
||||
data = SmallerField()
|
||||
|
||||
|
||||
class ChoicesModel(models.Model):
|
||||
SMALL_AB = Small('a', 'b')
|
||||
SMALL_CD = Small('c', 'd')
|
||||
SMALL_CHOICES = (
|
||||
(SMALL_AB, str(SMALL_AB)),
|
||||
(SMALL_CD, str(SMALL_CD)),
|
||||
)
|
||||
data = SmallField('small field', choices=SMALL_CHOICES)
|
||||
|
||||
|
||||
class DataModel(models.Model):
|
||||
data = JSONField()
|
|
@ -1,126 +1,9 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import inspect
|
||||
|
||||
from django.core import exceptions, serializers
|
||||
from django.db import connection
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from .fields import CustomTypedField, Small
|
||||
from .models import ChoicesModel, DataModel, MyModel, OtherModel
|
||||
|
||||
|
||||
class CustomField(TestCase):
|
||||
def test_refresh(self):
|
||||
d = DataModel.objects.create(data=[1, 2, 3])
|
||||
d.refresh_from_db(fields=['data'])
|
||||
self.assertIsInstance(d.data, list)
|
||||
self.assertEqual(d.data, [1, 2, 3])
|
||||
|
||||
def test_defer(self):
|
||||
d = DataModel.objects.create(data=[1, 2, 3])
|
||||
|
||||
self.assertIsInstance(d.data, list)
|
||||
|
||||
d = DataModel.objects.get(pk=d.pk)
|
||||
self.assertIsInstance(d.data, list)
|
||||
self.assertEqual(d.data, [1, 2, 3])
|
||||
|
||||
d = DataModel.objects.defer("data").get(pk=d.pk)
|
||||
self.assertIsInstance(d.data, list)
|
||||
self.assertEqual(d.data, [1, 2, 3])
|
||||
# Refetch for save
|
||||
d = DataModel.objects.defer("data").get(pk=d.pk)
|
||||
d.save()
|
||||
|
||||
d = DataModel.objects.get(pk=d.pk)
|
||||
self.assertIsInstance(d.data, list)
|
||||
self.assertEqual(d.data, [1, 2, 3])
|
||||
|
||||
def test_custom_field(self):
|
||||
# Creating a model with custom fields is done as per normal.
|
||||
s = Small(1, 2)
|
||||
self.assertEqual(str(s), "12")
|
||||
|
||||
m = MyModel.objects.create(name="m", data=s)
|
||||
# Custom fields still have normal field's attributes.
|
||||
self.assertEqual(m._meta.get_field("data").verbose_name, "small field")
|
||||
|
||||
# The m.data attribute has been initialized correctly. It's a Small
|
||||
# object.
|
||||
self.assertEqual((m.data.first, m.data.second), (1, 2))
|
||||
|
||||
# The data loads back from the database correctly and 'data' has the
|
||||
# right type.
|
||||
m1 = MyModel.objects.get(pk=m.pk)
|
||||
self.assertIsInstance(m1.data, Small)
|
||||
self.assertEqual(str(m1.data), "12")
|
||||
|
||||
# We can do normal filtering on the custom field (and will get an error
|
||||
# when we use a lookup type that does not make sense).
|
||||
s1 = Small(1, 3)
|
||||
s2 = Small("a", "b")
|
||||
self.assertQuerysetEqual(
|
||||
MyModel.objects.filter(data__in=[s, s1, s2]), [
|
||||
"m",
|
||||
],
|
||||
lambda m: m.name,
|
||||
)
|
||||
self.assertRaises(TypeError, lambda: MyModel.objects.filter(data__lt=s))
|
||||
|
||||
# Serialization works, too.
|
||||
stream = serializers.serialize("json", MyModel.objects.all())
|
||||
self.assertJSONEqual(stream, [{
|
||||
"pk": m1.pk,
|
||||
"model": "field_subclassing.mymodel",
|
||||
"fields": {"data": "12", "name": "m"}
|
||||
}])
|
||||
|
||||
obj = list(serializers.deserialize("json", stream))[0]
|
||||
self.assertEqual(obj.object, m)
|
||||
|
||||
# Test retrieving custom field data
|
||||
m.delete()
|
||||
|
||||
m1 = MyModel.objects.create(name="1", data=Small(1, 2))
|
||||
MyModel.objects.create(name="2", data=Small(2, 3))
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
MyModel.objects.all(), [
|
||||
"12",
|
||||
"23",
|
||||
],
|
||||
lambda m: str(m.data),
|
||||
ordered=False
|
||||
)
|
||||
|
||||
def test_field_subclassing(self):
|
||||
o = OtherModel.objects.create(data=Small("a", "b"))
|
||||
o = OtherModel.objects.get()
|
||||
self.assertEqual(o.data.first, "a")
|
||||
self.assertEqual(o.data.second, "b")
|
||||
|
||||
def test_subfieldbase_plays_nice_with_module_inspect(self):
|
||||
"""
|
||||
Custom fields should play nice with python standard module inspect.
|
||||
|
||||
http://users.rcn.com/python/download/Descriptor.htm#properties
|
||||
"""
|
||||
# Even when looking for totally different properties, SubfieldBase's
|
||||
# non property like behavior made inspect crash. Refs #12568.
|
||||
data = dict(inspect.getmembers(MyModel))
|
||||
self.assertIn('__module__', data)
|
||||
self.assertEqual(data['__module__'], 'field_subclassing.models')
|
||||
|
||||
def test_validation_of_choices_for_custom_field(self):
|
||||
# a valid choice
|
||||
o = ChoicesModel.objects.create(data=Small('a', 'b'))
|
||||
o.full_clean()
|
||||
|
||||
# an invalid choice
|
||||
o = ChoicesModel.objects.create(data=Small('d', 'e'))
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
o.full_clean()
|
||||
from .fields import CustomTypedField
|
||||
|
||||
|
||||
class TestDbType(SimpleTestCase):
|
||||
|
|
Loading…
Reference in New Issue