Skip to content

Commit 57b07ec

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 353f0b0 + 47dff32 commit 57b07ec

30 files changed

+607
-357
lines changed

.travis.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
language: python
2-
dist: xenial
2+
dist: bionic
33

44
python:
55
- "3.5"
66
- "3.6"
77
- "3.7"
8+
- "3.8"
89

910
install:
1011
- travis_retry pip install -U six setuptools pip wheel
11-
- travis_retry pip install -U tox tox-travis tox-venv coverage
12+
- travis_retry pip install -U tox tox-travis coverage
1213
script:
1314
- tox
1415
after_success:
@@ -17,15 +18,15 @@ after_success:
1718

1819
matrix:
1920
include:
20-
- python: "3.7"
21+
- python: "3.8"
2122
env: TOXENV="performance"
2223
script: tox -- -v 2
23-
- python: "3.7"
24+
- python: "3.8"
2425
env: TOXENV="warnings"
25-
- python: "3.7"
26+
- python: "3.8"
2627
env: TOXENV="isort,lint"
2728

28-
- python: "3.7"
29+
- python: "3.8"
2930
env: TOXENV="dist"
3031
script:
3132
- python setup.py bdist_wheel

LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2013-2015 Philip Neustrom <[email protected]>,
2+
2016-2020 Ryan P Kilby <[email protected]>
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
include LICENSE
12
include README.rst
23
recursive-include rest_framework_filters/templates *.html
34
global-exclude __pycache__

README.rst

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Django Rest Framework Filters
1010
.. image:: https://img.shields.io/pypi/v/djangorestframework-filters.svg
1111
:target: https://pypi.python.org/pypi/djangorestframework-filters
1212

13+
.. image:: https://img.shields.io/pypi/pyversions/djangorestframework-filters.svg
14+
:target: https://pypi.org/project/djangorestframework-filters/
15+
16+
.. image:: https://img.shields.io/pypi/l/tox-factor.svg
17+
:target: https://pypi.org/project/djangorestframework-filters/
18+
1319

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

58-
* **Python**: 3.4, 3.5, 3.6, 3.7
59-
* **Django**: 1.11, 2.0, 2.1, 2.2
60-
* **DRF**: 3.10
61-
* **django-filter**: 2.0
64+
* **Python**: 3.5, 3.6, 3.7, 3.8
65+
* **Django**: 1.11, 2.0, 2.1, 2.2, 3.0, 3.1
66+
* **DRF**: 3.11
67+
* **django-filter**: 2.1, 2.2 (Django 2.0+)
6268

6369

