Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
SunnyR committed Jun 7, 2024
2 parents 353f0b0 + 47dff32 commit 57b07ec
Show file tree
Hide file tree
Showing 30 changed files with 607 additions and 357 deletions.
13 changes: 7 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
language: python
dist: xenial
dist: bionic

python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"

install:
- travis_retry pip install -U six setuptools pip wheel
- travis_retry pip install -U tox tox-travis tox-venv coverage
- travis_retry pip install -U tox tox-travis coverage
script:
- tox
after_success:
Expand All @@ -17,15 +18,15 @@ after_success:

matrix:
include:
- python: "3.7"
- python: "3.8"
env: TOXENV="performance"
script: tox -- -v 2
- python: "3.7"
- python: "3.8"
env: TOXENV="warnings"
- python: "3.7"
- python: "3.8"
env: TOXENV="isort,lint"

- python: "3.7"
- python: "3.8"
env: TOXENV="dist"
script:
- python setup.py bdist_wheel
Expand Down
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright (c) 2013-2015 Philip Neustrom <philipn@gmail.com>,
2016-2020 Ryan P Kilby <kilbyr@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include LICENSE
include README.rst
recursive-include rest_framework_filters/templates *.html
global-exclude __pycache__
Expand Down
42 changes: 16 additions & 26 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Django Rest Framework Filters
.. image:: https://img.shields.io/pypi/v/djangorestframework-filters.svg
:target: https://pypi.python.org/pypi/djangorestframework-filters

.. image:: https://img.shields.io/pypi/pyversions/djangorestframework-filters.svg
:target: https://pypi.org/project/djangorestframework-filters/

.. image:: https://img.shields.io/pypi/l/tox-factor.svg
:target: https://pypi.org/project/djangorestframework-filters/


``django-rest-framework-filters`` is an extension to `Django REST framework`_ and `Django filter`_
that makes it easy to filter across relationships. Historically, this extension also provided a
Expand Down Expand Up @@ -55,10 +61,10 @@ Features
Requirements
------------

* **Python**: 3.4, 3.5, 3.6, 3.7
* **Django**: 1.11, 2.0, 2.1, 2.2
* **DRF**: 3.10
* **django-filter**: 2.0
* **Python**: 3.5, 3.6, 3.7, 3.8
* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0, 3.1
* **DRF**: 3.11
* **django-filter**: 2.1, 2.2 (Django 2.0+)


