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

1""" 

2PolymorphicModel Meta Class 

3""" 

4import inspect 

5import os 

6import sys 

7import warnings 

8 

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 

14 

15from .managers import PolymorphicManager 

16from .query import PolymorphicQuerySet 

17 

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"] 

21 

22DUMPDATA_COMMAND = os.path.join("django", "core", "management", "commands", "dumpdata.py") 

23 

24 

25class ManagerInheritanceWarning(RuntimeWarning): 

26 pass 

27 

28 

29################################################################################### 

30# PolymorphicModel meta class 

31 

32 

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. 

38 

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. 

42 

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). 

48 

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. 

51 

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 """ 

56 

57 def __new__(self, model_name, bases, attrs, **kwargs): 

58 # print; print '###', model_name, '- bases:', bases 

59 

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) 

63 

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}) 

72 

73 # create new model 

74 new_class = self.call_superclass_new_method(model_name, bases, attrs, **kwargs) 

75 

76 # check if the model fields are all allowed 

77 self.validate_model_fields(new_class) 

78 

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") 

82 

83 # for __init__ function of this class (monkeypatching inheritance accessors) 

84 new_class.polymorphic_super_sub_accessors_replaced = False 

85 

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 

92 

93 return new_class 

94 

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 ) 

111 

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 

118 

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)) 

126 

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""" 

131 

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 

145 

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 

157 

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 

167 

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 

178 

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 

194 

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 ) 

201 

202 return manager