-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Robert Schindler
committed
Jul 13, 2019
0 parents
commit e9ba959
Showing
13 changed files
with
541 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
__pycache__ | ||
*.pyc | ||
|
||
_build | ||
|
||
*.swp | ||
*.swo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
version: 2 | ||
|
||
sphinx: | ||
configuration: docs/conf.py | ||
|
||
# Optionally build your docs in additional formats such as PDF and ePub | ||
formats: all | ||
|
||
python: | ||
version: 3.7 | ||
install: | ||
- requirements: docs/requirements.txt | ||
- method: pip | ||
path: . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
django-flexquery | ||
================ | ||
|
||
This library aims to provide a new way of declaring reusable QuerySet filtering | ||
logic in your Django project, incorporating the DRY principle and maximizing user | ||
experience and performance. | ||
|
||
Its strengths are, among others: | ||
|
||
* Easy to learn in minutes | ||
* Cleanly integrates with Django's ORM | ||
* Small code footprint, less bugs - ~150 lines of code (LoC) | ||
* Fully documented code, formatted using the excellent `Black Code Formatter | ||
<https://github.com/python/black>`_. | ||
|
||
See the `documentation at Read The Docs <https://django-flexquery.readthedocs.org>`_ | ||
to convince yourself. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
try: | ||
from .flexquery import Manager, Q, FlexQuery, QuerySet | ||
except ImportError: | ||
# Django is missing, just provide version | ||
__all__ = [] | ||
else: | ||
__all__ = ["FlexQuery", "Manager", "Q", "QuerySet"] | ||
|
||
|
||
__version__ = "0.0.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
""" | ||
This module provides a convenient way to declare custom filtering logic with Django's | ||
model managers in a reusable fashion using Q objects. | ||
""" | ||
|
||
import inspect | ||
|
||
from django.core.exceptions import ImproperlyConfigured | ||
from django.db.models import Manager as _Manager, Q as _Q, QuerySet as _QuerySet | ||
from django.db.models.constants import LOOKUP_SEP | ||
from django.utils.decorators import classonlymethod | ||
|
||
|
||
class FlexQueryType(type): | ||
""" | ||
Custom metaclass for FlexQuery that implements the descriptor pattern. | ||
""" | ||
|
||
def __get__(self, instance, owner): | ||
if isinstance(instance, (_Manager, _QuerySet)): | ||
# Return a new FlexQuery instance bound to the holding object | ||
return self(instance) | ||
# Otherwise, preserve the unbound FlexQuery type | ||
return self | ||
|
||
def __repr__(self): | ||
return "<Unbound %s %r>" % ( | ||
self.__name__, | ||
(self.filter_func if self.q_func is None else self.q_func).__name__, | ||
) | ||
|
||
|
||
class FlexQuery(metaclass=FlexQueryType): | ||
""" | ||
Flexibly provides model-specific query constraints as an attribute of Manager | ||
or QuerySet objects. | ||
When a sub-type of FlexQuery is accessed as class attribute of a Manager or | ||
QuerySet object, its metaclass, which is implemented as a descriptor, will | ||
automatically initialize and return an instance of the FlexQuery type bound to | ||
the holding Manager or QuerySet. | ||
.. automethod:: __call__ | ||
.. automethod:: __init__ | ||
""" | ||
|
||
# Will be set when creating sub-types | ||
filter_func = None | ||
q_func = None | ||
|
||
def __call__(self, *args, **kwargs): | ||
"""Filters the base QuerySet with the provided filter function or Q object. | ||
All arguments are passed through to the FlexQuery's custom function. | ||
:returns QuerySet: | ||
""" | ||
if self.filter_func is not None: | ||
return self.filter_func(self.base.all(), *args, **kwargs) | ||
return self.base.filter(self.q_func(*args, **kwargs)) | ||
|
||
def __init__(self, base): | ||
"""Initializes a FlexQuery that will perform its filtering on the given base. | ||
:param base: Instance to use as base for filtering. | ||
:type base: Manager, QuerySet | ||
:raises ImproperlyConfigured: | ||
If this FlexQuery type wasn't created by one of the from_*() classmethods. | ||
:raises TypeError: If base is of unsupported type. | ||
""" | ||
if self.filter_func is None and self.q_func is None: | ||
raise ImproperlyConfigured( | ||
"Use one of the from_*() classmethods to create a FlexQuery type." | ||
) | ||
if not isinstance(base, (_Manager, _QuerySet)): | ||
raise TypeError( | ||
"Can only bind %s to a Manager or QuerySet, but not to %r." | ||
% (self.__class__.__name__, base) | ||
) | ||
self.base = base | ||
|
||
def __repr__(self): | ||
return "<Bound %s %r, base=%r>" % ( | ||
self.__class__.__name__, | ||
(self.filter_func if self.q_func is None else self.q_func).__name__, | ||
self.base, | ||
) | ||
|
||
def as_q(self, *args, **kwargs): | ||
"""Returns a Q object representing the custom filters. | ||
The Q object is either retrieved from the configured Q function or, if a | ||
filter function was provided instead, will contain a sub-query ensuring the | ||
object is in the base QuerySet filtered by that filter function. | ||
All arguments are passed through to the FlexQuery's custom function. | ||
:returns Q: | ||
""" | ||
if self.filter_func is not None: | ||
return Q(pk__in=self.filter_func(self.base.all(), *args, **kwargs)) | ||
return self.q_func(*args, **kwargs) | ||
|
||
@classonlymethod | ||
def from_filter(cls, func): | ||
"""Creates a FlexQuery type from a filter function. | ||
:param func: | ||
Callable taking a QuerySet as first positional argument, applying some | ||
filtering on it and returning it back. | ||
:type func: callable | ||
:returns type: | ||
""" | ||
return type(cls)( | ||
"%sFromFilterFunction" % cls.__name__, | ||
(cls,), | ||
{"filter_func": staticmethod(func)}, | ||
) | ||
|
||
@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 type: | ||
""" | ||
return type(cls)( | ||
"%sFromQFunction" % cls.__name__, (cls,), {"q_func": staticmethod(func)} | ||
) | ||
|
||
|
||
class Manager(_Manager): | ||
""" | ||
Use this Manager class' from_queryset() method if you want to derive a Manager from | ||
a QuerySet that has FlexQuery's defined. If Django's native Manager.from_queryset() | ||
was used instead, all FlexQuery's would be lost. | ||
""" | ||
|
||
@classmethod | ||
def _get_queryset_methods(cls, queryset_class): | ||
methods = super()._get_queryset_methods(queryset_class) | ||
methods.update( | ||
inspect.getmembers( | ||
queryset_class, | ||
predicate=lambda member: isinstance(member, FlexQueryType) | ||
and not getattr(member, "queryset_only", None), | ||
) | ||
) | ||
return methods | ||
|
||
|
||
class Q(_Q): | ||
""" | ||
A custom Q implementation that allows prefixing existing Q objects with some | ||
related field name dynamically. | ||
""" | ||
|
||
def prefix(self, prefix): | ||
"""Recursively copies the Q object, prefixing all lookup keys. | ||
The prefix and the existing filter key are delimited by the lookup separator __. | ||
Use this feature to delegate existing query constraints to a related field. | ||
:param prefix: | ||
Name of the related field to prepend to existing lookup keys. This isn't | ||
restricted to a single relation level, something like "tree__fruit" | ||
is perfectly valid as well. | ||
:type prefix: str | ||
:returns Q: | ||
""" | ||
return type(self)( | ||
*( | ||
child.prefix(prefix) | ||
if isinstance(child, Q) | ||
else (prefix + LOOKUP_SEP + child[0], child[1]) | ||
for child in self.children | ||
), | ||
_connector=self.connector, | ||
_negated=self.negated, | ||
) | ||
|
||
|
||
class QuerySet(_QuerySet): | ||
""" | ||
Adds support for deriving a Manager from QuerySet via as_manager(), preserving | ||
the FlexQuery's. | ||
""" | ||
|
||
@classmethod | ||
def as_manager(cls): | ||
manager = Manager.from_queryset(cls)() | ||
manager._built_with_as_manager = True | ||
return manager |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Minimal makefile for Sphinx documentation | ||
# | ||
|
||
# You can set these variables from the command line, and also | ||
# from the environment for the first two. | ||
SPHINXOPTS ?= | ||
SPHINXBUILD ?= sphinx-build | ||
SOURCEDIR = . | ||
BUILDDIR = _build | ||
|
||
# Put it first so that "make" without argument is like "make help". | ||
help: | ||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) | ||
|
||
.PHONY: help Makefile | ||
|
||
# Catch-all target: route all unknown targets to Sphinx using the new | ||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). | ||
%: Makefile | ||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
API Documentation | ||
================= | ||
|
||
.. automodule:: django_flexquery | ||
:members: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# Configuration file for the Sphinx documentation builder. | ||
# | ||
# This file only contains a selection of the most common options. For a full | ||
# list see the documentation: | ||
# http://www.sphinx-doc.org/en/master/config | ||
|
||
# -- Path setup -------------------------------------------------------------- | ||
|
||
# If extensions (or modules to document with autodoc) are in another directory, | ||
# add these directories to sys.path here. If the directory is relative to the | ||
# documentation root, use os.path.abspath to make it absolute, like shown here. | ||
# | ||
import os | ||
import sys | ||
sys.path.insert(0, os.path.abspath('..')) | ||
|
||
|
||
# -- Project information ----------------------------------------------------- | ||
|
||
project = 'django-flexquery' | ||
import datetime | ||
copyright = '2019-{}, Robert Schindler'.format(datetime.date.today().year) | ||
author = 'Robert Schindler' | ||
|
||
from django_flexquery import __version__ | ||
# The full version, including alpha/beta/rc tags | ||
release = __version__ | ||
|
||
|
||
# -- General configuration --------------------------------------------------- | ||
|
||
# Add any Sphinx extension module names here, as strings. They can be | ||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | ||
# ones. | ||
extensions = [ | ||
'sphinx.ext.autodoc', | ||
'sphinx.ext.napoleon', | ||
] | ||
|
||
# Add any paths that contain templates here, relative to this directory. | ||
templates_path = ['_templates'] | ||
|
||
# The master toctree document. | ||
master_doc = 'index' | ||
|
||
# List of patterns, relative to source directory, that match files and | ||
# directories to ignore when looking for source files. | ||
# This pattern also affects html_static_path and html_extra_path. | ||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] | ||
|
||
|
||
# -- Options for HTML output ------------------------------------------------- | ||
|
||
# The theme to use for HTML and HTML Help pages. See the documentation for | ||
# a list of builtin themes. | ||
# | ||
html_theme = 'sphinx_rtd_theme' | ||
|
||
# Add any paths that contain custom static files (such as style sheets) here, | ||
# relative to this directory. They are copied after the builtin static files, | ||
# so a file named "default.css" will overwrite the builtin "default.css". | ||
html_static_path = ['_static'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
django-flexquery | ||
================ | ||
|
||
.. toctree:: | ||
:hidden: | ||
|
||
Introduction <self> | ||
usage | ||
api | ||
|
||
This library aims to provide a new way of declaring reusable QuerySet filtering | ||
logic in your Django project, incorporating the DRY principle and maximizing user | ||
experience and performance. | ||
|
||
Its strengths are, among others: | ||
|
||
* Easy to learn in minutes | ||
* Cleanly integrates with Django's ORM | ||
* Small code footprint, less bugs - ~150 lines of code (LoC) | ||
* Fully documented code, formatted using the excellent `Black Code Formatter | ||
<https://github.com/python/black>`_. | ||
|
||
Have a look at the :doc:`usage` and :doc:`api` to get an idea of how it works. | ||
|
||
|
||
Requirements | ||
------------ | ||
|
||
Django 2.0+ is required. | ||
|
||
|
||
Installation | ||
------------ | ||
|
||
:: | ||
|
||
pip install 'git+git://github.com/efficiosoft/django-flexquery#master' | ||
|
||
No changes to your Django settings are required; no ``INSTALLED_APPS``, no | ||
``MIDDLEWARE_CLASSES``. |
Oops, something went wrong.