Browse Source

Added DjangoBackend, refactored other code

Johann Schmitz 2 years ago
parent
commit
ef5a21bd5b
Signed by: Johann Schmitz <johann@j-schmitz.net> GPG Key ID: A084064277C501ED

+ 1
- 0
.travis.yml View File

@@ -10,6 +10,7 @@ python:
10 10
 
11 11
 install:
12 12
 - pip install -r requirements.txt
13
+- pip install -r requirements_optional.txt
13 14
 - pip install -r requirements_dev.txt
14 15
 
15 16
 script: make travis

+ 1
- 0
requirements_optional.txt View File

@@ -0,0 +1 @@
1
+django

+ 13
- 4
src/phylter/backends/__init__.py View File

@@ -1,10 +1,19 @@
1 1
 # -*- coding: utf-8 -*-
2
-
3 2
 from phylter.backends.objects import ObjectsBackend
4 3
 
5
-backends = [
6
-	ObjectsBackend
7
-]
4
+backends = None
5
+
6
+if backends is None:
7
+	backends = []
8
+
9
+	try:
10
+		from phylter.backends.django_backend import DjangoBackend
11
+		backends.append(DjangoBackend)
12
+	except ImportError:
13
+		pass
14
+
15
+	backends.append(ObjectsBackend)
16
+
8 17
 
9 18
 def get_backend(o):
10 19
 	for b in backends:

+ 31
- 1
src/phylter/backends/base.py View File

@@ -1,6 +1,19 @@
1 1
 # -*- coding: utf-8 -*-
2
+import re
3
+import sys
2 4
 
3
-class Backend(object): # pragma: nocover
5
+number_re = re.compile("^-?\d+(\.\d+)?$")
6
+
7
+def digit_or_float(s):
8
+	return s.isdigit() or number_re.match(s) is not None
9
+
10
+if sys.version_info[0] == 2:
11
+	str_types = (str, unicode, bytes)
12
+else:
13
+	str_types = (str, bytes)
14
+
15
+
16
+class Backend(object):  # pragma: nocover
4 17
 
5 18
 	@staticmethod
6 19
 	def supports(o):
@@ -8,3 +21,20 @@ class Backend(object): # pragma: nocover
8 21
 
9 22
 	def apply(self, query, iterable):
10 23
 		raise NotImplementedError
24
+
25
+	def get_compatible_value(self, value, field_type=None):
26
+		if field_type in str_types or (field_type is None and isinstance(value, str_types)):
27
+			if value[0] in ("'", '"') and value[0] == value[-1]:
28
+				# quoted string
29
+				return value[1:-1]
30
+
31
+		if field_type in (int, float) or (field_type is None and isinstance(value, (int, float))):
32
+			if isinstance(value, (int, float)):
33
+				return value
34
+
35
+			if isinstance(value, str_types) and digit_or_float(value):
36
+				return float(value)
37
+
38
+			return value
39
+
40
+		return value

+ 47
- 0
src/phylter/backends/django_backend.py View File

@@ -0,0 +1,47 @@
1
+# -*- coding: utf-8 -*-
2
+from django.db.models import Q
3
+from django.db.models.manager import Manager
4
+from django.db.models.query import QuerySet
5
+
6
+from phylter.backends.base import Backend
7
+from phylter.conditions import Condition, EqualsCondition, GreaterThanCondition, GreaterThanOrEqualCondition, \
8
+	LessThanCondition, LessThanOrEqualCondition, AndOperator, OrOperator
9
+
10
+
11
+class DjangoBackend(Backend):
12
+
13
+	@staticmethod
14
+	def supports(o):
15
+		return isinstance(o, QuerySet) or isinstance(o, Manager)
16
+
17
+	def apply(self, query, iterable):
18
+		django_query = iterable if isinstance(iterable, QuerySet) else iterable.all()
19
+
20
+		for o in query.query:
21
+			django_query = self.apply_django_filter(django_query, o)
22
+
23
+		return django_query
24
+
25
+	def apply_django_filter(self, django_query, obj):
26
+		return django_query.filter(self.to_q(obj))
27
+
28
+	def to_q(self, obj):
29
+		if isinstance(obj, Condition):
30
+			suffix = {
31
+				EqualsCondition: "",
32
+				GreaterThanCondition: "__gt",
33
+				GreaterThanOrEqualCondition: "__gte",
34
+				LessThanCondition: "__lt",
35
+				LessThanOrEqualCondition: "__lte",
36
+			}[obj.__class__]
37
+
38
+			f = "%s%s" % (obj.left, suffix)
39
+			return Q(**{f: self.get_compatible_value(obj.right)})
40
+
41
+		if isinstance(obj, AndOperator):
42
+			return Q(self.to_q(obj.left), self.to_q(obj.right))
43
+
44
+		if isinstance(obj, OrOperator):
45
+			return Q(self.to_q(obj.left)) | Q(self.to_q(obj.right))
46
+
47
+		raise Exception("Unexpected item found in query: %s" % obj)

