Skip to content

Commit

Permalink
Release v2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Schindler committed Aug 6, 2019
1 parent 34c38f4 commit 4f2b65e
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 174 deletions.
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [Unreleased]
### Fixed
### Added
## 2.0.0 - 2019-08-06
## Added
* Added `FlexQuery.call_bound()` method as a hook to preprocess custom arguments.
### Changed
### Deprecated
### Removed
* Significantly simplified API:
It's straightforward to build a Q function that produces a sub-query, hence
`FlexQuery` types can now only be created from Q functions, simplifying both code
and unittests a lot.
* Custom functions now get the base QuerySet as first argument.


## 1.0.1 - 2019-07-29
Expand Down
2 changes: 1 addition & 1 deletion django_flexquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
__all__ = ["FlexQuery", "Manager", "Q", "QuerySet"]


__version__ = "1.0.1"
__version__ = "2.0.0"
105 changes: 25 additions & 80 deletions django_flexquery/flexquery.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,22 @@
from django.db.models import Manager as _Manager, QuerySet as _QuerySet
from django.utils.decorators import classonlymethod

from .q import Q


class FlexQueryType(type):
"""
Custom metaclass for FlexQuery that implements the descriptor pattern.
"""

def __get__(cls, instance, owner):
if issubclass(cls, InitializedFlexQueryMixin) and isinstance(
instance, (_Manager, _QuerySet)
):
if cls.func is not None and isinstance(instance, (_Manager, _QuerySet)):
# Return a new FlexQuery instance bound to the holding object
return cls(instance)
# Otherwise, preserve the FlexQuery type
return cls

def __repr__(cls):
if cls.q_func is not None or cls.qs_func is not None:
return "<type %s %r>" % (
cls.__name__,
(cls.qs_func if cls.q_func is None else cls.q_func).__name__,
)
if cls.func is not None:
return "<type %s %r>" % (cls.__name__, cls.func.__name__)
return super().__repr__()


Expand All @@ -48,35 +41,28 @@ class FlexQuery(metaclass=FlexQueryType):
.. automethod:: __call__
"""

# pylint: disable=not-callable

# Will be set when creating sub-types
q_func = None
qs_func = None
# Will be set on sub-types
func = None

def __call__(self, *args, **kwargs):
"""Filters the base QuerySet using the provided function.
All arguments are passed through to the FlexQuery's custom function.
"""Filters the base QuerySet using the provided function, relaying arguments.
:returns QuerySet:
"""
if self.q_func is None:
return self.qs_func(self.base.all(), *args, **kwargs)
return self.base.filter(self.q_func(*args, **kwargs))
return self.base.filter(self.call_bound(*args, **kwargs))