Installation
Expand Down Expand Up @@ -620,25 +626,9 @@ Publishing
$ twine upload dist/*
License
-------
Copyright (c) 2013-2015 Philip Neustrom <philipn@gmail.com>,
2016-2017 Ryan P Kilby <rpkilby@ncsu.edu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Copyright & License
-------------------
Copyright (c) 2013-2015 Philip Neustrom & 2016-2019 Ryan P Kilby. See `LICENSE`_ for details.
.. _`LICENSE`: https://github.com/philipn/django-rest-framework-filters/blob/master/LICENSE
16 changes: 9 additions & 7 deletions rest_framework_filters/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ def template(self):

@contextmanager
def patch_for_rendering(self, request):
"""
Patch `.get_filterset_class()` so the resulting filterset does not
perform filter expansion during form rendering.
"""
# Patch ``.get_filterset_class()`` so the resulting filterset does not perform
# filter expansion during form rendering.
original = self.get_filterset_class

def get_filterset_class(view, queryset=None):
Expand All @@ -46,8 +44,8 @@ def get_filterset_class(view, queryset=None):
self.get_filterset_class = original

def to_html(self, request, queryset, view):
# Patching the behavior of `.get_filterset_class()` in this method
# allows us to avoid maintenance issues with code duplication.
# Patching the behavior of ``.get_filterset_class()`` in this method allows us
# to avoid maintenance issues with code duplication.
with self.patch_for_rendering(request):
return super().to_html(request, queryset, view)

Expand All @@ -64,7 +62,11 @@ def filter_queryset(self, request, queryset, view):
# Decode the set of complex operations
encoded_querystring = request.query_params[self.complex_filter_param]
try:
complex_ops = decode_complex_ops(encoded_querystring, self.operators, self.negation)
complex_ops = decode_complex_ops(
encoded_querystring,
self.operators,
self.negation,
)
except ValidationError as exc:
raise ValidationError({self.complex_filter_param: exc.detail})

Expand Down
29 changes: 19 additions & 10 deletions rest_framework_filters/complex_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,9 @@


def decode_complex_ops(encoded_querystring, operators=None, negation=True):
"""
Returns a list of (querystring, negate, op) tuples that represent complex operations.
This function will raise a `ValidationError`s if:
- the individual querystrings are not wrapped in parentheses
- the set operators do not match the provided `operators`
- there is trailing content after the ending querysting
"""Decode the complex encoded querysting into a list of complex operations.
Ex::
.. code-block:: python
# unencoded query: (a=1) & (b=2) | ~(c=3)
>>> s = '%28a%253D1%29%20%26%20%28b%253D2%29%20%7C%20%7E%28c%253D3%29'
Expand All @@ -41,14 +35,29 @@ def decode_complex_ops(encoded_querystring, operators=None, negation=True):
('b=2', False, QuerySet.__or__),
('c=3', True, None),
]
Args:
encoded_querystring: The encoded querystring.
operators: A map of {operator symbols: queryset operations}. Defaults to the
``COMPLEX_OPERATIONS`` mapping.
negation: Whether to parse negation.
Returns:
A list of ``(querystring, negate, op)`` tuples that represent the operations.
Raises:
ValidationError: Raised under the following conditions:
- the individual querystrings are not wrapped in parentheses
- the set operators do not match the provided `operators`
- there is trailing content after the ending querysting
"""
complex_op_re = COMPLEX_OP_NEG_RE if negation else COMPLEX_OP_RE
if operators is None:
operators = COMPLEX_OPERATORS

# decode into: (a%3D1) & (b%3D2) | ~(c%3D3)
decoded_querystring = unquote(encoded_querystring)
matches = [m for m in complex_op_re.finditer(decoded_querystring)]
matches = list(complex_op_re.finditer(decoded_querystring))

if not matches:
msg = _("Unable to parse querystring. Decoded: '%(decoded)s'.")
Expand All @@ -67,9 +76,9 @@ def decode_complex_ops(encoded_querystring, operators=None, negation=True):

results.append(ComplexOp(querystring, negate, op_func))

msg = _("Ending querystring must not have trailing characters. Matched: '%(chars)s'.")
trailing_chars = decoded_querystring[matches[-1].end():]
if trailing_chars:
msg = _("Ending querystring must not have trailing characters. Matched: '%(chars)s'.")
errors.append(msg % {'chars': trailing_chars})

if errors:
Expand Down
35 changes: 23 additions & 12 deletions rest_framework_filters/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class AutoFilter:
This is a declarative alternative to the ``Meta.fields`` dict syntax, and
the below are functionally equivalent:
.. code-block:: python
class PersonFilter(filters.FilterSet):
name = AutoFilter(lookups=['exact', 'contains'])
Expand All @@ -30,6 +32,8 @@ class Meta:
Due to its declarative nature, an ``AutoFilter`` allows for paramater name
aliasing for its generated filters. e.g.,
.. code-block:: python
class BlogFilter(filters.FilterSet):
title = AutoFilter(field_name='name', lookups=['contains'])
Expand All @@ -42,6 +46,7 @@ class BlogFilter(filters.FilterSet):
filterable. However, an ``AutoFilter`` is typically replaced by a generated
``exact`` filter of the same name, which enables filtering by that param.
"""

creation_counter = 0

def __init__(self, field_name=None, *, method=None, lookups=None):
Expand All @@ -59,17 +64,20 @@ def __init__(self, filterset, *args, lookups=None, **kwargs):
self.filterset = filterset
self.lookups = lookups or []

def bind(self, bind_cls):
"""
Bind a filterset class to the filter instance. This class is used for
relative imports. Only the first bound class is used as filterset
inheritance might otherwise break these relative import paths.
def bind_filterset(self, filterset):
"""Bind a filterset class to the filter instance.
This class is used for relative imports. Only the first bound class is used as
filterset inheritance might otherwise break these relative import paths.
This is also necessary to allow ``.filterset`` to be resolved during FilterSet
class creation time, instead of during initialization.
This is also necessary to allow `.filterset` to be resolved during
FilterSet class creation time, instead of during initialization.
Args:
filterset: The filterset to bind
"""
if not hasattr(self, 'bind_cls'):
self.bind_cls = bind_cls
if not hasattr(self, 'bound_filterset'):
self.bound_filterset = filterset

def filterset():
def fget(self):
Expand All @@ -79,7 +87,7 @@ def fget(self):
self._filterset = import_string(self._filterset)
except ImportError:
# Fallback to building import path relative to bind class
path = '.'.join([self.bind_cls.__module__, self._filterset])
path = '.'.join([self.bound_filterset.__module__, self._filterset])
self._filterset = import_string(path)
return self._filterset

Expand Down Expand Up @@ -144,5 +152,8 @@ class AllLookupsFilter(AutoFilter):
def __init__(self, *args, **kwargs):
super().__init__(*args, lookups=ALL_LOOKUPS, **kwargs)
warnings.warn(
"`AllLookupsFilter()` has been deprecated in favor of `AutoFilter(lookups='__all__')`.",
DeprecationWarning, stacklevel=2)
"`AllLookupsFilter()` has been deprecated in favor of "
"`AutoFilter(lookups='__all__')`.",
DeprecationWarning,
stacklevel=2,
)
Loading

0 comments on commit 57b07ec

Please sign in to comment.