+ 2
- 16
src/phylter/backends/objects.py View File

@@ -4,10 +4,6 @@ from phylter.backends.base import Backend
4 4
 from phylter.conditions import Condition, OrOperator, AndOperator, EqualsCondition, \
5 5
 	GreaterThanOrEqualCondition, LessThanCondition, LessThanOrEqualCondition, GreaterThanCondition
6 6
 
7
-number_re = re.compile("^-?\d+(\.\d+)?$")
8
-
9
-def digit_or_float(s):
10
-	return s.isdigit() or number_re.match(s) is not None
11 7
 
12 8
 class ObjectsBackend(Backend):
13 9
 
@@ -25,8 +21,8 @@ class ObjectsBackend(Backend):
25 21
 
26 22
 	def eval_op(self, op, item):
27 23
 		if isinstance(op, Condition):
28
-			left_value = self.get_item_value(item, op.left)
29
-			right_value = self.get_item_value(item, op.right)
24
+			left_value = getattr(item, op.left)
25
+			right_value = self.get_compatible_value(op.right, type(left_value))
30 26
 
31 27
 			return {
32 28
 				EqualsCondition: lambda a, b: a == b,
@@ -43,13 +39,3 @@ class ObjectsBackend(Backend):
43 39
 			return self.eval_op(op.left, item) or self.eval_op(op.right, item)
44 40
 
45 41
 		raise Exception("Unexpected item found in query: %s" % op)
46
-
47
-	def get_item_value(self, item, name_or_value):
48
-		if isinstance(name_or_value, (int, float)) or digit_or_float(name_or_value):
49
-			return float(name_or_value)
50
-
51
-		if name_or_value[0] in ("'", '"') and name_or_value[0] == name_or_value[-1]:
52
-			# quoted string
53
-			return name_or_value[1:-1]
54
-
55
-		return getattr(item, name_or_value)

+ 63
- 18
tests/test_backends.py View File

@@ -1,7 +1,13 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import pytest
3
+import sys
4
+from django.db.models import Q
5
+from django.db.models.manager import Manager
6
+from django.db.models.query import QuerySet
3 7
 
4 8
 from phylter.backends import backends, get_backend
9
+from phylter.backends.base import Backend, str_types
10
+from phylter.backends.django_backend import DjangoBackend
5 11
 from phylter.backends.objects import ObjectsBackend
6 12
 from phylter.conditions import EqualsCondition, GreaterThanCondition, GreaterThanOrEqualCondition, LessThanCondition, \
7 13
 	LessThanOrEqualCondition, AndOperator, OrOperator
@@ -12,6 +18,7 @@ class TestBackends(object):
12 18
 
13 19
 	def test_objectbackend_exists(self):
14 20
 		assert ObjectsBackend in backends
21
+		assert DjangoBackend in backends
15 22
 
16 23
 	def test_get_backend(self):
17 24
 		class Foo(object):
@@ -20,6 +27,29 @@ class TestBackends(object):
20 27
 		be = get_backend(Foo())
21 28
 		assert isinstance(be, ObjectsBackend)
22 29
 
30
+	@pytest.mark.skipif(sys.version_info >= (3, 0), reason="requires python 2")
31
+	def test_str_types_py2(self):
32
+		assert str_types == (str, unicode, bytes)
33
+
34
+	@pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python 3")
35
+	def test_str_types_py3(self):
36
+		assert str_types == (str, bytes)
37
+
38
+	def test_get_compatible_value(self):
39
+		ob = Backend()
40
+
41
+		assert ob.get_compatible_value('"test"') == "test"
42
+		assert ob.get_compatible_value(10) == 10
43
+		assert ob.get_compatible_value("10", int) == 10
44
+		assert ob.get_compatible_value("10.0", int) == 10
45
+		assert ob.get_compatible_value("10.0", float) == 10
46
+		assert ob.get_compatible_value("-10", int) == -10
47
+		assert ob.get_compatible_value("-10.0", int) == -10
48
+		assert ob.get_compatible_value("-10.0", float) == -10
49
+
50
+		assert ob.get_compatible_value(True) == True
51
+
52
+
23 53
 class TestObjectBackend(object):
24 54
 
25 55
 	def test_supports(self):
@@ -30,24 +60,6 @@ class TestObjectBackend(object):
30 60
 		assert ObjectsBackend.supports(True)
31 61
 		assert ObjectsBackend.supports(Foo())
32 62
 
33
-	def test_get_item_value(self):
34
-		ob = ObjectsBackend()
35
-
36
-		assert ob.get_item_value(None, '"test"') == "test"
37
-		assert ob.get_item_value(None, "10") == 10
38
-		assert ob.get_item_value(None, "10.0") == 10.0
39
-		assert ob.get_item_value(None, "-10") == -10
40
-		assert ob.get_item_value(None, "-10.0") == -10.0
41
-
42
-		class Foo(object):
43
-			def __init__(self):
44
-				self.bar = 'baz'
45
-
46
-		assert ob.get_item_value(Foo(), 'bar') == 'baz'
47
-
48
-		with pytest.raises(AttributeError) as e:
49
-			assert ob.get_item_value(Foo(), 'bat')
50
-
51 63
 	def test_eval_op_condition(self):
52 64
 		ob = ObjectsBackend()
53 65
 
@@ -84,3 +96,36 @@ class TestObjectBackend(object):
84 96
 		query = Query([EqualsCondition('a', 1)])
85 97
 		assert list(ob.apply(query, [Foo()])) == [Foo()]
86 98
 		assert list(query.apply([Foo()])) == [Foo()]
99
+
100
+
101
+class TestDjangoBackend(object):
102
+
103
+	def test_supports(self):
104
+		assert DjangoBackend.supports(QuerySet())
105
+		assert DjangoBackend.supports(Manager())
106
+		assert not DjangoBackend.supports(Q())
107
+
108
+	def test_get_backend(self):
109
+		assert isinstance(get_backend(QuerySet()), DjangoBackend)
110
+		assert isinstance(get_backend(Manager()), DjangoBackend)
111
+
112
+	def test_to_q(self):
113
+		db = DjangoBackend()
114
+
115
+		assert db.to_q(EqualsCondition('a', 1)).children == [('a', 1)]
116
+		assert db.to_q(GreaterThanCondition('a', 0)).children == [('a__gt', 0)]
117
+		assert db.to_q(GreaterThanOrEqualCondition('a', 1)).children == [('a__gte', 1)]
118
+		assert db.to_q(LessThanCondition('a', 2)).children == [('a__lt', 2)]
119
+		assert db.to_q(LessThanOrEqualCondition('a', 1)).children == [('a__lte', 1)]
120
+
121
+		q = db.to_q(AndOperator(EqualsCondition('a', 1), GreaterThanCondition('a', 0)))
122
+		assert len(q.children) == 2
123
+		assert q.children[0].children == [('a', 1)]
124
+		assert q.children[1].children == [('a__gt', 0)]
125
+		assert q.connector == 'AND'
126
+
127
+		q = db.to_q(OrOperator(EqualsCondition('a', 1), GreaterThanCondition('a', 0)))
128
+		assert len(q.children) == 2
129
+		assert q.children[0].children == [('a', 1)]
130
+		assert q.children[1].children == [('a__gt', 0)]
131
+		assert q.connector == 'OR'