def test_extract_function(self): def func(): pass cls_method = classmethod(func) prop = queryable_property() # Test both regular functions as well as class methods assert prop._extract_function(func) is func assert prop._extract_function(cls_method) is func
def test_updater(self, old_updater): original = queryable_property() original.get_update_kwargs = old_updater def func(): pass clone = self.decorate_function(classmethod(func), original.updater) self.assert_cloned_property(original, clone, {'get_update_kwargs': func})
class ApplicationWithDecoratorBasedProperties(Application): categories = models.ManyToManyField(CategoryWithDecoratorBasedProperties, related_name='applications') objects = QueryablePropertiesManager() class Meta: verbose_name = 'Application' @queryable_property def highest_version(self): try: return self.versions.order_by('-major', '-minor', '-patch')[0].version except IndexError: return None @highest_version.annotater @classmethod def highest_version(cls): queryset = VersionWithDecoratorBasedProperties.objects.select_properties( 'version') queryset = queryset.filter(application=models.OuterRef('pk')).order_by( '-major', '-minor', '-patch') return models.Subquery(queryset.values('version')[:1], output_field=models.CharField()) @queryable_property(annotation_based=True) @classmethod def version_count(cls): return models.Count('versions') @queryable_property(annotation_based=True) @classmethod def support_start_date(cls): return models.Min('versions__supported_from') @queryable_property def major_sum(self): return self.versions.aggregate( major_sum=models.Sum('major'))['major_sum'] @major_sum.annotater @classmethod def major_sum(cls): return models.Sum('versions__major') lowered_version_changes = queryable_property() @lowered_version_changes.annotater @classmethod def lowered_version_changes(cls): from django.db.models.functions import Lower return Lower('versions__changes_or_default')
def test_lookup_boolean_exception(self): prop = queryable_property() def func(): pass with pytest.raises(QueryablePropertyError): self.decorate_function(func, prop.filter, { 'lookups': ['lt', 'lte'], 'boolean': True })
def test_remaining_lookups_via_parent_exception(self, value, lookups, has_mappings, expectation): prop = queryable_property() if has_mappings: prop.lookup_mappings = {} def func(): pass with expectation: self.decorate_function(func, prop.filter, { 'lookups': lookups, 'remaining_lookups_via_parent': value })
def test_setter(self, old_setter, kwargs): original = queryable_property() original.set_value = old_setter def func(): pass decorator_kwargs = kwargs and dict(kwargs) if decorator_kwargs: decorator_kwargs['cache_behavior'] = decorator_kwargs.pop( 'setter_cache_behavior') clone = self.decorate_function(func, original.setter, decorator_kwargs) self.assert_cloned_property(original, clone, dict(kwargs or {}, set_value=func))
def test_filter(self, initial_values, decorator_kwargs, expected_requires_annotation): original = queryable_property() original.__dict__.update(initial_values) def func(): pass clone = self.decorate_function(func, original.filter, decorator_kwargs) assert not isinstance(clone, LookupFilterMixin) self.assert_cloned_property( original, clone, { 'get_filter': func, 'filter_requires_annotation': expected_requires_annotation })
def test_annotater(self, initial_values, expected_requires_annotation): original = queryable_property() original.__dict__.update(initial_values) def func(): pass prop = self.decorate_function(func, original.annotater) assert isinstance(prop, AnnotationMixin) self.assert_cloned_property( original, prop, { 'get_annotation': func, 'filter_requires_annotation': expected_requires_annotation, 'get_filter': initial_values.get( 'get_filter', six.create_bound_method( six.get_unbound_function(AnnotationMixin.get_filter), prop)), })
def test_getter(self, old_getter, init_kwargs, old_docstring, new_docstring, kwargs): original = queryable_property(old_getter, **init_kwargs) if old_docstring is not None: original.__doc__ = old_docstring def func(): pass func.__doc__ = new_docstring clone = self.decorate_function(func, original.getter, kwargs) changed_attrs = dict(kwargs or {}, get_value=func, __doc__=new_docstring or old_docstring) if init_kwargs.get('annotation_based', False): changed_attrs['get_filter'] = six.create_bound_method( six.get_unbound_function(AnnotationMixin.get_filter), clone) changed_attrs['get_annotation'] = six.create_bound_method( six.get_unbound_function(AnnotationMixin.get_annotation), clone) self.assert_cloned_property(original, clone, changed_attrs)
def test_boolean_filter(self): original = queryable_property() original.model = Category original.name = 'test_property' def func(cls): return Q(some_field=5) clone = self.decorate_function(func, original.filter, {'boolean': True}) assert isinstance(clone, LookupFilterMixin) positive_condition = clone.get_filter(None, 'exact', True) negative_condition = clone.get_filter(None, 'exact', False) if DJANGO_VERSION < (1, 6): # In very old Django versions, negating adds another layer. negative_condition = negative_condition.children[0] assert positive_condition.children == negative_condition.children == [ ('some_field', 5) ] assert positive_condition.negated is False assert negative_condition.negated is True with pytest.raises(QueryablePropertyError): clone.get_filter(None, 'lt', None)
def test_lookup_filters(self): original = queryable_property() original.model = Category original.name = 'test_property' get_filter_func = six.get_unbound_function( LookupFilterMixin.get_filter) def func1(cls, lookup, value): return 1 clone1 = self.decorate_function(func1, original.filter, {'lookups': ['lt', 'gt']}) assert isinstance(clone1, LookupFilterMixin) self.assert_cloned_property( original, clone1, { 'lookup_mappings': { 'lt': func1, 'gt': func1 }, 'get_filter': six.create_bound_method(get_filter_func, clone1), }) assert clone1.get_filter(None, 'lt', None) == 1 assert clone1.get_filter(None, 'gt', None) == 1 with pytest.raises(QueryablePropertyError): clone1.get_filter(None, 'in', None) def func2(cls, lookup, value): return 2 clone2 = self.decorate_function( func2, clone1.filter, { 'lookups': ['lt', 'lte'], 'requires_annotation': True, 'remaining_lookups_via_parent': True, }) assert isinstance(clone2, LookupFilterMixin) self.assert_cloned_property( clone1, clone2, { 'lookup_mappings': { 'lt': func2, 'lte': func2, 'gt': func1 }, 'get_filter': six.create_bound_method(get_filter_func, clone2), 'filter_requires_annotation': True, # Should be overridable on every call. 'remaining_lookups_via_parent': True, # Should be overridable on every call. }) assert clone2.get_filter(None, 'lt', None) == 2 assert clone2.get_filter(None, 'lte', None) == 2 assert clone2.get_filter(None, 'gt', None) == 1 with pytest.raises( TypeError ): # super().get_filter will be None, which isn't callable clone2.get_filter(None, 'in', None) def func3(cls, lookup, value): return 3 clone3 = self.decorate_function( func3, clone2.filter, { 'lookups': ['exact'], 'requires_annotation': False, 'remaining_lookups_via_parent': False, }) assert isinstance(clone3, LookupFilterMixin) self.assert_cloned_property( clone2, clone3, { 'lookup_mappings': { 'lt': func2, 'lte': func2, 'gt': func1, 'exact': func3 }, 'get_filter': six.create_bound_method(get_filter_func, clone3), 'filter_requires_annotation': False, # Should be overridable on every call. 'remaining_lookups_via_parent': False, # Should be overridable on every call. }) assert clone3.get_filter(None, 'lt', None) == 2 assert clone3.get_filter(None, 'lte', None) == 2 assert clone3.get_filter(None, 'gt', None) == 1 assert clone3.get_filter(None, 'exact', None) == 3 with pytest.raises(QueryablePropertyError): clone3.get_filter(None, 'in', None)
def test_clone(self, init_kwargs, clone_kwargs): prop = queryable_property(**init_kwargs) clone = prop._clone(**clone_kwargs) self.assert_cloned_property(prop, clone, clone_kwargs)