From 6b03179e126d4df01623dccc162c1579f349e41e Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 29 Jan 2013 00:28:09 -0500 Subject: [PATCH] Fixed #19688 -- Allow model subclassing with a custom metaclass using six.with_metaclass --- django/db/models/base.py | 13 +++++++++-- tests/modeltests/base/__init__.py | 0 tests/modeltests/base/models.py | 5 +++++ tests/modeltests/base/tests.py | 36 +++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/modeltests/base/__init__.py create mode 100644 tests/modeltests/base/models.py create mode 100644 tests/modeltests/base/tests.py diff --git a/django/db/models/base.py b/django/db/models/base.py index 5f058654bf0..8eb4048365b 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -58,12 +58,21 @@ class ModelBase(type): """ def __new__(cls, name, bases, attrs): super_new = super(ModelBase, cls).__new__ + # six.with_metaclass() inserts an extra class called 'NewBase' in the - # inheritance tree: Model -> NewBase -> object. Ignore this class. + # inheritance tree: Model -> NewBase -> object. But the initialization + # should be executed only once for a given model class. + + # attrs will never be empty for classes declared in the standard way + # (ie. with the `class` keyword). This is quite robust. + if name == 'NewBase' and attrs == {}: + return super_new(cls, name, bases, attrs) + + # Also ensure initialization is only performed for subclasses of Model + # (excluding Model class itself). parents = [b for b in bases if isinstance(b, ModelBase) and not (b.__name__ == 'NewBase' and b.__mro__ == (b, object))] if not parents: - # If this isn't a subclass of Model, don't do anything special. return super_new(cls, name, bases, attrs) # Create the class. diff --git a/tests/modeltests/base/__init__.py b/tests/modeltests/base/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/modeltests/base/models.py b/tests/modeltests/base/models.py new file mode 100644 index 00000000000..3d39302aba9 --- /dev/null +++ b/tests/modeltests/base/models.py @@ -0,0 +1,5 @@ +from django.db.models.base import ModelBase + + +class CustomBaseModel(ModelBase): + pass diff --git a/tests/modeltests/base/tests.py b/tests/modeltests/base/tests.py new file mode 100644 index 00000000000..6229b7a305d --- /dev/null +++ b/tests/modeltests/base/tests.py @@ -0,0 +1,36 @@ +from __future__ import unicode_literals + +from django.db import models +from django.test.testcases import SimpleTestCase +from django.utils import six +from django.utils.unittest import skipIf + +from .models import CustomBaseModel + + +class CustomBaseTest(SimpleTestCase): + + @skipIf(six.PY3, 'test metaclass definition under Python 2') + def test_py2_custom_base(self): + """ + Make sure models.Model can be subclassed with a valid custom base + using __metaclass__ + """ + try: + class MyModel(models.Model): + __metaclass__ = CustomBaseModel + except Exception: + self.fail("models.Model couldn't be subclassed with a valid " + "custom base using __metaclass__.") + + def test_six_custom_base(self): + """ + Make sure models.Model can be subclassed with a valid custom base + using `six.with_metaclass`. + """ + try: + class MyModel(six.with_metaclass(CustomBaseModel, models.Model)): + pass + except Exception: + self.fail("models.Model couldn't be subclassed with a valid " + "custom base using `six.with_metaclass`.")