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)