From dba3c5ae5d0bc9b74d35704526ea45871eb97644 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sun, 30 Mar 2025 22:11:36 -0400 Subject: [PATCH 1/2] Black'en code base. --- polymodels/__init__.py | 4 +- polymodels/fields.py | 92 ++++++++------ polymodels/forms.py | 2 +- polymodels/managers.py | 27 +++-- polymodels/models.py | 88 +++++++++----- polymodels/utils.py | 4 +- setup.cfg | 2 +- tests/base.py | 9 +- tests/forms.py | 6 +- tests/migrations/0001_initial.py | 122 ++++++++++--------- tests/models.py | 22 ++-- tests/settings.py | 14 +-- tests/test_fields.py | 184 +++++++++++++++++----------- tests/test_managers.py | 199 +++++++++++++++++-------------- tests/test_models.py | 170 ++++++++++++++++---------- tests/test_related.py | 4 +- 16 files changed, 555 insertions(+), 394 deletions(-) diff --git a/polymodels/__init__.py b/polymodels/__init__.py index e1321fd..a34ad0c 100644 --- a/polymodels/__init__.py +++ b/polymodels/__init__.py @@ -1,3 +1,3 @@ -VERSION = (1, 8, 0, 'final', 0) +VERSION = (1, 8, 0, "final", 0) -__version__ = '1.8.0' +__version__ = "1.8.0" diff --git a/polymodels/fields.py b/polymodels/fields.py index 43b85c9..577e4e0 100644 --- a/polymodels/fields.py +++ b/polymodels/fields.py @@ -4,7 +4,8 @@ from django.db.models import ForeignKey, Q from django.db.models.fields import NOT_PROVIDED from django.db.models.fields.related import ( - RelatedField, lazy_related_operation, + RelatedField, + lazy_related_operation, ) from django.utils.deconstruct import deconstructible from django.utils.functional import LazyObject, empty @@ -21,7 +22,7 @@ def __init__(self, field, limit_choices_to): @property def value(self): - subclasses_lookup = self.field.polymorphic_type.subclasses_lookup('pk') + subclasses_lookup = self.field.polymorphic_type.subclasses_lookup("pk") limit_choices_to = self.limit_choices_to if limit_choices_to is None: limit_choices_to = subclasses_lookup.copy() @@ -29,7 +30,7 @@ def value(self): limit_choices_to = dict(limit_choices_to, **subclasses_lookup) elif isinstance(limit_choices_to, Q): limit_choices_to = limit_choices_to & Q(**subclasses_lookup) - self.__dict__['value'] = limit_choices_to + self.__dict__["value"] = limit_choices_to return limit_choices_to def __call__(self): @@ -42,8 +43,8 @@ def __init__(self, remote_field, db): self.__dict__.update(remote_field=remote_field, db=db) def _setup(self): - remote_field = self.__dict__.get('remote_field') - db = self.__dict__.get('db') + remote_field = self.__dict__.get("remote_field") + db = self.__dict__.get("db") self._wrapped = remote_field.model._default_manager.using(db).complex_filter( remote_field.limit_choices_to() ) @@ -53,7 +54,7 @@ def __getattr__(self, attr): # Django 2.1+ in order to clear possible cached results. # Since no results might have been cached before _setup() is called # it's safe to keep deferring until something else is accessed. - if attr == 'all' and self._wrapped is empty: + if attr == "all" and self._wrapped is empty: return lambda: self return super().__getattr__(attr) @@ -79,14 +80,12 @@ def __repr__(self): class PolymorphicTypeField(ForeignKey): default_error_messages = { - 'invalid': _('Specified model is not a subclass of %(model)s.') + "invalid": _("Specified model is not a subclass of %(model)s.") } - description = _( - 'Content type of a subclass of %(type)s' - ) + description = _("Content type of a subclass of %(type)s") default_kwargs = { - 'to': 'contenttypes.contenttype', - 'related_name': '+', + "to": "contenttypes.contenttype", + "related_name": "+", } def __init__(self, polymorphic_type, *args, **kwargs): @@ -94,17 +93,22 @@ def __init__(self, polymorphic_type, *args, **kwargs): self.overriden_default = False for kwarg, value in self.default_kwargs.items(): kwargs.setdefault(kwarg, value) - kwargs['limit_choices_to'] = LimitChoicesToSubclasses(self, kwargs.pop('limit_choices_to', None)) + kwargs["limit_choices_to"] = LimitChoicesToSubclasses( + self, kwargs.pop("limit_choices_to", None) + ) super().__init__(*args, **kwargs) def contribute_to_class(self, cls, name): super().contribute_to_class(cls, name) polymorphic_type = self.polymorphic_type - if (isinstance(polymorphic_type, str) or - polymorphic_type._meta.pk is None): + if isinstance(polymorphic_type, str) or polymorphic_type._meta.pk is None: + def resolve_polymorphic_type(model, related_model, field): field.do_polymorphic_type(related_model) - lazy_related_operation(resolve_polymorphic_type, cls, polymorphic_type, field=self) + + lazy_related_operation( + resolve_polymorphic_type, cls, polymorphic_type, field=self + ) else: self.do_polymorphic_type(polymorphic_type) @@ -115,49 +119,61 @@ def do_polymorphic_type(self, polymorphic_type): self.overriden_default = True self.polymorphic_type = polymorphic_type self.type = polymorphic_type.__name__ - self.error_messages['invalid'] = ( - 'Specified content type is not of a subclass of %s.' % polymorphic_type._meta.object_name + self.error_messages["invalid"] = ( + "Specified content type is not of a subclass of %s." + % polymorphic_type._meta.object_name ) def check(self, **kwargs): errors = super().check(**kwargs) if isinstance(self.polymorphic_type, str): - errors.append(checks.Error( - ("Field defines a relation with model '%s', which " - "is either not installed, or is abstract.") % self.polymorphic_type, - id='fields.E300', - )) + errors.append( + checks.Error( + ( + "Field defines a relation with model '%s', which " + "is either not installed, or is abstract." + ) + % self.polymorphic_type, + id="fields.E300", + ) + ) elif not issubclass(self.polymorphic_type, BasePolymorphicModel): - errors.append(checks.Error( - "The %s type is not a subclass of BasePolymorphicModel." % self.polymorphic_type.__name__, - id='polymodels.E004', - )) + errors.append( + checks.Error( + "The %s type is not a subclass of BasePolymorphicModel." + % self.polymorphic_type.__name__, + id="polymodels.E004", + ) + ) return errors def formfield(self, **kwargs): - db = kwargs.pop('using', None) + db = kwargs.pop("using", None) if isinstance(self.polymorphic_type, str): raise ValueError( - "Cannot create form field for %r yet, because its related model %r has not been loaded yet" % ( - self.name, self.polymorphic_type - ) + "Cannot create form field for %r yet, because its related model %r has not been loaded yet" + % (self.name, self.polymorphic_type) ) defaults = { - 'form_class': forms.ModelChoiceField, - 'queryset': LazyPolymorphicTypeQueryset(self.remote_field, db), - 'to_field_name': self.remote_field.field_name, + "form_class": forms.ModelChoiceField, + "queryset": LazyPolymorphicTypeQueryset(self.remote_field, db), + "to_field_name": self.remote_field.field_name, } defaults.update(kwargs) return super(RelatedField, self).formfield(**defaults) def deconstruct(self): name, path, args, kwargs = super().deconstruct() - opts = getattr(self.polymorphic_type, '_meta', None) - kwargs['polymorphic_type'] = "%s.%s" % (opts.app_label, opts.object_name) if opts else self.polymorphic_type + opts = getattr(self.polymorphic_type, "_meta", None) + kwargs["polymorphic_type"] = ( + "%s.%s" % (opts.app_label, opts.object_name) + if opts + else self.polymorphic_type + ) for kwarg, value in list(kwargs.items()): if self.default_kwargs.get(kwarg) == value: kwargs.pop(kwarg) if self.overriden_default: - kwargs.pop('default') - kwargs.pop('limit_choices_to', None) + kwargs.pop("default") + kwargs.pop("limit_choices_to", None) return name, path, args, kwargs diff --git a/polymodels/forms.py b/polymodels/forms.py index 8e16585..213ebb4 100644 --- a/polymodels/forms.py +++ b/polymodels/forms.py @@ -22,7 +22,7 @@ def __getitem__(self, model): class PolymorphicModelForm(models.ModelForm, metaclass=PolymorphicModelFormMetaclass): def __new__(cls, *args, **kwargs): - instance = kwargs.get('instance', None) + instance = kwargs.get("instance", None) if instance: cls = cls[instance.__class__] return super().__new__(cls) diff --git a/polymodels/managers.py b/polymodels/managers.py index f2c3b8c..cf9ec37 100644 --- a/polymodels/managers.py +++ b/polymodels/managers.py @@ -5,8 +5,10 @@ from django.db import models from django.db.models.query import ModelIterable -type_cast_iterator = partial(map, methodcaller('type_cast')) -type_cast_prefetch_iterator = partial(map, methodcaller('type_cast', with_prefetched_objects=True)) +type_cast_iterator = partial(map, methodcaller("type_cast")) +type_cast_prefetch_iterator = partial( + map, methodcaller("type_cast", with_prefetched_objects=True) +) class PolymorphicModelIterable(ModelIterable): @@ -31,9 +33,7 @@ def select_subclasses(self, *models): subclasses = set() for model in models: if not issubclass(model, self.model): - raise TypeError( - "%r is not a subclass of %r" % (model, self.model) - ) + raise TypeError("%r is not a subclass of %r" % (model, self.model)) subclasses.update(model.subclass_accessors) # Collect all `select_related` required lookups for subclass in subclasses: @@ -41,9 +41,7 @@ def select_subclasses(self, *models): related_lookup = accessors[subclass].related_lookup if related_lookup: related_lookups.add(related_lookup) - queryset = self.filter( - **self.model.content_type_lookup(*tuple(subclasses)) - ) + queryset = self.filter(**self.model.content_type_lookup(*tuple(subclasses))) else: # Collect all `select_related` required relateds for accessor in accessors.values(): @@ -63,7 +61,9 @@ def _fetch_all(self): # Override _fetch_all in order to disable PolymorphicModelIterable's # type casting when prefetch_related is used because the latter might # crash or disfunction when dealing with a mixed set of objects. - prefetch_related_objects = self._prefetch_related_lookups and not self._prefetch_done + prefetch_related_objects = ( + self._prefetch_related_lookups and not self._prefetch_done + ) type_cast = False if self._result_cache is None: iterable_class = self._iterable_class @@ -74,17 +74,20 @@ def _fetch_all(self): if prefetch_related_objects: self._prefetch_related_objects() if type_cast: - self._result_cache = list(type_cast_prefetch_iterator(self._result_cache)) + self._result_cache = list( + type_cast_prefetch_iterator(self._result_cache) + ) class PolymorphicManager(models.Manager.from_queryset(PolymorphicQuerySet)): def contribute_to_class(self, model, name): # Avoid circular reference from .models import BasePolymorphicModel + if not issubclass(model, BasePolymorphicModel): raise ImproperlyConfigured( - '`%s` can only be used on ' - '`BasePolymorphicModel` subclasses.' % self.__class__.__name__ + "`%s` can only be used on " + "`BasePolymorphicModel` subclasses." % self.__class__.__name__ ) return super().contribute_to_class(model, name) diff --git a/polymodels/models.py b/polymodels/models.py index 2d933e4..726ca7d 100644 --- a/polymodels/models.py +++ b/polymodels/models.py @@ -14,7 +14,9 @@ from .utils import copy_fields, get_content_type, get_content_types -class SubclassAccessor(namedtuple('SubclassAccessor', ['attrs', 'proxy', 'related_lookup'])): +class SubclassAccessor( + namedtuple("SubclassAccessor", ["attrs", "proxy", "related_lookup"]) +): @staticmethod def _identity(obj): return obj @@ -23,7 +25,7 @@ def _identity(obj): def attrgetter(self): if not self.attrs: return self._identity - return attrgetter('.'.join(self.attrs)) + return attrgetter(".".join(self.attrs)) def __call__(self, obj, with_prefetched_objects=False): # Cast to the right concrete model by going up in the @@ -41,7 +43,7 @@ def __call__(self, obj, with_prefetched_objects=False): return casted -EMPTY_ACCESSOR = SubclassAccessor((), None, '') +EMPTY_ACCESSOR = SubclassAccessor((), None, "") class SubclassAccessors(defaultdict): @@ -85,14 +87,22 @@ def __missing__(self, model_key): with self.lock: for model in self.apps.get_models(): opts = model._meta - if opts.proxy and issubclass(model, owner) and (owner._meta.proxy or opts.concrete_model is owner): - accessors[model] = SubclassAccessor((), model, '') + if ( + opts.proxy + and issubclass(model, owner) + and (owner._meta.proxy or opts.concrete_model is owner) + ): + accessors[model] = SubclassAccessor((), model, "") # Use .get() instead of `in` as proxy inheritance is also # stored in _meta.parents as None. elif opts.parents.get(owner): part = opts.model_name - for child, (parts, proxy, _lookup) in self[self.get_model_key(opts)].items(): - accessors[child] = SubclassAccessor((part,) + parts, proxy, LOOKUP_SEP.join((part,) + parts)) + for child, (parts, proxy, _lookup) in self[ + self.get_model_key(opts) + ].items(): + accessors[child] = SubclassAccessor( + (part,) + parts, proxy, LOOKUP_SEP.join((part,) + parts) + ) return accessors @@ -118,7 +128,9 @@ def save(self, *args, **kwargs): def delete(self, using=None, keep_parents=False): kept_parent = None if keep_parents: - parent_ptr = next(iter(self._meta.concrete_model._meta.parents.values()), None) + parent_ptr = next( + iter(self._meta.concrete_model._meta.parents.values()), None + ) if parent_ptr: kept_parent = getattr(self, parent_ptr.name) if kept_parent: @@ -135,7 +147,7 @@ def delete(self, using=None, keep_parents=False): @classmethod def content_type_lookup(cls, *models, **kwargs): - query_name = kwargs.pop('query_name', None) or cls.CONTENT_TYPE_FIELD + query_name = kwargs.pop("query_name", None) or cls.CONTENT_TYPE_FIELD if models: query_name = "%s__in" % query_name value = set(ct.pk for ct in get_content_types(*models).values()) @@ -153,39 +165,51 @@ def subclasses_lookup(cls, query_name=None): def check(cls, **kwargs): errors = super().check(**kwargs) try: - content_type_field_name = getattr(cls, 'CONTENT_TYPE_FIELD') + content_type_field_name = getattr(cls, "CONTENT_TYPE_FIELD") except AttributeError: - errors.append(checks.Error( - '`BasePolymorphicModel` subclasses must define a `CONTENT_TYPE_FIELD`.', - hint=None, - obj=cls, - id='polymodels.E001', - )) + errors.append( + checks.Error( + "`BasePolymorphicModel` subclasses must define a `CONTENT_TYPE_FIELD`.", + hint=None, + obj=cls, + id="polymodels.E001", + ) + ) else: try: content_type_field = cls._meta.get_field(content_type_field_name) except FieldDoesNotExist: - errors.append(checks.Error( - "`CONTENT_TYPE_FIELD` points to an inexistent field '%s'." % content_type_field_name, - hint=None, - obj=cls, - id='polymodels.E002', - )) - else: - if (not isinstance(content_type_field, models.ForeignKey) or - content_type_field.remote_field.model is not ContentType): - errors.append(checks.Error( - "`%s` must be a `ForeignKey` to `ContentType`." % content_type_field_name, + errors.append( + checks.Error( + "`CONTENT_TYPE_FIELD` points to an inexistent field '%s'." + % content_type_field_name, hint=None, - obj=content_type_field, - id='polymodels.E003', - )) + obj=cls, + id="polymodels.E002", + ) + ) + else: + if ( + not isinstance(content_type_field, models.ForeignKey) + or content_type_field.remote_field.model is not ContentType + ): + errors.append( + checks.Error( + "`%s` must be a `ForeignKey` to `ContentType`." + % content_type_field_name, + hint=None, + obj=content_type_field, + id="polymodels.E003", + ) + ) return errors class PolymorphicModel(BasePolymorphicModel): - CONTENT_TYPE_FIELD = 'content_type' - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+') + CONTENT_TYPE_FIELD = "content_type" + content_type = models.ForeignKey( + ContentType, on_delete=models.CASCADE, related_name="+" + ) objects = PolymorphicManager() diff --git a/polymodels/utils.py b/polymodels/utils.py index 1ba43cc..1b08210 100644 --- a/polymodels/utils.py +++ b/polymodels/utils.py @@ -15,4 +15,6 @@ def copy_fields(src, to): get_content_type = partial(ContentType.objects.get_for_model, for_concrete_model=False) -get_content_types = partial(ContentType.objects.get_for_models, for_concrete_models=False) +get_content_types = partial( + ContentType.objects.get_for_models, for_concrete_models=False +) diff --git a/setup.cfg b/setup.cfg index cb409af..96d021c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ max-line-length = 119 [isort] combine_as_imports=true include_trailing_comma=true -multi_line_output=5 +multi_line_output=3 not_skip=__init__.py [metadata] diff --git a/tests/base.py b/tests/base.py index 785f677..67e626b 100644 --- a/tests/base.py +++ b/tests/base.py @@ -10,10 +10,13 @@ class TestCase(TestCase): def assertQuerysetEqual(self, qs, values, transform=None, ordered=True, msg=None): if self.DJANGO_GTE_42: - super().assertQuerySetEqual(qs, values, transform=transform or repr, ordered=ordered, msg=msg) + super().assertQuerySetEqual( + qs, values, transform=transform or repr, ordered=ordered, msg=msg + ) else: - super().assertQuerysetEqual(qs, values, transform=transform or repr, ordered=ordered, msg=msg) - + super().assertQuerysetEqual( + qs, values, transform=transform or repr, ordered=ordered, msg=msg + ) def tearDown(self): ContentType.objects.clear_cache() diff --git a/tests/forms.py b/tests/forms.py index 5d5b849..0f3f33b 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -5,17 +5,17 @@ class AnimalForm(PolymorphicModelForm): class Meta: - fields = ['name'] + fields = ["name"] model = Animal class SnakeForm(AnimalForm): class Meta: - fields = ['name'] + fields = ["name"] model = Snake class BigSnakeForm(SnakeForm): class Meta: - fields = ['name'] + fields = ["name"] model = BigSnake diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index cd3c1e9..eb92fc1 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -8,185 +8,183 @@ class Migration(migrations.Migration): initial = True - dependencies = [('contenttypes', '0002_remove_content_type_name')] + dependencies = [("contenttypes", "0002_remove_content_type_name")] operations = [ migrations.CreateModel( - name='Animal', + name="Animal", fields=[ ( - 'id', + "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, - verbose_name='ID', + verbose_name="ID", ), ), - ('name', models.CharField(max_length=50)), + ("name", models.CharField(max_length=50)), ], - options={'ordering': ['id']}, + options={"ordering": ["id"]}, ), migrations.CreateModel( - name='Trait', + name="Trait", fields=[ ( - 'id', + "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, - verbose_name='ID', + verbose_name="ID", ), ), ( - 'content_type', + "content_type", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name='+', - to='contenttypes.ContentType', + related_name="+", + to="contenttypes.ContentType", ), ), ( - 'mammal_type', + "mammal_type", polymodels.fields.PolymorphicTypeField( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - polymorphic_type='tests.Mammal', + polymorphic_type="tests.Mammal", ), ), ( - 'snake_type', + "snake_type", polymodels.fields.PolymorphicTypeField( on_delete=django.db.models.deletion.CASCADE, - polymorphic_type='tests.Snake', + polymorphic_type="tests.Snake", ), ), ( - 'trait_type', + "trait_type", polymodels.fields.PolymorphicTypeField( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - polymorphic_type='tests.Trait', + polymorphic_type="tests.Trait", ), ), ], - options={'abstract': False}, + options={"abstract": False}, ), migrations.CreateModel( - name='Zoo', + name="Zoo", fields=[ ( - 'id', + "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, - verbose_name='ID', + verbose_name="ID", ), ), ( - 'zoos', - models.ManyToManyField( - 'Animal', related_name='zoos' - ), - ) + "zoos", + models.ManyToManyField("Animal", related_name="zoos"), + ), ], ), migrations.CreateModel( - name='Mammal', + name="Mammal", fields=[ ( - 'animal_ptr', + "animal_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to='tests.Animal', + to="tests.Animal", ), ) ], - options={'abstract': False}, - bases=('tests.animal',), + options={"abstract": False}, + bases=("tests.animal",), ), migrations.CreateModel( - name='Snake', + name="Snake", fields=[ ( - 'animal_ptr', + "animal_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to='tests.Animal', + to="tests.Animal", ), ), - ('length', models.SmallIntegerField()), - ('color', models.CharField(blank=True, max_length=100)), + ("length", models.SmallIntegerField()), + ("color", models.CharField(blank=True, max_length=100)), ], - options={'ordering': ['id']}, - bases=('tests.animal',), + options={"ordering": ["id"]}, + bases=("tests.animal",), ), migrations.AddField( - model_name='zoo', - name='animals', - field=models.ManyToManyField(to='tests.Animal'), + model_name="zoo", + name="animals", + field=models.ManyToManyField(to="tests.Animal"), ), migrations.AddField( - model_name='animal', - name='content_type', + model_name="animal", + name="content_type", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, - related_name='+', - to='contenttypes.ContentType', + related_name="+", + to="contenttypes.ContentType", ), ), migrations.CreateModel( - name='AcknowledgedTrait', + name="AcknowledgedTrait", fields=[], - options={'proxy': True, 'indexes': []}, - bases=('tests.trait',), + options={"proxy": True, "indexes": []}, + bases=("tests.trait",), ), migrations.CreateModel( - name='Monkey', + name="Monkey", fields=[ ( - 'mammal_ptr', + "mammal_ptr", models.OneToOneField( auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, - to='tests.Mammal', + to="tests.Mammal", ), ), ( - 'friends', + "friends", models.ManyToManyField( - related_name='_monkey_friends_+', to='tests.Monkey' + related_name="_monkey_friends_+", to="tests.Monkey" ), ), ], - options={'abstract': False}, - bases=('tests.mammal',), + options={"abstract": False}, + bases=("tests.mammal",), ), migrations.CreateModel( - name='BigSnake', + name="BigSnake", fields=[], - options={'proxy': True, 'indexes': []}, - bases=('tests.snake',), + options={"proxy": True, "indexes": []}, + bases=("tests.snake",), ), migrations.CreateModel( - name='HugeSnake', + name="HugeSnake", fields=[], - options={'proxy': True, 'indexes': []}, - bases=('tests.bigsnake',), + options={"proxy": True, "indexes": []}, + bases=("tests.bigsnake",), ), ] diff --git a/tests/models.py b/tests/models.py index f55a8ff..486ee4f 100644 --- a/tests/models.py +++ b/tests/models.py @@ -5,14 +5,14 @@ class Zoo(models.Model): - animals = models.ManyToManyField('Animal', related_name='zoos') + animals = models.ManyToManyField("Animal", related_name="zoos") class Animal(PolymorphicModel): name = models.CharField(max_length=50) class Meta: - ordering = ['id'] + ordering = ["id"] def __str__(self): return self.name @@ -20,7 +20,7 @@ def __str__(self): class NotInstalledAnimal(Animal): class Meta: - app_label = 'not_installed' + app_label = "not_installed" class Mammal(Animal): @@ -28,13 +28,17 @@ class Mammal(Animal): class Monkey(Mammal): - friends = models.ManyToManyField('self') + friends = models.ManyToManyField("self") class Trait(PolymorphicModel): - trait_type = PolymorphicTypeField('self', on_delete=models.CASCADE, blank=True, null=True) - mammal_type = PolymorphicTypeField(Mammal, on_delete=models.CASCADE, blank=True, null=True) - snake_type = PolymorphicTypeField('Snake', on_delete=models.CASCADE) + trait_type = PolymorphicTypeField( + "self", on_delete=models.CASCADE, blank=True, null=True + ) + mammal_type = PolymorphicTypeField( + Mammal, on_delete=models.CASCADE, blank=True, null=True + ) + snake_type = PolymorphicTypeField("Snake", on_delete=models.CASCADE) class AcknowledgedTrait(Trait): @@ -47,14 +51,14 @@ class Reptile(Animal): class Meta: abstract = True - ordering = ['id'] + ordering = ["id"] class Snake(Reptile): color = models.CharField(max_length=100, blank=True) class Meta: - ordering = ['id'] + ordering = ["id"] class BigSnake(Snake): diff --git a/tests/settings.py b/tests/settings.py index adc9e56..fef010d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,15 +1,15 @@ -SECRET_KEY = 'not-anymore' +SECRET_KEY = "not-anymore" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", }, } INSTALLED_APPS = [ - 'django.contrib.contenttypes', - 'polymodels', - 'tests', + "django.contrib.contenttypes", + "polymodels", + "tests", ] -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tests/test_fields.py b/tests/test_fields.py index 724012c..061e5cc 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -14,10 +14,10 @@ class ContentTypeReferenceTests(TestCase): - reference = ContentTypeReference(str('tests'), str('snake')) + reference = ContentTypeReference(str("tests"), str("snake")) def test_equality(self): - self.assertEqual(self.reference, ContentTypeReference('tests', 'snake')) + self.assertEqual(self.reference, ContentTypeReference("tests", "snake")) def test_retreival(self): self.assertEqual(self.reference(), get_content_type(Snake).pk) @@ -42,20 +42,25 @@ def test_limit_choices_to(self): """ field = PolymorphicTypeField(Trait, on_delete=models.CASCADE) remote_field = field.remote_field - subclasses_lookup = Trait.subclasses_lookup('pk') + subclasses_lookup = Trait.subclasses_lookup("pk") self.assertEqual(remote_field.limit_choices_to(), subclasses_lookup) # Test dict() limit_choices_to. - limit_choices_to = {'app_label': 'polymodels'} - field = PolymorphicTypeField(Trait, on_delete=models.CASCADE, limit_choices_to=limit_choices_to) + limit_choices_to = {"app_label": "polymodels"} + field = PolymorphicTypeField( + Trait, on_delete=models.CASCADE, limit_choices_to=limit_choices_to + ) remote_field = field.remote_field self.assertEqual( remote_field.limit_choices_to(), dict(subclasses_lookup, **limit_choices_to) ) # Test Q() limit_choices_to. - field = PolymorphicTypeField(Trait, on_delete=models.CASCADE, limit_choices_to=Q(**limit_choices_to)) + field = PolymorphicTypeField( + Trait, on_delete=models.CASCADE, limit_choices_to=Q(**limit_choices_to) + ) remote_field = field.remote_field self.assertEqual( - str(remote_field.limit_choices_to()), str(Q(**limit_choices_to) & Q(**subclasses_lookup)) + str(remote_field.limit_choices_to()), + str(Q(**limit_choices_to) & Q(**subclasses_lookup)), ) def test_invalid_type(self): @@ -64,7 +69,7 @@ def test_invalid_type(self): trait.mammal_type = snake_type trait.snake_type = snake_type with self.assertRaisesMessage( - ValidationError, 'Specified content type is not of a subclass of Mammal.' + ValidationError, "Specified content type is not of a subclass of Mammal." ): trait.full_clean() @@ -79,53 +84,66 @@ def test_valid_proxy_subclass(self): trait.full_clean() def test_description(self): - trait_type = Trait._meta.get_field('trait_type') + trait_type = Trait._meta.get_field("trait_type") self.assertEqual( trait_type.description % trait_type.__dict__, - 'Content type of a subclass of Trait' + "Content type of a subclass of Trait", ) def test_checks(self): - test_apps = Apps(['tests', 'django.contrib.contenttypes']) + test_apps = Apps(["tests", "django.contrib.contenttypes"]) class ContentType(models.Model): class Meta: apps = test_apps - app_label = 'contenttypes' + app_label = "contenttypes" class CheckModel(PolymorphicModel): - valid = PolymorphicTypeField('self', on_delete=models.CASCADE) - unresolved = PolymorphicTypeField('unresolved', on_delete=models.CASCADE) - non_polymorphic_base = PolymorphicTypeField('contenttypes.ContentType', on_delete=models.CASCADE) + valid = PolymorphicTypeField("self", on_delete=models.CASCADE) + unresolved = PolymorphicTypeField("unresolved", on_delete=models.CASCADE) + non_polymorphic_base = PolymorphicTypeField( + "contenttypes.ContentType", on_delete=models.CASCADE + ) class Meta: apps = test_apps - self.assertEqual(CheckModel._meta.get_field('valid').check(), []) - self.assertEqual(CheckModel._meta.get_field('unresolved').check(), [ - checks.Error( - "Field defines a relation with model 'unresolved', which is either not installed, or is abstract.", - id='fields.E300', - ), - ]) - self.assertEqual(CheckModel._meta.get_field('non_polymorphic_base').check(), [ - checks.Error( - "The ContentType type is not a subclass of BasePolymorphicModel.", - id='polymodels.E004', - ), - ]) + self.assertEqual(CheckModel._meta.get_field("valid").check(), []) + self.assertEqual( + CheckModel._meta.get_field("unresolved").check(), + [ + checks.Error( + "Field defines a relation with model 'unresolved', which is either not installed, or is abstract.", + id="fields.E300", + ), + ], + ) + self.assertEqual( + CheckModel._meta.get_field("non_polymorphic_base").check(), + [ + checks.Error( + "The ContentType type is not a subclass of BasePolymorphicModel.", + id="polymodels.E004", + ), + ], + ) def test_formfield_issues_no_queries(self): - trait_type = Trait._meta.get_field('trait_type') + trait_type = Trait._meta.get_field("trait_type") with self.assertNumQueries(0): formfield = trait_type.formfield() - self.assertSetEqual(set(formfield.queryset), { - get_content_type(Trait), - get_content_type(AcknowledgedTrait), - }) + self.assertSetEqual( + set(formfield.queryset), + { + get_content_type(Trait), + get_content_type(AcknowledgedTrait), + }, + ) def test_unresolved_relationship_formfield(self): - field = PolymorphicTypeField('Snake', to='app.Unresolved', on_delete=models.CASCADE) + field = PolymorphicTypeField( + "Snake", to="app.Unresolved", on_delete=models.CASCADE + ) with self.assertRaises(ValueError): field.formfield() @@ -135,14 +153,18 @@ def safe_exec(self, string, value=None): exec(string, globals(), scope) except Exception as e: if value: - self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e)) + self.fail( + "Could not exec %r (from value %r): %s" % (string.strip(), value, e) + ) else: self.fail("Could not exec %r: %s" % (string.strip(), e)) return scope def serialize_round_trip(self, value): string, imports = MigrationWriter.serialize(value) - return self.safe_exec("%s\ntest_value_result = %s" % ("\n".join(imports), string), value)['test_value_result'] + return self.safe_exec( + "%s\ntest_value_result = %s" % ("\n".join(imports), string), value + )["test_value_result"] def assertDeconstructionEqual(self, field, deconstructed): self.assertEqual(field.deconstruct(), deconstructed) @@ -153,44 +175,70 @@ def test_field_deconstruction(self): test_apps = Apps() class Foo(PolymorphicModel): - foo = PolymorphicTypeField('self', on_delete=models.CASCADE) + foo = PolymorphicTypeField("self", on_delete=models.CASCADE) class Meta: apps = test_apps - app_label = 'polymodels' + app_label = "polymodels" class Bar(models.Model): - foo = PolymorphicTypeField('Foo', on_delete=models.CASCADE) + foo = PolymorphicTypeField("Foo", on_delete=models.CASCADE) foo_null = PolymorphicTypeField(Foo, on_delete=models.CASCADE, null=True) - foo_default = PolymorphicTypeField(Foo, on_delete=models.CASCADE, default=get_content_type(Foo).pk) + foo_default = PolymorphicTypeField( + Foo, on_delete=models.CASCADE, default=get_content_type(Foo).pk + ) class Meta: apps = test_apps - app_label = 'polymodels' - - self.assertDeconstructionEqual(Foo._meta.get_field('foo'), ( - 'foo', 'polymodels.fields.PolymorphicTypeField', [], { - 'polymorphic_type': 'polymodels.Foo', - 'on_delete': models.CASCADE, - } - )) - self.assertDeconstructionEqual(Bar._meta.get_field('foo'), ( - 'foo', 'polymodels.fields.PolymorphicTypeField', [], { - 'polymorphic_type': 'polymodels.Foo', - 'on_delete': models.CASCADE, - } - )) - self.assertDeconstructionEqual(Bar._meta.get_field('foo_null'), ( - 'foo_null', 'polymodels.fields.PolymorphicTypeField', [], { - 'polymorphic_type': 'polymodels.Foo', - 'null': True, - 'on_delete': models.CASCADE, - } - )) - self.assertDeconstructionEqual(Bar._meta.get_field('foo_default'), ( - 'foo_default', 'polymodels.fields.PolymorphicTypeField', [], { - 'polymorphic_type': 'polymodels.Foo', - 'default': get_content_type(Foo).pk, - 'on_delete': models.CASCADE, - } - )) + app_label = "polymodels" + + self.assertDeconstructionEqual( + Foo._meta.get_field("foo"), + ( + "foo", + "polymodels.fields.PolymorphicTypeField", + [], + { + "polymorphic_type": "polymodels.Foo", + "on_delete": models.CASCADE, + }, + ), + ) + self.assertDeconstructionEqual( + Bar._meta.get_field("foo"), + ( + "foo", + "polymodels.fields.PolymorphicTypeField", + [], + { + "polymorphic_type": "polymodels.Foo", + "on_delete": models.CASCADE, + }, + ), + ) + self.assertDeconstructionEqual( + Bar._meta.get_field("foo_null"), + ( + "foo_null", + "polymodels.fields.PolymorphicTypeField", + [], + { + "polymorphic_type": "polymodels.Foo", + "null": True, + "on_delete": models.CASCADE, + }, + ), + ) + self.assertDeconstructionEqual( + Bar._meta.get_field("foo_default"), + ( + "foo_default", + "polymodels.fields.PolymorphicTypeField", + [], + { + "polymorphic_type": "polymodels.Foo", + "default": get_content_type(Foo).pk, + "on_delete": models.CASCADE, + }, + ), + ) diff --git a/tests/test_managers.py b/tests/test_managers.py index 9fdda07..79c4a0b 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -9,126 +9,146 @@ class PolymorphicQuerySetTest(TestCase): def test_select_subclasses(self): - Animal.objects.create(name='animal') - Mammal.objects.create(name='mammal') - Monkey.objects.create(name='monkey') - Snake.objects.create(name='snake', length=10) - BigSnake.objects.create(name='big snake', length=101) - HugeSnake.objects.create(name='huge snake', length=155) + Animal.objects.create(name="animal") + Mammal.objects.create(name="mammal") + Monkey.objects.create(name="monkey") + Snake.objects.create(name="snake", length=10) + BigSnake.objects.create(name="big snake", length=101) + HugeSnake.objects.create(name="huge snake", length=155) # Assert `select_subclasses` correctly calls `select_related` and `filter`. animals = Animal.objects.select_subclasses() animals_expected_query_select_related = { - 'mammal': {'monkey': {}}, - 'snake': {}, + "mammal": {"monkey": {}}, + "snake": {}, } - self.assertEqual(animals.query.select_related, animals_expected_query_select_related) + self.assertEqual( + animals.query.select_related, animals_expected_query_select_related + ) with self.assertNumQueries(1): - self.assertQuerysetEqual(animals.all(), - ['', - '', - '', - '', - '', - '']) + self.assertQuerysetEqual( + animals.all(), + [ + "", + "", + "", + "", + "", + "", + ], + ) with self.assertNumQueries(1): - self.assertQuerysetEqual(list(animals.iterator()), - ['', - '', - '', - '', - '', - '']) + self.assertQuerysetEqual( + list(animals.iterator()), + [ + "", + "", + "", + "", + "", + "", + ], + ) # Filter out non-mammal (direct subclass) animal_mammals = Animal.objects.select_subclasses(Mammal) - animal_mammals_expected_query_select_related = { - 'mammal': {'monkey': {}} - } + animal_mammals_expected_query_select_related = {"mammal": {"monkey": {}}} self.assertEqual( animal_mammals.query.select_related, - animal_mammals_expected_query_select_related + animal_mammals_expected_query_select_related, ) with self.assertNumQueries(1): - self.assertQuerysetEqual(animal_mammals.all(), - ['', - '']) + self.assertQuerysetEqual( + animal_mammals.all(), ["", ""] + ) # Filter out non-snake (subclass through an abstract one) animal_snakes = Animal.objects.select_subclasses(Snake) - self.assertEqual(animal_snakes.query.select_related, {'snake': {}}) + self.assertEqual(animal_snakes.query.select_related, {"snake": {}}) with self.assertNumQueries(1): - self.assertQuerysetEqual(animal_snakes.all(), - ['', - '', - '']) + self.assertQuerysetEqual( + animal_snakes.all(), + ["", "", ""], + ) # Subclass with only proxies snakes = Snake.objects.select_subclasses() self.assertFalse(snakes.query.select_related) with self.assertNumQueries(1): - self.assertQuerysetEqual(snakes.all(), - ['', - '', - '']) + self.assertQuerysetEqual( + snakes.all(), + ["", "", ""], + ) # Subclass filter proxies snake_bigsnakes = Snake.objects.select_subclasses(BigSnake) self.assertFalse(snakes.query.select_related) with self.assertNumQueries(1): - self.assertQuerysetEqual(snake_bigsnakes.all(), - ['', - '']) + self.assertQuerysetEqual( + snake_bigsnakes.all(), + ["", ""], + ) def test_select_subclasses_get(self): - snake = Snake.objects.create(name='snake', length=10) + snake = Snake.objects.create(name="snake", length=10) self.assertEqual(Animal.objects.select_subclasses().get(), snake) def test_select_subclasses_values(self): - Animal.objects.create(name='animal') + Animal.objects.create(name="animal") self.assertQuerysetEqual( - Animal.objects.select_subclasses().values_list('name', flat=True), ['animal'], lambda x: x + Animal.objects.select_subclasses().values_list("name", flat=True), + ["animal"], + lambda x: x, ) def test_exclude_subclasses(self): - Animal.objects.create(name='animal') - Mammal.objects.create(name='first mammal') - Mammal.objects.create(name='second mammal') - Monkey.objects.create(name='donkey kong') - self.assertQuerysetEqual(Animal.objects.exclude_subclasses(), - ['']) - self.assertQuerysetEqual(Mammal.objects.exclude_subclasses(), - ['', - '']) - self.assertQuerysetEqual(Monkey.objects.exclude_subclasses(), - ['']) + Animal.objects.create(name="animal") + Mammal.objects.create(name="first mammal") + Mammal.objects.create(name="second mammal") + Monkey.objects.create(name="donkey kong") + self.assertQuerysetEqual( + Animal.objects.exclude_subclasses(), [""] + ) + self.assertQuerysetEqual( + Mammal.objects.exclude_subclasses(), + ["", ""], + ) + self.assertQuerysetEqual( + Monkey.objects.exclude_subclasses(), [""] + ) def test_select_subclasses_prefetch_related(self): zoo = Zoo.objects.create() - animal = Animal.objects.create(name='animal') - mammal = Mammal.objects.create(name='mammal') - monkey = Monkey.objects.create(name='monkey') + animal = Animal.objects.create(name="animal") + mammal = Mammal.objects.create(name="mammal") + monkey = Monkey.objects.create(name="monkey") zoo.animals.add(animal, mammal, monkey) - other_monkey = Monkey.objects.create(name='monkey') + other_monkey = Monkey.objects.create(name="monkey") monkey.friends.add(other_monkey) - queryset = Animal.objects.select_subclasses().prefetch_related('zoos') + queryset = Animal.objects.select_subclasses().prefetch_related("zoos") with self.assertNumQueries(2): - self.assertSequenceEqual(queryset, [ - animal, - mammal, - monkey, - other_monkey, - ]) + self.assertSequenceEqual( + queryset, + [ + animal, + mammal, + monkey, + other_monkey, + ], + ) self.assertSequenceEqual(queryset[0].zoos.all(), [zoo]) self.assertSequenceEqual(queryset[1].zoos.all(), [zoo]) self.assertSequenceEqual(queryset[2].zoos.all(), [zoo]) # Test prefetch related combination. queryset = Animal.objects.select_subclasses().prefetch_related( - 'zoos', - 'mammal__monkey__friends', + "zoos", + "mammal__monkey__friends", ) with self.assertNumQueries(3): - self.assertSequenceEqual(queryset, [ - animal, - mammal, - monkey, - other_monkey, - ]) + self.assertSequenceEqual( + queryset, + [ + animal, + mammal, + monkey, + other_monkey, + ], + ) self.assertSequenceEqual(queryset[0].zoos.all(), [zoo]) self.assertSequenceEqual(queryset[1].zoos.all(), [zoo]) self.assertSequenceEqual(queryset[2].zoos.all(), [zoo]) @@ -139,28 +159,29 @@ def test_select_subclasses_prefetch_related(self): class PolymorphicManagerTest(TestCase): def test_improperly_configured(self): with self.assertRaisesMessage( - ImproperlyConfigured, '`PolymorphicManager` can only be used on `BasePolymorphicModel` subclasses.' + ImproperlyConfigured, + "`PolymorphicManager` can only be used on `BasePolymorphicModel` subclasses.", ): + class NonPolymorphicModel(models.Model): objects = PolymorphicManager() class Meta: - app_label = 'polymodels' + app_label = "polymodels" def test_proxy_filtering(self): """ Make sure managers attached to proxy models returns a queryset of proxies only. """ - Snake.objects.create(name='snake', length=1) - BigSnake.objects.create(name='big snake', length=10) - HugeSnake.objects.create(name='huge snake', length=100) - self.assertQuerysetEqual(Snake.objects.all(), - ['', - '', - '']) - self.assertQuerysetEqual(BigSnake.objects.all(), - ['', - '']) - self.assertQuerysetEqual(HugeSnake.objects.all(), - ['']) + Snake.objects.create(name="snake", length=1) + BigSnake.objects.create(name="big snake", length=10) + HugeSnake.objects.create(name="huge snake", length=100) + self.assertQuerysetEqual( + Snake.objects.all(), + ["", "", ""], + ) + self.assertQuerysetEqual( + BigSnake.objects.all(), ["", ""] + ) + self.assertQuerysetEqual(HugeSnake.objects.all(), [""]) diff --git a/tests/test_models.py b/tests/test_models.py index 9efc93d..abeaab0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,10 @@ from django.test.testcases import SimpleTestCase from polymodels.models import ( - EMPTY_ACCESSOR, BasePolymorphicModel, SubclassAccessor, SubclassAccessors, + EMPTY_ACCESSOR, + BasePolymorphicModel, + SubclassAccessor, + SubclassAccessors, ) from .base import TestCase @@ -15,69 +18,83 @@ class BasePolymorphicModelTest(TestCase): def test_checks(self): test_apps = Apps() - options = type(str('Meta'), (), {'apps': test_apps, 'app_label': 'polymodels'}) + options = type(str("Meta"), (), {"apps": test_apps, "app_label": "polymodels"}) class NoCtFieldModel(BasePolymorphicModel): Meta = options - self.assertIn(checks.Error( - '`BasePolymorphicModel` subclasses must define a `CONTENT_TYPE_FIELD`.', - hint=None, - obj=NoCtFieldModel, - id='polymodels.E001', - ), NoCtFieldModel.check()) + self.assertIn( + checks.Error( + "`BasePolymorphicModel` subclasses must define a `CONTENT_TYPE_FIELD`.", + hint=None, + obj=NoCtFieldModel, + id="polymodels.E001", + ), + NoCtFieldModel.check(), + ) class InexistentCtFieldModel(BasePolymorphicModel): - CONTENT_TYPE_FIELD = 'inexistent_field' + CONTENT_TYPE_FIELD = "inexistent_field" Meta = options - self.assertIn(checks.Error( - "`CONTENT_TYPE_FIELD` points to an inexistent field 'inexistent_field'.", - hint=None, - obj=InexistentCtFieldModel, - id='polymodels.E002', - ), InexistentCtFieldModel.check()) + self.assertIn( + checks.Error( + "`CONTENT_TYPE_FIELD` points to an inexistent field 'inexistent_field'.", + hint=None, + obj=InexistentCtFieldModel, + id="polymodels.E002", + ), + InexistentCtFieldModel.check(), + ) class InvalidCtFieldModel(BasePolymorphicModel): - CONTENT_TYPE_FIELD = 'a_char_field' + CONTENT_TYPE_FIELD = "a_char_field" a_char_field = models.CharField(max_length=255) Meta = options - self.assertIn(checks.Error( - "`a_char_field` must be a `ForeignKey` to `ContentType`.", - hint=None, - obj=InvalidCtFieldModel._meta.get_field('a_char_field'), - id='polymodels.E003', - ), InvalidCtFieldModel.check()) + self.assertIn( + checks.Error( + "`a_char_field` must be a `ForeignKey` to `ContentType`.", + hint=None, + obj=InvalidCtFieldModel._meta.get_field("a_char_field"), + id="polymodels.E003", + ), + InvalidCtFieldModel.check(), + ) class InvalidCtFkFieldToModel(BasePolymorphicModel): - CONTENT_TYPE_FIELD = 'a_fk' - a_fk = models.ForeignKey('self', on_delete=models.CASCADE) + CONTENT_TYPE_FIELD = "a_fk" + a_fk = models.ForeignKey("self", on_delete=models.CASCADE) Meta = options - self.assertIn(checks.Error( - "`a_fk` must be a `ForeignKey` to `ContentType`.", - hint=None, - obj=InvalidCtFkFieldToModel._meta.get_field('a_fk'), - id='polymodels.E003', - ), InvalidCtFkFieldToModel.check()) + self.assertIn( + checks.Error( + "`a_fk` must be a `ForeignKey` to `ContentType`.", + hint=None, + obj=InvalidCtFkFieldToModel._meta.get_field("a_fk"), + id="polymodels.E003", + ), + InvalidCtFkFieldToModel.check(), + ) def test_type_cast(self): - animal_dog = Animal.objects.create(name='dog') + animal_dog = Animal.objects.create(name="dog") with self.assertNumQueries(0): self.assertEqual( - animal_dog.type_cast(), animal_dog, - 'Type casting a correctly typed class should work.' + animal_dog.type_cast(), + animal_dog, + "Type casting a correctly typed class should work.", ) - mammal_cat = Mammal.objects.create(name='cat') + mammal_cat = Mammal.objects.create(name="cat") with self.assertNumQueries(0): self.assertEqual( - mammal_cat.type_cast(), mammal_cat, - 'Type casting a correctly typed subclass should work.' + mammal_cat.type_cast(), + mammal_cat, + "Type casting a correctly typed subclass should work.", ) animal_cat = Animal.objects.get(pk=mammal_cat.pk) @@ -90,18 +107,22 @@ def test_type_cast(self): animal_dog.type_cast(Mammal) # That's a big snake - anaconda_snake = Snake.objects.create(name='anaconda', length=152, color='green') + anaconda_snake = Snake.objects.create( + name="anaconda", length=152, color="green" + ) with self.assertNumQueries(0): self.assertIsInstance( - anaconda_snake.type_cast(BigSnake), BigSnake, - 'Proxy type casting should work' + anaconda_snake.type_cast(BigSnake), + BigSnake, + "Proxy type casting should work", ) with self.assertNumQueries(0): self.assertIsInstance( - anaconda_snake.type_cast(HugeSnake), HugeSnake, - 'Two level proxy type casting should work' + anaconda_snake.type_cast(HugeSnake), + HugeSnake, + "Two level proxy type casting should work", ) for subclass in [Snake, BigSnake, HugeSnake]: @@ -109,19 +130,19 @@ def test_type_cast(self): with self.assertNumQueries(1): anaconda_animal_type_casted = anaconda_animal.type_cast(subclass) self.assertIsInstance(anaconda_animal_type_casted, subclass) - self.assertEqual(anaconda_animal_type_casted.color, 'green') + self.assertEqual(anaconda_animal_type_casted.color, "green") def test_content_type_saving(self): # Creating a base class should assign the correct content_type. animal_content_type = ContentType.objects.get_for_model(Animal) with self.assertNumQueries(1): - animal = Animal.objects.create(name='dog') + animal = Animal.objects.create(name="dog") self.assertEqual(animal.content_type, animal_content_type) # Creating subclass should assign the correct content_type. mammal_content_type = ContentType.objects.get_for_model(Mammal) with self.assertNumQueries(2): - mammal = Mammal.objects.create(name='cat') + mammal = Mammal.objects.create(name="cat") self.assertEqual(mammal.content_type, mammal_content_type) # Updating a subclass's base class pointer should preserve content_type. @@ -131,7 +152,9 @@ def test_content_type_saving(self): # Creating a base class should honor explicit content_type. with self.assertNumQueries(1): - explicit_mammal = Animal.objects.create(name='beaver', content_type=mammal_content_type) + explicit_mammal = Animal.objects.create( + name="beaver", content_type=mammal_content_type + ) self.assertEqual(explicit_mammal.content_type, mammal_content_type) with self.assertNumQueries(2): beaver = Mammal.objects.create(animal_ptr=explicit_mammal) @@ -139,7 +162,7 @@ def test_content_type_saving(self): self.assertEqual(beaver.content_type, mammal_content_type) def test_delete_keep_parents(self): - snake = HugeSnake.objects.create(name='snek', length=30) + snake = HugeSnake.objects.create(name="snek", length=30) animal = snake.animal_ptr snake.delete(keep_parents=True) animal.refresh_from_db() @@ -149,7 +172,7 @@ def test_delete_keep_parents(self): class SubclassAccessorsTests(SimpleTestCase): def test_dynamic_model_creation_cache_busting(self): - test_apps = Apps(['tests']) + test_apps = Apps(["tests"]) class Base(models.Model): class Meta: @@ -157,23 +180,29 @@ class Meta: accessors = SubclassAccessors() - self.assertEqual(Base.accessors['tests', 'base'], {Base: EMPTY_ACCESSOR}) + self.assertEqual(Base.accessors["tests", "base"], {Base: EMPTY_ACCESSOR}) class DynamicChild(Base): class Meta: apps = test_apps - self.assertEqual(Base.accessors['tests', 'base'], { - Base: EMPTY_ACCESSOR, - DynamicChild: (('dynamicchild',), None, 'dynamicchild'), - }) + self.assertEqual( + Base.accessors["tests", "base"], + { + Base: EMPTY_ACCESSOR, + DynamicChild: (("dynamicchild",), None, "dynamicchild"), + }, + ) - self.assertEqual(DynamicChild.accessors, { - DynamicChild: EMPTY_ACCESSOR, - }) + self.assertEqual( + DynamicChild.accessors, + { + DynamicChild: EMPTY_ACCESSOR, + }, + ) def test_key_error(self): - test_apps = Apps(['tests']) + test_apps = Apps(["tests"]) class Base(models.Model): class Meta: @@ -186,10 +215,10 @@ class Meta: apps = test_apps with self.assertRaises(KeyError): - Base.accessors['tests', 'other'] + Base.accessors["tests", "other"] def test_proxy_accessors(self): - test_apps = Apps(['tests']) + test_apps = Apps(["tests"]) class Base(models.Model): class Meta: @@ -221,12 +250,25 @@ class Meta: apps = test_apps proxy = True - self.assertEqual(Root.accessors[Subclass], SubclassAccessor(('subclass',), None, 'subclass')) - self.assertEqual(Root.accessors[SubclassProxy], SubclassAccessor(('subclass',), SubclassProxy, 'subclass')) + self.assertEqual( + Root.accessors[Subclass], SubclassAccessor(("subclass",), None, "subclass") + ) + self.assertEqual( + Root.accessors[SubclassProxy], + SubclassAccessor(("subclass",), SubclassProxy, "subclass"), + ) self.assertEqual( Root.accessors[SubclassProxyProxy], - SubclassAccessor(('subclass',), SubclassProxyProxy, 'subclass') + SubclassAccessor(("subclass",), SubclassProxyProxy, "subclass"), + ) + self.assertEqual( + Subclass.accessors[SubclassProxy], SubclassAccessor((), SubclassProxy, "") + ) + self.assertEqual( + Subclass.accessors[SubclassProxyProxy], + SubclassAccessor((), SubclassProxyProxy, ""), + ) + self.assertEqual( + SubclassProxy.accessors[SubclassProxyProxy], + SubclassAccessor((), SubclassProxyProxy, ""), ) - self.assertEqual(Subclass.accessors[SubclassProxy], SubclassAccessor((), SubclassProxy, '')) - self.assertEqual(Subclass.accessors[SubclassProxyProxy], SubclassAccessor((), SubclassProxyProxy, '')) - self.assertEqual(SubclassProxy.accessors[SubclassProxyProxy], SubclassAccessor((), SubclassProxyProxy, '')) diff --git a/tests/test_related.py b/tests/test_related.py index 983b62c..51e41d5 100644 --- a/tests/test_related.py +++ b/tests/test_related.py @@ -9,8 +9,8 @@ def test_select_subclasses(self): `select_subclasses` on a related manager. """ zoo = Zoo.objects.create() - yeti = Mammal.objects.create(name='Yeti') - pepe = Monkey.objects.create(name='Pepe') + yeti = Mammal.objects.create(name="Yeti") + pepe = Monkey.objects.create(name="Pepe") zoo.animals.add(yeti) zoo_animals = zoo.animals.select_subclasses() self.assertIn(yeti, zoo_animals) From 58284dfcc3cf35b6239edf2953c7265724fd7ad3 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sun, 30 Mar 2025 22:12:08 -0400 Subject: [PATCH 2/2] Drop support for Django < 4.2 and Python < 3.9. --- .github/workflows/release.yml | 6 ++-- .github/workflows/test.yml | 9 +++--- setup.py | 55 ++++++++++++++++---------------- tox.ini | 60 +++++++++++++++++++++-------------- 4 files changed, 71 insertions(+), 59 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ad6ed55..0ae7ae4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.9 - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69fb84f..ebc278f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,17 +5,18 @@ on: [push, pull_request] jobs: test: runs-on: ubuntu-latest + strategy: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -25,7 +26,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: diff --git a/setup.py b/setup.py index 5a66068..7e95bc8 100755 --- a/setup.py +++ b/setup.py @@ -3,40 +3,39 @@ from polymodels import __version__ -github_url = 'https://github.com/charettes/django-polymodels' -long_desc = open('README.rst').read() +github_url = "https://github.com/charettes/django-polymodels" +long_desc = open("README.rst").read() setup( - name='django-polymodels', + name="django-polymodels", version=__version__, - description='Polymorphic models implementation for django', + description="Polymorphic models implementation for django", long_description=long_desc, - long_description_content_type='text/x-rst', + long_description_content_type="text/x-rst", url=github_url, - author='Simon Charette', - author_email='charette.s@gmail.com', - install_requires=( - 'Django>=3.2', - ), - packages=find_packages(exclude=['tests', 'tests.*']), + author="Simon Charette", + author_email="charette.s@gmail.com", + install_requires=("Django>=4.2",), + packages=find_packages(exclude=["tests", "tests.*"]), include_package_data=True, - license='MIT License', + license="MIT License", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 4.1', - 'Framework :: Django :: 4.2', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Software Development :: Libraries :: Python Modules' + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", ], ) diff --git a/tox.ini b/tox.ini index 64b6274..7d01e33 100644 --- a/tox.ini +++ b/tox.ini @@ -2,60 +2,72 @@ skipsdist = true args_are_paths = false envlist = - flake8, - isort, - pypi, - py37-3.2, - py{38,39}-{3.2,4.0,4.1,4.2}, - py310-{3.2,4.0,4.1,4.2,main} - py311-{3.2,4.0,4.1,4.2,main} + black + flake8 + isort + pypi + py39-4.2 + py310-{4.2,5.0,5.1,5.2,main} + py{311,312}-{4.2,5.0,5.1,5.2,main} + py313-{5.1,5.2,main} [gh-actions] python = - 3.7: py37 - 3.8: py38 - 3.9: py39 + 3.9: py39, black, flake8, isort 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 [testenv] basepython = - py37: python3.7 - py38: python3.8 py39: python3.9 py310: python3.10 py311: python3.11 + py312: python3.12 + py313: python3.13 usedevelop = true +setenv = + DJANGO_SETTINGS_MODULE=tests.settings +passenv = + GITHUB_* + DB_* commands = - {envpython} -R -Wonce {envbindir}/coverage run -a -m django test -v2 --settings=tests.settings {posargs} + {envpython} -R -Wonce {envbindir}/coverage run -a -m django test -v2 {posargs} coverage report deps = coverage - 3.2: Django>=3.2a1,<4 - 4.0: Django>=4.0,<4.1 - 4.1: Django>=4.1,<4.2 4.2: Django>=4.2,<5 + 5.0: Django>=5,<5.1 + 5.1: Django>=5.1,<5.2 + 5.2: Django>=5.2a1,<6.0 main: https://github.com/django/django/archive/main.tar.gz -passenv = - GITHUB_* +ignore_outcome = + main: true + +[testenv:black] +usedevelop = false +basepython = python3.9 +commands = black --check polymodels tests +deps = black [testenv:flake8] usedevelop = false -basepython = python3.7 +basepython = python3.9 commands = flake8 deps = flake8 [testenv:isort] usedevelop = false -basepython = python3.7 -commands = isort --recursive --check-only --diff polymodels tests +basepython = python3.9 +commands = isort --check-only --diff polymodels tests deps = isort - Django + Django>=4.2 [testenv:pypi] usedevelop = false -basepython = python3.7 +basepython = python3.9 commands = python setup.py sdist --format=gztar bdist_wheel twine check dist/* @@ -63,4 +75,4 @@ deps = pip setuptools twine - wheel + wheel \ No newline at end of file