diff --git a/.gitignore b/.gitignore index bab3b71..c18fed3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist/ docs/_build/ htmlcov/ venv/ +.venv/ diff --git a/.travis.yml b/.travis.yml index c320375..42a2019 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,3 @@ -# https://travis-ci.org/django-polymorphic/django-polymorphic -dist: xenial -sudo: false language: python services: @@ -11,32 +8,38 @@ addons: matrix: fast_finish: true include: - # Django 1.11: Python 2.7, 3.5, or 3.6 - - { env: TOXENV=py27-django111, python: 2.7 } - - { env: TOXENV=py35-django111, python: 3.5 } - - { env: TOXENV=py36-django111, python: 3.6 } - - { env: TOXENV=py36-django111-postgres DB=postgres, python: 3.6 } - # Django 2.0: Python 3.5, or 3.6 - - { env: TOXENV=py35-django20, python: 3.5 } - - { env: TOXENV=py36-django20, python: 3.6 } - - { env: TOXENV=py36-django20-postgres DB=postgres, python: 3.6 } - # Django 2.1: Python 3.6, or 3.7 + # Django 2.1: Python 3.5, 3.6, or 3.7 + - { env: TOXENV=py35-django21, python: 3.5 } - { env: TOXENV=py36-django21, python: 3.6 } - { env: TOXENV=py37-django21, python: 3.7 } - { env: TOXENV=py37-django21-postgres DB=postgres, python: 3.7 } - # Django 2.2: Python 3.6, or 3.7 + # Django 2.2: Python 3.5, 3.6, 3.7 or 3.8 + - { env: TOXENV=py35-django22, python: 3.5 } - { env: TOXENV=py36-django22, python: 3.6 } - { env: TOXENV=py37-django22, python: 3.7 } - - { env: TOXENV=py37-django22-postgres DB=postgres, python: 3.7 } + - { env: TOXENV=py38-django22, python: 3.8 } + - { env: TOXENV=py38-django22-postgres DB=postgres, python: 3.8 } + # Django 3.0: Python 3.6, 3.7 or 3.8 + - { env: TOXENV=py36-django30, python: 3.6 } + - { env: TOXENV=py37-django30, python: 3.7 } + - { env: TOXENV=py38-django30, python: 3.8 } + - { env: TOXENV=py38-django30-postgres DB=postgres, python: 3.8 } + # Django 3.1: Python 3.6, 3.7 or 3.8 + - { env: TOXENV=py36-django31, python: 3.6 } + - { env: TOXENV=py37-django31, python: 3.7 } + - { env: TOXENV=py38-django31, python: 3.8 } + - { env: TOXENV=py38-django31-postgres DB=postgres, python: 3.8 } # Django development master (direct from GitHub source): - { env: TOXENV=py36-djangomaster, python: 3.6 } - { env: TOXENV=py37-djangomaster, python: 3.7 } - - { env: TOXENV=py37-djangomaster-postgres DB=postgres, python: 3.7 } + - { env: TOXENV=py38-djangomaster, python: 3.8 } + - { env: TOXENV=py38-djangomaster-postgres DB=postgres, python: 3.8 } allow_failures: - env: TOXENV=py36-djangomaster - env: TOXENV=py37-djangomaster - - env: TOXENV=py37-djangomaster-postgres DB=postgres + - env: TOXENV=py38-djangomaster + - env: TOXENV=py38-djangomaster-postgres DB=postgres cache: directories: @@ -57,7 +60,3 @@ script: after_success: - coverage xml -i - codecov - -branches: - only: - - master diff --git a/AUTHORS.rst b/AUTHORS.rst index 81590be..0d7efd1 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,12 +13,14 @@ Contributors * Abel Daniel * Adam Chainz * Adam Wentz +* Adam Donaghy * Andrew Ingram (contributed setup.py) * Al Johri * Alex Alvarez * Andrew Dodd * Angel Velasquez * Austin Matsick +* Bastien Vallet * Ben Konrath * Bert Constantin * Bertrand Bordage diff --git a/README.rst b/README.rst index 76a95b9..15d49be 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,8 @@ Django to perform an ``INNER JOIN`` to fetch the model fields from the database. While taking this in mind, there are valid reasons for using subclassed models. That's what this library is designed for! -The current release of *django-polymorphic* supports Django 1.11, 2.0, 2.1, 2.2 and Python 2.7 and 3.5+ is supported. +The current release of *django-polymorphic* supports Django 2.1, 2.2, 3.0, 3.1 +and Python 3.5+ is supported. For older Django versions, install *django-polymorphic==1.3*. For more information, see the `documentation at Read the Docs `_. diff --git a/docs/_ext/djangodummy/requirements.txt b/docs/_ext/djangodummy/requirements.txt index 62f1e49..de7dbd2 100644 --- a/docs/_ext/djangodummy/requirements.txt +++ b/docs/_ext/djangodummy/requirements.txt @@ -1,5 +1,5 @@ # for readthedocs # Remaining requirements are picked up from setup.py -Django == 2.2.3 +Django == 2.2.13 django-extra-views == 0.12.0 sphinxcontrib-django == 0.4 diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/changelog.rst b/docs/changelog.rst index 426c0d4..f086aad 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,14 @@ Changelog ========= -Changes in 2.1.2 (2019-17-15) +Changes in 3.0.0 (2020-08-21) +----------------------------- + +* Support for Django 3.X +* Dropped support for python 2.X +* A lot of various fixes and improvements by various authors. Thanks a lot! + +Changes in 2.1.2 (2019-07-15) ----------------------------- * Fix ``PolymorphicInlineModelAdmin`` media jQuery include for Django 2.0+ diff --git a/docs/migrating.rst b/docs/migrating.rst index 866da3b..2db2355 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -29,7 +29,6 @@ can be included in a single Django migration. For example: .. code-block:: python # -*- coding: utf-8 -*- - from __future__ import unicode_literals from django.db import migrations, models diff --git a/docs/quickstart.rst b/docs/quickstart.rst index c40cb06..c659fbd 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -12,7 +12,7 @@ Update the settings file:: 'django.contrib.contenttypes', ) -The current release of *django-polymorphic* supports Django 1.11, 2.0 and Python 2.7 and 3.5+ is supported. +The current release of *django-polymorphic* supports Django 1.11, 2.0, 2.1, 2.2 and Python 2.7 and 3.5+ is supported. For older Django versions, use *django-polymorphic==1.3*. Making Your Models Polymorphic diff --git a/example/orders/models.py b/example/orders/models.py index cf12286..97ec22e 100644 --- a/example/orders/models.py +++ b/example/orders/models.py @@ -1,12 +1,10 @@ from django.db import models from django.utils.dates import MONTHS_3 -from django.utils.six import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from polymorphic.models import PolymorphicModel -@python_2_unicode_compatible class Order(models.Model): """ An example order that has polymorphic relations @@ -23,7 +21,6 @@ class Order(models.Model): return self.title -@python_2_unicode_compatible class Payment(PolymorphicModel): """ A generic payment model. diff --git a/polymorphic/admin/childadmin.py b/polymorphic/admin/childadmin.py index db30c9f..98d7d39 100644 --- a/polymorphic/admin/childadmin.py +++ b/polymorphic/admin/childadmin.py @@ -5,7 +5,7 @@ import inspect from django.contrib import admin from django.urls import resolve -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from polymorphic.utils import get_base_polymorphic_model diff --git a/polymorphic/admin/filters.py b/polymorphic/admin/filters.py index 171599b..37efc7f 100644 --- a/polymorphic/admin/filters.py +++ b/polymorphic/admin/filters.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.core.exceptions import PermissionDenied -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class PolymorphicChildModelFilter(admin.SimpleListFilter): diff --git a/polymorphic/admin/forms.py b/polymorphic/admin/forms.py index 08e950f..8a705f0 100644 --- a/polymorphic/admin/forms.py +++ b/polymorphic/admin/forms.py @@ -1,6 +1,6 @@ from django import forms from django.contrib.admin.widgets import AdminRadioSelect -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class PolymorphicModelChoiceForm(forms.Form): diff --git a/polymorphic/admin/helpers.py b/polymorphic/admin/helpers.py index 122f467..130ef31 100644 --- a/polymorphic/admin/helpers.py +++ b/polymorphic/admin/helpers.py @@ -8,7 +8,7 @@ import json from django.contrib.admin.helpers import AdminField, InlineAdminForm, InlineAdminFormSet from django.utils.encoding import force_text from django.utils.text import capfirst -from django.utils.translation import ugettext +from django.utils.translation import gettext from polymorphic.formsets import BasePolymorphicModelFormSet @@ -96,7 +96,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet): "name": "#%s" % self.formset.prefix, "options": { "prefix": self.formset.prefix, - "addText": ugettext("Add another %(verbose_name)s") + "addText": gettext("Add another %(verbose_name)s") % {"verbose_name": capfirst(verbose_name)}, "childTypes": [ { @@ -105,7 +105,7 @@ class PolymorphicInlineAdminFormSet(InlineAdminFormSet): } for model in self.formset.child_forms.keys() ], - "deleteText": ugettext("Remove"), + "deleteText": gettext("Remove"), }, } ) diff --git a/polymorphic/admin/parentadmin.py b/polymorphic/admin/parentadmin.py index c9f7598..f56bde8 100644 --- a/polymorphic/admin/parentadmin.py +++ b/polymorphic/admin/parentadmin.py @@ -1,8 +1,6 @@ """ The parent admin displays the list view of the base model. """ -import sys - from django.contrib import admin from django.contrib.admin.helpers import AdminErrorList, AdminForm from django.contrib.admin.templatetags.admin_urls import add_preserved_filters @@ -11,26 +9,16 @@ from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.db import models from django.http import Http404, HttpResponseRedirect from django.template.response import TemplateResponse +from django.urls import URLResolver from django.utils.encoding import force_text from django.utils.http import urlencode from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from polymorphic.utils import get_base_polymorphic_model from .forms import PolymorphicModelChoiceForm -try: - # Django 2.0+ - from django.urls import URLResolver -except ImportError: - # Django < 2.0 - from django.urls import RegexURLResolver as URLResolver - - -if sys.version_info[0] >= 3: - long = int - class RegistrationClosed(RuntimeError): "The admin model can't be registered anymore at this point." @@ -146,13 +134,14 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): """ self._lazy_setup() choices = [] - for model in self.get_child_models(): + content_types = ContentType.objects.get_for_models(*self.get_child_models(), for_concrete_models=False) + + for model, ct in content_types.items(): perm_function_name = "has_{0}_permission".format(action) model_admin = self._get_real_admin_by_model(model) perm_function = getattr(model_admin, perm_function_name) if not perm_function(request): continue - ct = ContentType.objects.get_for_model(model, for_concrete_model=False) choices.append((ct.id, model._meta.verbose_name)) return choices @@ -293,9 +282,9 @@ class PolymorphicParentModelAdmin(admin.ModelAdmin): try: pos = path.find("/") if pos == -1: - object_id = long(path) + object_id = int(path) else: - object_id = long(path[0:pos]) + object_id = int(path[0:pos]) except ValueError: raise Http404( "No ct_id parameter, unable to find admin subclass for path '{0}'.".format( diff --git a/polymorphic/base.py b/polymorphic/base.py index 81d988a..1bf6059 100644 --- a/polymorphic/base.py +++ b/polymorphic/base.py @@ -2,8 +2,6 @@ """ PolymorphicModel Meta Class """ -from __future__ import absolute_import - import inspect import os import sys diff --git a/polymorphic/compat.py b/polymorphic/compat.py index e52a3a9..1589eb2 100644 --- a/polymorphic/compat.py +++ b/polymorphic/compat.py @@ -1,22 +1,4 @@ """Compatibility with Python 2 (taken from 'django.utils.six')""" -import sys - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - - -if PY3: - string_types = (str,) - integer_types = (int,) - class_types = (type,) - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = (basestring,) - integer_types = (int, long) - def with_metaclass(meta, *bases): class metaclass(type): @@ -24,22 +6,3 @@ def with_metaclass(meta, *bases): return meta(name, bases, d) return type.__new__(metaclass, "temporary_class", (), {}) - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if "__str__" not in klass.__dict__: - raise ValueError( - "@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % klass.__name__ - ) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode("utf-8") - return klass diff --git a/polymorphic/contrib/extra_views.py b/polymorphic/contrib/extra_views.py index f3e050d..9822c47 100644 --- a/polymorphic/contrib/extra_views.py +++ b/polymorphic/contrib/extra_views.py @@ -4,8 +4,6 @@ The ``extra_views.advanced`` provides a method to combine that with a create/upd This package provides classes that support both options for polymorphic formsets. """ -from __future__ import absolute_import - import extra_views from django.core.exceptions import ImproperlyConfigured diff --git a/polymorphic/managers.py b/polymorphic/managers.py index a43840b..5e894d4 100644 --- a/polymorphic/managers.py +++ b/polymorphic/managers.py @@ -2,17 +2,14 @@ """ The manager class for use in the models. """ -from __future__ import unicode_literals from django.db import models -from polymorphic.compat import python_2_unicode_compatible from polymorphic.query import PolymorphicQuerySet __all__ = ("PolymorphicManager", "PolymorphicQuerySet") -@python_2_unicode_compatible class PolymorphicManager(models.Manager): """ Manager for PolymorphicModel diff --git a/polymorphic/models.py b/polymorphic/models.py index 63731e8..5c34ff7 100644 --- a/polymorphic/models.py +++ b/polymorphic/models.py @@ -2,8 +2,6 @@ """ Seamless Polymorphic Inheritance for Django Models """ -from __future__ import absolute_import - from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models.fields.related import ( diff --git a/polymorphic/query.py b/polymorphic/query.py index 4dc4d30..8b0259b 100644 --- a/polymorphic/query.py +++ b/polymorphic/query.py @@ -2,16 +2,14 @@ """ QuerySet for PolymorphicModel """ -from __future__ import absolute_import - import copy from collections import defaultdict +from django import get_version as get_django_version from django.contrib.contenttypes.models import ContentType -from django.db.models import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist from django.db.models.query import ModelIterable, Q, QuerySet -from . import compat from .query_translate import ( translate_polymorphic_field_path, translate_polymorphic_filter_definitions_in_args, @@ -160,24 +158,40 @@ class PolymorphicQuerySet(QuerySet): # Implementation in _translate_polymorphic_filter_defnition.""" return self.filter(not_instance_of=args) - def _filter_or_exclude(self, negate, *args, **kwargs): - # We override this internal Django functon as it is used for all filter member functions. - q_objects = translate_polymorphic_filter_definitions_in_args( - self.model, args, using=self.db - ) - # filter_field='data' - additional_args = translate_polymorphic_filter_definitions_in_kwargs( - self.model, kwargs, using=self.db - ) - return super(PolymorphicQuerySet, self)._filter_or_exclude( - negate, *(list(q_objects) + additional_args), **kwargs - ) + # Makes _filter_or_exclude compatible with the change in signature introduced in django at 9c9a3fe + if get_django_version() >= "3.2": + def _filter_or_exclude(self, negate, args, kwargs): + # We override this internal Django function as it is used for all filter member functions. + q_objects = translate_polymorphic_filter_definitions_in_args( + queryset_model=self.model, args=args, using=self.db + ) + # filter_field='data' + additional_args = translate_polymorphic_filter_definitions_in_kwargs( + queryset_model=self.model, kwargs=kwargs, using=self.db + ) + args = list(q_objects) + additional_args + return super(PolymorphicQuerySet, self)._filter_or_exclude( + negate=negate, args=args, kwargs=kwargs + ) + else: + def _filter_or_exclude(self, negate, *args, **kwargs): + # We override this internal Django function as it is used for all filter member functions. + q_objects = translate_polymorphic_filter_definitions_in_args( + self.model, args, using=self.db + ) + # filter_field='data' + additional_args = translate_polymorphic_filter_definitions_in_kwargs( + self.model, kwargs, using=self.db + ) + return super(PolymorphicQuerySet, self)._filter_or_exclude( + negate, *(list(q_objects) + additional_args), **kwargs + ) def order_by(self, *field_names): """translate the field paths in the args, then call vanilla order_by.""" field_names = [ translate_polymorphic_field_path(self.model, a) - if isinstance(a, compat.string_types) + if isinstance(a, str) else a # allow expressions to pass unchanged for a in field_names ] @@ -524,4 +538,4 @@ class PolymorphicQuerySet(QuerySet): if not self.model.polymorphic_query_multiline_output: return olist clist = PolymorphicQuerySet._p_list_class(olist) - return clist + return clist \ No newline at end of file diff --git a/polymorphic/query_translate.py b/polymorphic/query_translate.py index cbecf10..45328a9 100644 --- a/polymorphic/query_translate.py +++ b/polymorphic/query_translate.py @@ -2,14 +2,12 @@ """ PolymorphicQuerySet support functions """ -from __future__ import absolute_import - import copy from collections import deque from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import FieldError +from django.core.exceptions import FieldError, FieldDoesNotExist from django.db import models from django.db.models import Q from django.db.models.fields.related import ForeignObjectRel, RelatedField @@ -144,7 +142,7 @@ def translate_polymorphic_field_path(queryset_model, field_path): into modela__modelb__modelc__field3. Returns: translated path (unchanged, if no translation needed) """ - if not isinstance(field_path, compat.string_types): + if not isinstance(field_path, str): raise ValueError("Expected field name as string: {0}".format(field_path)) classname, sep, pure_field_path = field_path.partition("___") @@ -189,7 +187,7 @@ def translate_polymorphic_field_path(queryset_model, field_path): # Can also test whether the field exists in the related object to avoid ambiguity between # class names and field names, but that never happens when your class names are in CamelCase. return field_path # No exception raised, field does exist. - except models.FieldDoesNotExist: + except FieldDoesNotExist: pass submodels = _get_all_sub_models(queryset_model) diff --git a/polymorphic/showfields.py b/polymorphic/showfields.py index ffa14f5..8da8330 100644 --- a/polymorphic/showfields.py +++ b/polymorphic/showfields.py @@ -3,13 +3,9 @@ import re from django.db import models -from . import compat -from .compat import python_2_unicode_compatible - RE_DEFERRED = re.compile("_Deferred_.*") -@python_2_unicode_compatible class ShowFieldBase(object): """ base class for the ShowField... model mixins, does the work """ @@ -42,7 +38,7 @@ class ShowFieldBase(object): out += content.__class__.__name__ elif issubclass(field_type, models.ManyToManyField): out += "%d" % content.count() - elif isinstance(content, compat.integer_types): + elif isinstance(content, int): out += str(content) elif content is None: out += "None" diff --git a/polymorphic/templatetags/polymorphic_admin_tags.py b/polymorphic/templatetags/polymorphic_admin_tags.py index 9914b84..aa68843 100644 --- a/polymorphic/templatetags/polymorphic_admin_tags.py +++ b/polymorphic/templatetags/polymorphic_admin_tags.py @@ -1,7 +1,5 @@ from django.template import Library, Node, TemplateSyntaxError -from polymorphic import compat - register = Library() @@ -31,7 +29,7 @@ class BreadcrumbScope(Node): # Instead, have an assignment tag that inserts that in the template. base_opts = self.base_opts.resolve(context) new_vars = {} - if base_opts and not isinstance(base_opts, compat.string_types): + if base_opts and not isinstance(base_opts, str): new_vars = { "app_label": base_opts.app_label, # What this is all about "opts": base_opts, diff --git a/polymorphic/templatetags/polymorphic_formset_tags.py b/polymorphic/templatetags/polymorphic_formset_tags.py index 6a78eca..0b2895f 100644 --- a/polymorphic/templatetags/polymorphic_formset_tags.py +++ b/polymorphic/templatetags/polymorphic_formset_tags.py @@ -3,7 +3,7 @@ import json from django.template import Library from django.utils.encoding import force_text from django.utils.text import capfirst -from django.utils.translation import ugettext +from django.utils.translation import gettext from polymorphic.formsets import BasePolymorphicModelFormSet @@ -44,10 +44,10 @@ def as_script_options(formset): "prefix": formset.prefix, "pkFieldName": formset.model._meta.pk.name, "addText": getattr(formset, "add_text", None) - or ugettext("Add another %(verbose_name)s") + or gettext("Add another %(verbose_name)s") % {"verbose_name": capfirst(verbose_name)}, "showAddButton": getattr(formset, "show_add_button", True), - "deleteText": ugettext("Delete"), + "deleteText": gettext("Delete"), } if isinstance(formset, BasePolymorphicModelFormSet): diff --git a/polymorphic/tests/migrations/0001_initial.py b/polymorphic/tests/migrations/0001_initial.py index 4707a36..38106a3 100644 --- a/polymorphic/tests/migrations/0001_initial.py +++ b/polymorphic/tests/migrations/0001_initial.py @@ -1450,6 +1450,13 @@ class Migration(migrations.Migration): to="tests.ParentModelWithManager", ), ), + migrations.AddField( + model_name="childmodelwithmanager", + name="field1", + field=models.CharField( + max_length=10, + ), + ), migrations.AddField( model_name="childmodelwithmanager", name="polymorphic_ctype", diff --git a/polymorphic/tests/models.py b/polymorphic/tests/models.py index 96e361f..494b98f 100644 --- a/polymorphic/tests/models.py +++ b/polymorphic/tests/models.py @@ -222,6 +222,7 @@ class ParentModelWithManager(PolymorphicModel): class ChildModelWithManager(PolymorphicModel): # Also test whether foreign keys receive the manager: + field1 = models.CharField(max_length=10) # needed as MyManager uses it fk = models.ForeignKey( ParentModelWithManager, on_delete=models.CASCADE, related_name="childmodel_set" ) diff --git a/polymorphic/tests/test_multidb.py b/polymorphic/tests/test_multidb.py index eec9e5a..dee08d4 100644 --- a/polymorphic/tests/test_multidb.py +++ b/polymorphic/tests/test_multidb.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.test import TestCase @@ -20,7 +18,7 @@ from polymorphic.tests.models import ( class MultipleDatabasesTests(TestCase): - multi_db = True + databases = ["default", "secondary"] def test_save_to_non_default_database(self): Model2A.objects.db_manager("secondary").create(field1="A1") diff --git a/polymorphic/tests/test_orm.py b/polymorphic/tests/test_orm.py index 62231f7..8a55c8e 100644 --- a/polymorphic/tests/test_orm.py +++ b/polymorphic/tests/test_orm.py @@ -238,22 +238,12 @@ class PolymorphicTests(TransactionTestCase): objects_deferred[0].__dict__, "field1 was not deferred (using defer())", ) - self.assertRegex( - repr(objects_deferred[0]), - r"", - ) - self.assertRegex( - repr(objects_deferred[1]), - r"", - ) - self.assertRegex( - repr(objects_deferred[2]), - r"", - ) - self.assertRegex( - repr(objects_deferred[3]), - r"", - ) + + # Check that we have exactly one deferred field ('field1') per resulting object. + for obj in objects_deferred: + deferred_fields = obj.get_deferred_fields() + self.assertEqual(1, len(deferred_fields)) + self.assertIn("field1", deferred_fields) objects_only = Model2A.objects.only("pk", "polymorphic_ctype", "field1") @@ -271,48 +261,37 @@ class PolymorphicTests(TransactionTestCase): self.assertNotIn( "field4", objects_only[3].__dict__, "field4 was not deferred (using only())" ) - self.assertRegex( - repr(objects_only[0]), r"" - ) - self.assertRegex( - repr(objects_only[1]), - r"", - ) - self.assertRegex( - repr(objects_only[2]), - r"", - ) - self.assertRegex( - repr(objects_only[3]), - r"", - ) + self.assertNotIn("field1", objects_only[0].get_deferred_fields()) + + self.assertIn("field2", objects_only[1].get_deferred_fields()) + + # objects_only[2] has several deferred fields, ensure they are all set as such. + model2c_deferred = objects_only[2].get_deferred_fields() + self.assertIn("field2", model2c_deferred) + self.assertIn("field3", model2c_deferred) + self.assertIn("model2a_ptr_id", model2c_deferred) + + # objects_only[3] has a few more fields that should be set as deferred. + model2d_deferred = objects_only[3].get_deferred_fields() + self.assertIn("field2", model2d_deferred) + self.assertIn("field3", model2d_deferred) + self.assertIn("field4", model2d_deferred) + self.assertIn("model2a_ptr_id", model2d_deferred) + self.assertIn("model2b_ptr_id", model2d_deferred) ModelX.objects.create(field_b="A1", field_x="A2") ModelY.objects.create(field_b="B1", field_y="B2") + # If we defer a field on a descendent, the parent's field is not deferred. objects_deferred = Base.objects.defer("ModelY___field_y") - self.assertRegex( - repr(objects_deferred[0]), - r"", - ) - self.assertRegex( - repr(objects_deferred[1]), - r"", - ) + self.assertNotIn("field_y", objects_deferred[0].get_deferred_fields()) + self.assertIn("field_y", objects_deferred[1].get_deferred_fields()) objects_only = Base.objects.only( "polymorphic_ctype", "ModelY___field_y", "ModelX___field_x" ) - self.assertRegex( - repr(objects_only[0]), - r"", - ) - self.assertRegex( - repr(objects_only[1]), - r"", - ) + self.assertIn("field_b", objects_only[0].get_deferred_fields()) + self.assertIn("field_b", objects_only[1].get_deferred_fields()) def test_defer_related_fields(self): self.create_model2abcd() @@ -481,21 +460,22 @@ class PolymorphicTests(TransactionTestCase): def test_foreignkey_field(self): self.create_model2abcd() - object2a = Model2A.base_objects.get(field1="C1") + object2a = Model2A.objects.get(field1="C1") self.assertEqual(object2a.model2b.__class__, Model2B) - object2b = Model2B.base_objects.get(field1="C1") + object2b = Model2B.objects.get(field1="C1") self.assertEqual(object2b.model2c.__class__, Model2C) def test_onetoone_field(self): self.create_model2abcd() + # FIXME: We should not use base_objects here. a = Model2A.base_objects.get(field1="C1") b = One2OneRelatingModelDerived.objects.create( one2one=a, field1="f1", field2="f2" ) - # this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem + # FIXME: this result is basically wrong, probably due to Django cacheing (we used base_objects), but should not be a problem self.assertEqual(b.one2one.__class__, Model2A) self.assertEqual(b.one2one_id, b.one2one.id) @@ -581,32 +561,18 @@ class PolymorphicTests(TransactionTestCase): select={"topic": "tests_modelextraexternal.topic"}, where=["tests_modelextraa.id = tests_modelextraexternal.id"], ) - if compat.PY3: - self.assertEqual( - repr(objects[0]), - '', - ) - self.assertEqual( - repr(objects[1]), - '', - ) - self.assertEqual( - repr(objects[2]), - '', - ) - else: - self.assertEqual( - repr(objects[0]), - '', - ) - self.assertEqual( - repr(objects[1]), - '', - ) - self.assertEqual( - repr(objects[2]), - '', - ) + self.assertEqual( + repr(objects[0]), + '', + ) + self.assertEqual( + repr(objects[1]), + '', + ) + self.assertEqual( + repr(objects[2]), + '', + ) self.assertEqual(len(objects), 3) def test_instance_of_filter(self): @@ -822,7 +788,6 @@ class PolymorphicTests(TransactionTestCase): self.assertIs(type(ModelWithMyManager.objects), MyManager) self.assertIs(type(ModelWithMyManager._default_manager), MyManager) - self.assertIs(type(ModelWithMyManager.base_objects), models.Manager) def test_user_defined_manager_as_secondary(self): self.create_model2abcd() @@ -845,7 +810,6 @@ class PolymorphicTests(TransactionTestCase): self.assertIs( type(ModelWithMyManagerNoDefault._default_manager), PolymorphicManager ) - self.assertIs(type(ModelWithMyManagerNoDefault.base_objects), models.Manager) def test_user_objects_manager_as_secondary(self): self.create_model2abcd() @@ -855,7 +819,6 @@ class PolymorphicTests(TransactionTestCase): self.assertIs(type(ModelWithMyManagerDefault.my_objects), MyManager) self.assertIs(type(ModelWithMyManagerDefault.objects), PolymorphicManager) self.assertIs(type(ModelWithMyManagerDefault._default_manager), MyManager) - self.assertIs(type(ModelWithMyManagerDefault.base_objects), models.Manager) def test_user_defined_queryset_as_manager(self): self.create_model2abcd() @@ -878,7 +841,6 @@ class PolymorphicTests(TransactionTestCase): type(ModelWithMyManager2._default_manager).__name__, "PolymorphicManagerFromMyManagerQuerySet", ) - self.assertIs(type(ModelWithMyManager2.base_objects), models.Manager) def test_manager_inheritance(self): # by choice of MRO, should be MyManager from MROBase1. diff --git a/polymorphic/utils.py b/polymorphic/utils.py index 14c9a9f..8c00758 100644 --- a/polymorphic/utils.py +++ b/polymorphic/utils.py @@ -1,5 +1,3 @@ -import sys - from django.contrib.contenttypes.models import ContentType from django.db import DEFAULT_DB_ALIAS @@ -59,12 +57,9 @@ def sort_by_subclass(*classes): """ Sort a series of models by their inheritance order. """ - if sys.version_info[0] == 2: - return sorted(classes, cmp=_compare_mro) - else: - from functools import cmp_to_key + from functools import cmp_to_key - return sorted(classes, key=cmp_to_key(_compare_mro)) + return sorted(classes, key=cmp_to_key(_compare_mro)) def get_base_polymorphic_model(ChildModel, allow_abstract=False): diff --git a/runtests.py b/runtests.py index 75a7253..ac8d9f1 100755 --- a/runtests.py +++ b/runtests.py @@ -74,6 +74,7 @@ if not settings.configured: ], POLYMORPHIC_TEST_SWAPPABLE="polymorphic.swappedmodel", ROOT_URLCONF=None, + SECRET_KEY="supersecret" ) diff --git a/setup.cfg b/setup.cfg index a805f90..a576a6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] name = django-polymorphic -version = 2.1.2 +version = 3.0.0 description = Seamless polymorphic inheritance for Django models long_description = file:README.rst author = Bert Constantin author_email = bert.constantin@gmx.de maintainer = Christopher Glass -maintainer_email = tribaal@gmail.com +maintainer_email = tribaal@ubuntu.com url = https://github.com/django-polymorphic/django-polymorphic download_url = https://github.com/django-polymorphic/django-polymorphic/tarball/master keywords = django, polymorphic @@ -14,25 +14,26 @@ classifiers = Development Status :: 5 - Production/Stable Environment :: Web Environment Framework :: Django - Framework :: Django :: 1.11 - Framework :: Django :: 2.0 Framework :: Django :: 2.1 Framework :: Django :: 2.2 + Framework :: Django :: 3.0 + Framework :: Django :: 3.1 Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Topic :: Database [options] packages = find: include_package_data = True install_requires = - Django >= 1.11 + Django >= 2.1 [options.packages.find] exclude = diff --git a/tox.ini b/tox.ini index 8ccc8fe..2e4d2c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = - py27-django{111} - py35-django{111,20} - py36-django{111,20,21,22,master} - py37-django{21,22,master} + py35-django{21,22} + py36-django{21,22,30,31} + py37-django{21,22,30,31} + py38-django{21,22,30,31} docs [testenv] @@ -12,12 +12,13 @@ setenv = postgres: DEFAULT_DATABASE = postgres:///default postgres: SECONDARY_DATABASE = postgres:///secondary deps = + ipdb coverage dj-database-url - django111: Django >= 1.11, < 2.0 - django20: Django ~= 2.0 django21: Django ~= 2.1 django22: Django ~= 2.2 + django30: Django ~= 3.0 + django31: Django ~= 3.1 djangomaster: https://github.com/django/django/archive/master.tar.gz postgres: psycopg2 commands =