Removed SubfieldBase per deprecation timeline.

This commit is contained in:
Tim Graham 2015-08-12 09:26:57 -04:00
parent 4fd264b6f1
commit 08ab262649
6 changed files with 2 additions and 312 deletions

View File

@ -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

View File

@ -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

View File

@ -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`.

View File

@ -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):

View File

@ -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()

View File

@ -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):