Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/base.py: 67%
83 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2PolymorphicModel Meta Class
3"""
4import inspect
5import os
6import sys
7import warnings
9import django
10from django.core.exceptions import ImproperlyConfigured
11from django.db import models
12from django.db.models.base import ModelBase
13from django.db.models.manager import ManagerDescriptor
15from .managers import PolymorphicManager
16from .query import PolymorphicQuerySet
18# PolymorphicQuerySet Q objects (and filter()) support these additional key words.
19# These are forbidden as field names (a descriptive exception is raised)
20POLYMORPHIC_SPECIAL_Q_KWORDS = ["instance_of", "not_instance_of"]
22DUMPDATA_COMMAND = os.path.join("django", "core", "management", "commands", "dumpdata.py")
25class ManagerInheritanceWarning(RuntimeWarning):
26 pass
29###################################################################################
30# PolymorphicModel meta class
33class PolymorphicModelBase(ModelBase):
34 """
35 Manager inheritance is a pretty complex topic which may need
36 more thought regarding how this should be handled for polymorphic
37 models.
39 In any case, we probably should propagate 'objects' and 'base_objects'
40 from PolymorphicModel to every subclass. We also want to somehow
41 inherit/propagate _default_manager as well, as it needs to be polymorphic.
43 The current implementation below is an experiment to solve this
44 problem with a very simplistic approach: We unconditionally
45 inherit/propagate any and all managers (using _copy_to_model),
46 as long as they are defined on polymorphic models
47 (the others are left alone).
49 Like Django ModelBase, we special-case _default_manager:
50 if there are any user-defined managers, it is set to the first of these.
52 We also require that _default_manager as well as any user defined
53 polymorphic managers produce querysets that are derived from
54 PolymorphicQuerySet.
55 """
57 def __new__(self, model_name, bases, attrs, **kwargs):
58 # print; print '###', model_name, '- bases:', bases
60 # Workaround compatibility issue with six.with_metaclass() and custom Django model metaclasses:
61 if not attrs and model_name == "NewBase": 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true
62 return super().__new__(self, model_name, bases, attrs, **kwargs)
64 # Make sure that manager_inheritance_from_future is set, since django-polymorphic 1.x already
65 # simulated that behavior on the polymorphic manager to all subclasses behave like polymorphics
66 if django.VERSION < (2, 0): 66 ↛ 67line 66 didn't jump to line 67, because the condition on line 66 was never true
67 if "Meta" in attrs:
68 if not hasattr(attrs["Meta"], "manager_inheritance_from_future"):
69 attrs["Meta"].manager_inheritance_from_future = True
70 else:
71 attrs["Meta"] = type("Meta", (object,), {"manager_inheritance_from_future": True})
73 # create new model
74 new_class = self.call_superclass_new_method(model_name, bases, attrs, **kwargs)
76 # check if the model fields are all allowed
77 self.validate_model_fields(new_class)
79 # validate resulting default manager
80 if not new_class._meta.abstract and not new_class._meta.swapped:
81 self.validate_model_manager(new_class.objects, model_name, "objects")
83 # for __init__ function of this class (monkeypatching inheritance accessors)
84 new_class.polymorphic_super_sub_accessors_replaced = False
86 # determine the name of the primary key field and store it into the class variable
87 # polymorphic_primary_key_name (it is needed by query.py)
88 for f in new_class._meta.fields:
89 if f.primary_key and type(f) != models.OneToOneField:
90 new_class.polymorphic_primary_key_name = f.name
91 break
93 return new_class
95 @classmethod
96 def call_superclass_new_method(self, model_name, bases, attrs, **kwargs):
97 """call __new__ method of super class and return the newly created class.
98 Also work around a limitation in Django's ModelBase."""
99 # There seems to be a general limitation in Django's app_label handling
100 # regarding abstract models (in ModelBase). See issue 1 on github - TODO: propose patch for Django
101 # We run into this problem if polymorphic.py is located in a top-level directory
102 # which is directly in the python path. To work around this we temporarily set
103 # app_label here for PolymorphicModel.
104 meta = attrs.get("Meta", None)
105 do_app_label_workaround = (
106 meta
107 and attrs["__module__"] == "polymorphic"
108 and model_name == "PolymorphicModel"
109 and getattr(meta, "app_label", None) is None
110 )
112 if do_app_label_workaround: 112 ↛ 113line 112 didn't jump to line 113, because the condition on line 112 was never true
113 meta.app_label = "poly_dummy_app_label"
114 new_class = super().__new__(self, model_name, bases, attrs, **kwargs)
115 if do_app_label_workaround: 115 ↛ 116line 115 didn't jump to line 116, because the condition on line 115 was never true
116 del meta.app_label
117 return new_class
119 @classmethod
120 def validate_model_fields(self, new_class):
121 "check if all fields names are allowed (i.e. not in POLYMORPHIC_SPECIAL_Q_KWORDS)"
122 for f in new_class._meta.fields:
123 if f.name in POLYMORPHIC_SPECIAL_Q_KWORDS: 123 ↛ 124line 123 didn't jump to line 124, because the condition on line 123 was never true
124 e = 'PolymorphicModel: "%s" - field name "%s" is not allowed in polymorphic models'
125 raise AssertionError(e % (new_class.__name__, f.name))
127 @classmethod
128 def validate_model_manager(self, manager, model_name, manager_name):
129 """check if the manager is derived from PolymorphicManager
130 and its querysets from PolymorphicQuerySet - throw AssertionError if not"""
132 if not issubclass(type(manager), PolymorphicManager): 132 ↛ 133line 132 didn't jump to line 133, because the condition on line 132 was never true
133 if django.VERSION < (2, 0):
134 extra = "\nConsider using Meta.manager_inheritance_from_future = True for Django 1.x projects"
135 else:
136 extra = ""
137 e = (
138 'PolymorphicModel: "{0}.{1}" manager is of type "{2}", but must be a subclass of'
139 " PolymorphicManager.{extra} to support retrieving subclasses".format(
140 model_name, manager_name, type(manager).__name__, extra=extra
141 )
142 )
143 warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
144 return manager
146 if not getattr(manager, "queryset_class", None) or not issubclass( 146 ↛ 149line 146 didn't jump to line 149
147 manager.queryset_class, PolymorphicQuerySet
148 ):
149 e = (
150 'PolymorphicModel: "{}.{}" has been instantiated with a queryset class '
151 "which is not a subclass of PolymorphicQuerySet (which is required)".format(
152 model_name, manager_name
153 )
154 )
155 warnings.warn(e, ManagerInheritanceWarning, stacklevel=3)
156 return manager
158 @property
159 def base_objects(self):
160 warnings.warn(
161 "Using PolymorphicModel.base_objects is deprecated.\n"
162 "Use {}.objects.non_polymorphic() instead.".format(self.__class__.__name__),
163 DeprecationWarning,
164 stacklevel=2,
165 )
166 return self._base_objects
168 @property
169 def _base_objects(self):
170 # Create a manager so the API works as expected. Just don't register it
171 # anymore in the Model Meta, so it doesn't substitute our polymorphic
172 # manager as default manager for the third level of inheritance when
173 # that third level doesn't define a manager at all.
174 manager = models.Manager()
175 manager.name = "base_objects"
176 manager.model = self
177 return manager
179 @property
180 def _default_manager(self):
181 if len(sys.argv) > 1 and sys.argv[1] == "dumpdata": 181 ↛ 191line 181 didn't jump to line 191, because the condition on line 181 was never true
182 # TODO: investigate Django how this can be avoided
183 # hack: a small patch to Django would be a better solution.
184 # Django's management command 'dumpdata' relies on non-polymorphic
185 # behaviour of the _default_manager. Therefore, we catch any access to _default_manager
186 # here and return the non-polymorphic default manager instead if we are called from 'dumpdata.py'
187 # Otherwise, the base objects will be upcasted to polymorphic models, and be outputted as such.
188 # (non-polymorphic default manager is 'base_objects' for polymorphic models).
189 # This way we don't need to patch django.core.management.commands.dumpdata
190 # for all supported Django versions.
191 frm = inspect.stack()[1] # frm[1] is caller file name, frm[3] is caller function name
192 if DUMPDATA_COMMAND in frm[1]:
193 return self._base_objects
195 manager = super()._default_manager
196 if not isinstance(manager, PolymorphicManager): 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true
197 warnings.warn(
198 "{}._default_manager is not a PolymorphicManager".format(self.__class__.__name__),
199 ManagerInheritanceWarning,
200 )
202 return manager