Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions polymodels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = (1, 8, 0, 'final', 0)
VERSION = (1, 8, 0, "final", 0)

__version__ = '1.8.0'
__version__ = "1.8.0"
92 changes: 54 additions & 38 deletions polymodels/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,15 +22,15 @@ 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()
elif isinstance(limit_choices_to, dict):
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):
Expand All @@ -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()
)
Expand All @@ -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)

Expand All @@ -79,32 +80,35 @@ 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):
self.polymorphic_type = polymorphic_type
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)

Expand All @@ -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
2 changes: 1 addition & 1 deletion polymodels/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
27 changes: 15 additions & 12 deletions polymodels/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -31,19 +33,15 @@ 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:
# Avoid collecting ourself and proxy subclasses
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():
Expand All @@ -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
Expand All @@ -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)

Expand Down
Loading
Loading