def __init__(self, base):
"""Binds a FlexQuery instance to the given base to perform its filtering on.
:param base: instance to use as base for filtering
:type base: Manager, QuerySet
:type base: Manager | QuerySet
:raises ImproperlyConfigured:
if this FlexQuery type wasn't created by one of the from_*() classmethods
if this FlexQuery type wasn't created by the from_q() classmethod
:raises TypeError: if base is of unsupported type
"""
if self.q_func is None and self.qs_func is None:
if self.func is None:
raise ImproperlyConfigured(
"Use one of the from_*() classmethods to create a FlexQuery type."
"Use the from_q() classmethod to create a FlexQuery sub-type."
)
if not isinstance(base, (_Manager, _QuerySet)):
raise TypeError(
Expand All @@ -88,78 +74,37 @@ def __init__(self, base):
def __repr__(self):
return "<%s %r, bound to %r>" % (
self.__class__.__name__,
(self.qs_func if self.q_func is None else self.q_func).__name__,
self.func.__name__,
self.base,
)

def as_q(self, *args, **kwargs):
"""Returns a Q object representing the custom filters.
"""Returns the result of the configured function, relaying arguments.
:returns Q:
"""
return self.call_bound(*args, **kwargs)

The Q object is either retrieved from the configured Q function or, if a
QuerySet filtering function was provided instead, will contain a sub-query
ensuring the object is in the base QuerySet filtered by that function.
def call_bound(self, *args, **kwargs):
"""Calls the provided Q function with self.base.all() as first argument.
All arguments are passed through to the FlexQuery's custom function.
This may be overloaded if arguments need to be preprocessed in some way
before being passed to the custom function.
:returns Q:
"""
if self.q_func is None:
return Q(pk__in=self.qs_func(self.base.all(), *args, **kwargs))
return self.q_func(*args, **kwargs)
# pylint: disable=not-callable
return self.func(self.base.all(), *args, **kwargs)

@classonlymethod
def from_q(cls, func):
"""Creates a FlexQuery type from a Q function.
:param func: callable returning a Q object
:type func: callable
:returns FlexQueryType:
"""
return type(cls)(
"%sFromQFunction" % cls.__name__,
(InitializedFlexQueryMixin, cls),
{"q_func": staticmethod(func)},
)

@classonlymethod
def from_queryset(cls, func):
"""Creates a FlexQuery type from a queryset filtering function.
:param func:
callable taking a QuerySet as first positional argument, applying some
filtering on it and returning it back
:param func: callable taking a base QuerySet and returning a Q object
:type func: callable
:returns FlexQueryType:
"""
return type(cls)(
"%sFromQuerySetFunction" % cls.__name__,
(InitializedFlexQueryMixin, cls),
{"qs_func": staticmethod(func)},
)


class InitializedFlexQueryMixin:
"""
Mixin that prevents further usage of the from_*() classmethods on sub-types
of FlexQuery.
"""

# pylint: disable=missing-docstring,unused-argument

@classonlymethod
def _already_initialized(cls):
raise NotImplementedError(
"%r was already initialized with a function. Use FlexQuery.from_*() "
"directly to derive new FlexQuery types." % cls
)

@classonlymethod
def from_q(cls, func):
cls._already_initialized()

@classonlymethod
def from_queryset(cls, func):
cls._already_initialized()
return type(cls)(cls.__name__, (cls,), {"func": staticmethod(func)})


class Manager(_Manager):
Expand Down
48 changes: 17 additions & 31 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,25 @@ Nothing more to it. The real power comes when using these ``Q`` objects with
The ``FlexQuery`` Class
-----------------------

The ``FlexQuery`` class provides two classmethods which can be used as decorators
for functions declared on a custom ``Manager`` or ``QuerySet``::
The ``FlexQuery`` class provides a decorator for functions declared on a custom
``Manager`` or ``QuerySet``::

from django_flexquery import FlexQuery, Manager, Q, QuerySet

# It's crucial to inherit from the QuerySet class of django_flexquery, because
# the FlexQuery's wouldn't make it over to a Manager derived using as_manager()
# with the stock Django implementation. That's the only difference however.
class FruitQuerySet(QuerySet):
# Declare a function that filters a QuerySet in some way.
@FlexQuery.from_queryset
def small(queryset):
return queryset.filter(size__lt=10)

# Or declare a function that generates a Q object.
# Declare a function that generates a Q object.
# base is a copy of the base QuerySet instance. It's not needed most of
# the time unless you want to embed a sub-query based on the current QuerySet
# into the Q object.
@FlexQuery.from_q
def large():
def large(base):
return Q(size__gte=10)

``FruitQuerySet.small`` and ``FruitQuerySet.large`` are now sub-types of ``FlexQuery``
encapsulating the respective custom function.
``FruitQuerySet.large`` now is a sub-type of ``FlexQuery`` encapsulating the custom
function.

You can then derive a ``Manager`` from ``FruitQuerySet`` in two ways, using the
known Django API::
Expand All @@ -68,8 +66,6 @@ known Django API::
When we assume such a ``Manager`` being the default manager of a ``Fruit`` model
with a ``size`` field, we can now perform the following queries::

Fruit.objects.small()
Fruit.objects.filter(Fruit.objects.small.as_q())
Fruit.objects.large()
Fruit.objects.filter(Fruit.objects.large.as_q())

Expand All @@ -78,16 +74,15 @@ the ``FlexQuery`` type whenever it is accessed as class attribute of a ``Manager
or ``QuerySet`` object. The resulting ``FlexQuery`` instance will be tied to its
owner and use that for all its filtering.

A ``FlexQuery`` instance is directly callable (``Fruit.objects.small()``), which just
executes our custom filter function on the base ``QuerySet``. This is a well-known
usage pattern you might have come across often when writing custom Django model
managers or querysets.
A ``FlexQuery`` instance is directly callable (``Fruit.objects.large()``), which just
applies the filters returned by our custom Q function to the base ``QuerySet``. This
is a well-known usage pattern you might have come across often when writing custom
Django model managers or querysets.

However, ``FlexQuery`` also comes with an ``as_q()`` method,
which generates a ``Q`` object incorporating our custom filtering
(``Fruit.objects.filter(Fruit.objects.small.as_q())``), even though we just provided
a function that filters an existing ``QuerySet``. The ``FlexQuery`` can mediate
between these two and deliver what you need in your particular situation.
However, ``FlexQuery`` also comes with an ``as_q()`` method, which lets you access the
``Q`` object directly (``Fruit.objects.filter(Fruit.objects.large.as_q())``). The
``FlexQuery`` can mediate between these two and deliver what you need in your
particular situation.


Conversion Costs
Expand All @@ -98,15 +93,6 @@ function is a cheap operation. The ``Q`` object generated by your custom functio
simply applied to the base using ``QuerySet.filter()``, resulting in a new ``QuerySet``
you may either evaluate straight away or use to create a sub-query.

The other direction, wrapping the logic performed by a custom ``QuerySet`` filtering
function in ``a`` Q object, is however not so simple. Since the ``FlexQuery`` can't
know what your filter function does to the ``QuerySet``, it creates a ``Q`` object
of the form ``Q(pk__in=your_queryset_func(...))``, which results in a sub-query
being generated for sure.

As you can see, you are more flexible when creating the ``FlexQuery`` type from a
``Q`` function instead of a ``QuerySet`` filtering function whenever possible.


Why do I Need This?
-------------------
Expand Down

0 comments on commit 4f2b65e

Please sign in to comment.