6470
Installation
@@ -620,25 +626,9 @@ Publishing
620626
$ twine upload dist/*
621627
622628
623-
License
624-
-------
625-
Copyright (c) 2013-2015 Philip Neustrom <philipn@gmail.com>,
626-
2016-2017 Ryan P Kilby <rpkilby@ncsu.edu>
627-
628-
Permission is hereby granted, free of charge, to any person obtaining a copy
629-
of this software and associated documentation files (the "Software"), to deal
630-
in the Software without restriction, including without limitation the rights
631-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
632-
copies of the Software, and to permit persons to whom the Software is
633-
furnished to do so, subject to the following conditions:
634-
635-
The above copyright notice and this permission notice shall be included in
636-
all copies or substantial portions of the Software.
637-
638-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
639-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
640-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
641-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
642-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
643-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
644-
THE SOFTWARE.
629+
Copyright & License
630+
-------------------
631+
632+
Copyright (c) 2013-2015 Philip Neustrom & 2016-2019 Ryan P Kilby. See `LICENSE`_ for details.
633+
634+
.. _`LICENSE`: https://github.com/philipn/django-rest-framework-filters/blob/master/LICENSE

rest_framework_filters/backends.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ def template(self):
2020

2121
@contextmanager
2222
def patch_for_rendering(self, request):
23-
"""
24-
Patch `.get_filterset_class()` so the resulting filterset does not
25-
perform filter expansion during form rendering.
26-
"""
23+
# Patch ``.get_filterset_class()`` so the resulting filterset does not perform
24+
# filter expansion during form rendering.
2725
original = self.get_filterset_class
2826

2927
def get_filterset_class(view, queryset=None):
@@ -46,8 +44,8 @@ def get_filterset_class(view, queryset=None):
4644
self.get_filterset_class = original
4745

4846
def to_html(self, request, queryset, view):
49-
# Patching the behavior of `.get_filterset_class()` in this method
50-
# allows us to avoid maintenance issues with code duplication.
47+
# Patching the behavior of ``.get_filterset_class()`` in this method allows us
48+
# to avoid maintenance issues with code duplication.
5149
with self.patch_for_rendering(request):
5250
return super().to_html(request, queryset, view)
5351

@@ -64,7 +62,11 @@ def filter_queryset(self, request, queryset, view):
6462
# Decode the set of complex operations
6563
encoded_querystring = request.query_params[self.complex_filter_param]
6664
try:
67-
complex_ops = decode_complex_ops(encoded_querystring, self.operators, self.negation)
65+
complex_ops = decode_complex_ops(
66+
encoded_querystring,
67+
self.operators,
68+
self.negation,
69+
)
6870
except ValidationError as exc:
6971
raise ValidationError({self.complex_filter_param: exc.detail})
7072

rest_framework_filters/complex_ops.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,9 @@
2323

2424

2525
def decode_complex_ops(encoded_querystring, operators=None, negation=True):
26-
"""
27-
Returns a list of (querystring, negate, op) tuples that represent complex operations.
28-
29-
This function will raise a `ValidationError`s if:
30-
- the individual querystrings are not wrapped in parentheses
31-
- the set operators do not match the provided `operators`
32-
- there is trailing content after the ending querysting
26+
"""Decode the complex encoded querysting into a list of complex operations.
3327
34-
Ex::
28+
.. code-block:: python
3529
3630
# unencoded query: (a=1) & (b=2) | ~(c=3)
3731
>>> s = '%28a%253D1%29%20%26%20%28b%253D2%29%20%7C%20%7E%28c%253D3%29'
@@ -41,14 +35,29 @@ def decode_complex_ops(encoded_querystring, operators=None, negation=True):
4135
('b=2', False, QuerySet.__or__),
4236
('c=3', True, None),
4337
]
38+
39+
Args:
40+
encoded_querystring: The encoded querystring.
41+
operators: A map of {operator symbols: queryset operations}. Defaults to the
42+
``COMPLEX_OPERATIONS`` mapping.
43+
negation: Whether to parse negation.
44+
45+
Returns:
46+
A list of ``(querystring, negate, op)`` tuples that represent the operations.
47+
48+
Raises:
49+
ValidationError: Raised under the following conditions:
50+
- the individual querystrings are not wrapped in parentheses
51+
- the set operators do not match the provided `operators`
52+
- there is trailing content after the ending querysting
4453
"""
4554
complex_op_re = COMPLEX_OP_NEG_RE if negation else COMPLEX_OP_RE
4655
if operators is None:
4756
operators = COMPLEX_OPERATORS
4857

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

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

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

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

7584
if errors:

rest_framework_filters/filters.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class AutoFilter:
1515
This is a declarative alternative to the ``Meta.fields`` dict syntax, and
1616
the below are functionally equivalent:
1717
18+
.. code-block:: python
19+
1820
class PersonFilter(filters.FilterSet):
1921
name = AutoFilter(lookups=['exact', 'contains'])
2022
@@ -30,6 +32,8 @@ class Meta:
3032
Due to its declarative nature, an ``AutoFilter`` allows for paramater name
3133
aliasing for its generated filters. e.g.,
3234
35+
.. code-block:: python
36+
3337
class BlogFilter(filters.FilterSet):
3438
title = AutoFilter(field_name='name', lookups=['contains'])
3539
@@ -42,6 +46,7 @@ class BlogFilter(filters.FilterSet):
4246
filterable. However, an ``AutoFilter`` is typically replaced by a generated
4347
``exact`` filter of the same name, which enables filtering by that param.
4448
"""
49+
4550
creation_counter = 0
4651

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

62-
def bind(self, bind_cls):
63-
"""
64-
Bind a filterset class to the filter instance. This class is used for
65-
relative imports. Only the first bound class is used as filterset
66-
inheritance might otherwise break these relative import paths.
67+
def bind_filterset(self, filterset):
68+
"""Bind a filterset class to the filter instance.
69+
70+
This class is used for relative imports. Only the first bound class is used as
71+
filterset inheritance might otherwise break these relative import paths.
72+
73+
This is also necessary to allow ``.filterset`` to be resolved during FilterSet
74+
class creation time, instead of during initialization.
6775
68-
This is also necessary to allow `.filterset` to be resolved during
69-
FilterSet class creation time, instead of during initialization.
76+
Args:
77+
filterset: The filterset to bind
7078
"""
71-
if not hasattr(self, 'bind_cls'):
72-
self.bind_cls = bind_cls
79+
if not hasattr(self, 'bound_filterset'):
80+
self.bound_filterset = filterset
7381

7482
def filterset():
7583
def fget(self):
@@ -79,7 +87,7 @@ def fget(self):
7987
self._filterset = import_string(self._filterset)
8088
except ImportError:
8189
# Fallback to building import path relative to bind class
82-
path = '.'.join([self.bind_cls.__module__, self._filterset])
90+
path = '.'.join([self.bound_filterset.__module__, self._filterset])
8391
self._filterset = import_string(path)
8492
return self._filterset
8593

@@ -144,5 +152,8 @@ class AllLookupsFilter(AutoFilter):
144152
def __init__(self, *args, **kwargs):
145153
super().__init__(*args, lookups=ALL_LOOKUPS, **kwargs)
146154
warnings.warn(
147-
"`AllLookupsFilter()` has been deprecated in favor of `AutoFilter(lookups='__all__')`.",
148-
DeprecationWarning, stacklevel=2)
155+
"`AllLookupsFilter()` has been deprecated in favor of "
156+
"`AutoFilter(lookups='__all__')`.",
157+
DeprecationWarning,
158+
stacklevel=2,
159+
)

0 commit comments

Comments
 (0)