Remove Django-style WHERE; consider Python expressions
This commit is contained in:
		| @@ -35,6 +35,7 @@ install: | ||||
|  # - cd $TRAVIS_BUILD_DIR | ||||
|  # $TRAVIS_BUILD_DIR is set to the location of the cloned repository: | ||||
|  # for example: /home/travis/build/gramps-project/gramps | ||||
|  - git clone -b master https://github.com/srossross/meta | ||||
|  - python setup.py build | ||||
|  | ||||
| before_script: | ||||
| @@ -45,7 +46,7 @@ before_script: | ||||
| script: | ||||
|  # --exclude=TestUser because of older version of mock | ||||
|  #                    without configure_mock | ||||
|  - GRAMPS_RESOURCES=. nosetests3 --nologcapture --with-coverage --cover-package=gramps --exclude=TestcaseGenerator --exclude=vcard --exclude=merge_ref_test  --exclude=user_test gramps | ||||
|  - PYTHONPATH=meta GRAMPS_RESOURCES=. nosetests3 --nologcapture --with-coverage --cover-package=gramps --exclude=TestcaseGenerator --exclude=vcard --exclude=merge_ref_test  --exclude=user_test gramps | ||||
|  | ||||
| after_success: | ||||
|  - codecov | ||||
|   | ||||
| @@ -1287,7 +1287,7 @@ class DbReadBase(object): | ||||
|                     if compare(item, op, value): | ||||
|                         return True | ||||
|                 return False | ||||
|             if op == "=": | ||||
|             if op in ["=", "=="]: | ||||
|                 matched = v == value | ||||
|             elif op == ">": | ||||
|                 matched = v > value | ||||
| @@ -1430,15 +1430,15 @@ class DbReadBase(object): | ||||
|         name = self.get_table_func(table,"class_func").get_field_alias(name) | ||||
|         return name.replace(".", "__") | ||||
|  | ||||
|     Person = property(lambda self:QuerySet(self, "Person")) | ||||
|     Family = property(lambda self:QuerySet(self, "Family")) | ||||
|     Note = property(lambda self:QuerySet(self, "Note")) | ||||
|     Citation = property(lambda self:QuerySet(self, "Citation")) | ||||
|     Source = property(lambda self:QuerySet(self, "Source")) | ||||
|     Repository = property(lambda self:QuerySet(self, "Repository")) | ||||
|     Place = property(lambda self:QuerySet(self, "Place")) | ||||
|     Event = property(lambda self:QuerySet(self, "Event")) | ||||
|     Tag = property(lambda self:QuerySet(self, "Tag")) | ||||
|     Person = property(lambda self: QuerySet(self, "Person")) | ||||
|     Family = property(lambda self: QuerySet(self, "Family")) | ||||
|     Note = property(lambda self: QuerySet(self, "Note")) | ||||
|     Citation = property(lambda self: QuerySet(self, "Citation")) | ||||
|     Source = property(lambda self: QuerySet(self, "Source")) | ||||
|     Repository = property(lambda self: QuerySet(self, "Repository")) | ||||
|     Place = property(lambda self: QuerySet(self, "Place")) | ||||
|     Event = property(lambda self: QuerySet(self, "Event")) | ||||
|     Tag = property(lambda self: QuerySet(self, "Tag")) | ||||
|  | ||||
| class DbWriteBase(DbReadBase): | ||||
|     """ | ||||
| @@ -2089,41 +2089,6 @@ class DbWriteBase(DbReadBase): | ||||
|         """ | ||||
|         return getattr(self, table_name) | ||||
|  | ||||
| class Operator(object): | ||||
|     """ | ||||
|     Base for QuerySet operators. | ||||
|     """ | ||||
|     op = "OP" | ||||
|     def __init__(self, *expressions, **kwargs): | ||||
|         if self.op in ["AND", "OR"]: | ||||
|             exprs = [expression.list for expression | ||||
|                      in expressions] | ||||
|             for key in kwargs: | ||||
|                 exprs.append( | ||||
|                     _select_field_operator_value(key, "=", kwargs[key])) | ||||
|         else: # "NOT" | ||||
|             if expressions: | ||||
|                 exprs = expressions.list | ||||
|             else: | ||||
|                 key, value = list(kwargs.items())[0] | ||||
|                 exprs = _select_field_operator_value(key, "=", value) | ||||
|         self.list = [self.op, exprs] | ||||
|  | ||||
| class AND(Operator): | ||||
|     op = "AND" | ||||
|  | ||||
| class OR(Operator): | ||||
|     """ | ||||
|     OR operator for QuerySet logical WHERE expressions. | ||||
|     """ | ||||
|     op = "OR" | ||||
|  | ||||
| class NOT(Operator): | ||||
|     """ | ||||
|     NOT operator for QuerySet logical WHERE expressions. | ||||
|     """ | ||||
|     op = "NOT" | ||||
|  | ||||
| class QuerySet(object): | ||||
|     """ | ||||
|     A container for selection criteria before being actually | ||||
| @@ -2164,20 +2129,15 @@ class QuerySet(object): | ||||
|         self.needs_to_run = True | ||||
|         return self | ||||
|  | ||||
|     def _add_where_clause(self, *args, **kwargs): | ||||
|     def _add_where_clause(self, *args): | ||||
|         """ | ||||
|         Add a condition to the where clause. | ||||
|         """ | ||||
|         # First, handle AND, OR, NOT args: | ||||
|         and_expr = [] | ||||
|         for arg in args: | ||||
|             expr = arg.list | ||||
|         for expr in args: | ||||
|             and_expr.append(expr) | ||||
|         # Next, handle kwargs: | ||||
|         for keyword in kwargs: | ||||
|             and_expr.append( | ||||
|                 _select_field_operator_value( | ||||
|                     keyword, "=", kwargs[keyword])) | ||||
|         if and_expr: | ||||
|             if self.where_by: | ||||
|                 self.where_by = ["AND", [self.where_by] + and_expr] | ||||
| @@ -2260,20 +2220,32 @@ class QuerySet(object): | ||||
|         self.database = proxy_class(self.database, *args, **kwargs) | ||||
|         return self | ||||
|  | ||||
|     def filter(self, *args, **kwargs): | ||||
|     def where(self, where_clause): | ||||
|         """ | ||||
|         Apply a where_clause (closure) to the selection process. | ||||
|         """ | ||||
|         from gramps.gen.db.where import eval_where | ||||
|         # if there is already a generator, then error: | ||||
|         if self.generator: | ||||
|             raise Exception("Queries in invalid order") | ||||
|         where_by = eval_where(where_clause) | ||||
|         self._add_where_clause(where_by) | ||||
|         return self | ||||
|  | ||||
|     def filter(self, *args): | ||||
|         """ | ||||
|         Apply a filter to the database. | ||||
|         """ | ||||
|         from gramps.gen.proxy import FilterProxyDb | ||||
|         from gramps.gen.filters import GenericFilter | ||||
|         from gramps.gen.db.where import eval_where | ||||
|         for i in range(len(args)): | ||||
|             arg = args[i] | ||||
|             if isinstance(arg, GenericFilter): | ||||
|                 self.database = FilterProxyDb(self.database, arg, *args[i+1:]) | ||||
|                 if arg.where_by: | ||||
|                     self._add_where_clause(arg.where_by) | ||||
|             elif isinstance(arg, Operator): | ||||
|                 self._add_where_clause(arg) | ||||
|                 if hasattr(arg, "where"): | ||||
|                     where_by = eval_where(arg.where) | ||||
|                     self._add_where_clause(where_by) | ||||
|             elif callable(arg): | ||||
|                 if self.generator and self.needs_to_run: | ||||
|                     ## error | ||||
| @@ -2285,8 +2257,6 @@ class QuerySet(object): | ||||
|                 self.generator = filter(arg, self.generator) | ||||
|             else: | ||||
|                 pass # ignore, may have been arg from previous Filter | ||||
|         if kwargs: | ||||
|             self._add_where_clause(**kwargs) | ||||
|         return self | ||||
|  | ||||
|     def map(self, f): | ||||
| @@ -2329,33 +2299,3 @@ class QuerySet(object): | ||||
|                     item.add_tag(tag.handle) | ||||
|                     commit_func(item, trans) | ||||
|  | ||||
| def _to_dot_format(field): | ||||
|     """ | ||||
|     Convert a field keyword arg into a proper | ||||
|     dotted field name. | ||||
|     """ | ||||
|     return field.replace("__", ".") | ||||
|  | ||||
| def _select_field_operator_value(field, op, value): | ||||
|     """ | ||||
|     Convert a field keyword arg into proper | ||||
|     field, op, and value. | ||||
|     """ | ||||
|     alias = { | ||||
|         "LT": "<", | ||||
|         "GT": ">", | ||||
|         "LTE": "<=", | ||||
|         "GTE": ">=", | ||||
|         "IS_NOT": "IS NOT", | ||||
|         "IS_NULL": "IS NULL", | ||||
|         "IS_NOT_NULL": "IS NOT NULL", | ||||
|         "NE": "<>", | ||||
|     } | ||||
|     for operator in ["LIKE", "IN"] + list(alias.keys()): | ||||
|         operator = "__" + operator | ||||
|         if field.endswith(operator): | ||||
|             op = field[-len(operator) + 2:] | ||||
|             field = field[:-len(operator)] | ||||
|             op = alias.get(op, op) | ||||
|     field = _to_dot_format(field) | ||||
|     return (field, op, value) | ||||
|   | ||||
							
								
								
									
										102
									
								
								gramps/gen/db/test/test_where.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								gramps/gen/db/test/test_where.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| # | ||||
| # Gramps - a GTK+/GNOME based genealogy program | ||||
| # | ||||
| # Copyright (C) 2016       Gramps Development Team | ||||
| # | ||||
| # This program is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA | ||||
| # | ||||
|  | ||||
| from gramps.gen.db.where import eval_where | ||||
| import unittest | ||||
|  | ||||
| ########## | ||||
| # Tests: | ||||
|  | ||||
| def make_closure(surname): | ||||
|     """ | ||||
|     Test closure. | ||||
|     """ | ||||
|     from gramps.gen.lib import Person | ||||
|     return (lambda person:  | ||||
|             (person.primary_name.surname_list[0].surname == surname and | ||||
|              person.gender == Person.MALE)) | ||||
|  | ||||
| class Thing(object): | ||||
|     def __init__(self): | ||||
|         self.list = ["I0", "I1", "I2"] | ||||
|  | ||||
|     def where(self): | ||||
|         return lambda person: person.gramps_id == self.list[1] | ||||
|  | ||||
| class ClosureTest(unittest.TestCase): | ||||
|     def check(self, test): | ||||
|         result = eval_where(test[0]) | ||||
|         self.assertTrue(result == test[1], "%s is not %s" % (result, test[1])) | ||||
|  | ||||
|     def test_01(self): | ||||
|         self.check( | ||||
|             (lambda family: (family.private and  | ||||
|                              family.mother_handle.gramps_id != "I0001"),  | ||||
|              ['AND', [['private', '==', True],  | ||||
|                       ['mother_handle.gramps_id', '!=', 'I0001']]])) | ||||
|  | ||||
|     def test_02(self): | ||||
|         self.check( | ||||
|             (lambda person: LIKE(person.gramps_id, "I0001"), | ||||
|              ['gramps_id', 'LIKE', 'I0001'])) | ||||
|  | ||||
|     def test_03(self): | ||||
|         self.check( | ||||
|             (lambda note: note.gramps_id == "N0001", | ||||
|              ['gramps_id', '==', 'N0001'])) | ||||
|  | ||||
|     def test_04(self): | ||||
|         self.check( | ||||
|             (lambda person: person.event_ref_list.ref.gramps_id == "E0001", | ||||
|              ['event_ref_list.ref.gramps_id', '==', 'E0001'])) | ||||
|  | ||||
|     def test_05(self): | ||||
|         self.check( | ||||
|             (lambda person: LIKE(person.gramps_id, "I0001") or person.private, | ||||
|              ["OR", [['gramps_id', 'LIKE', 'I0001'], | ||||
|                      ["private", "==", True]]])) | ||||
|  | ||||
|     def test_06(self): | ||||
|         self.check( | ||||
|             (lambda person: person.event_ref_list <= 0, | ||||
|              ["event_ref_list", "<=", 0])) | ||||
|  | ||||
|     def test_07(self): | ||||
|         self.check( | ||||
|             (lambda person: person.primary_name.surname_list[0].surname == "Smith", | ||||
|              ["primary_name.surname_list.0.surname", "==", "Smith"])) | ||||
|  | ||||
|     def test_08(self): | ||||
|         self.check( | ||||
|             (make_closure("Smith"), | ||||
|              ["AND", [["primary_name.surname_list.0.surname", "==", "Smith"], | ||||
|                       ["gender", "==", 1]]])) | ||||
|  | ||||
|     def test_09(self): | ||||
|         self.check( | ||||
|             [Thing().where(), ["gramps_id", "==", "I1"]]) | ||||
|  | ||||
|     def test_10(self): | ||||
|         self.check( | ||||
|             (lambda person: LIKE(person.gramps_id, "I000%"), | ||||
|              ["gramps_id", "LIKE", "I000%"])) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     unittest.main() | ||||
							
								
								
									
										151
									
								
								gramps/gen/db/where.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								gramps/gen/db/where.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| # | ||||
| # Gramps - a GTK+/GNOME based genealogy program | ||||
| # | ||||
| # Copyright (C) 2016       Gramps Development Team | ||||
| # | ||||
| # This program is free software; you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation; either version 2 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program; if not, write to the Free Software | ||||
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA | ||||
| # | ||||
|  | ||||
| from meta.asttools import Visitor | ||||
| from meta.decompiler import _ast, decompile_func | ||||
|  | ||||
| import copy | ||||
|  | ||||
| class ParseFilter(Visitor): | ||||
|     def visitName(self, node): | ||||
|         return node.id | ||||
|      | ||||
|     def visitNum(self, node): | ||||
|         return node.n | ||||
|          | ||||
|     def visitlong(self, node): | ||||
|         return node | ||||
|  | ||||
|     def process_expression(self, expr): | ||||
|         if isinstance(expr, str): | ||||
|             # boolean | ||||
|             return [self.process_field(expr), "==", True] | ||||
|         elif len(expr) == 3: | ||||
|             # (field, op, value) | ||||
|             return [self.process_field(expr[0]),  | ||||
|                     expr[1],  | ||||
|                     self.process_value(expr[2])] | ||||
|         else: | ||||
|             # list of exprs | ||||
|             return [self.process_expression(exp) for | ||||
|                     exp in expr] | ||||
|  | ||||
|     def process_value(self, value): | ||||
|         try: | ||||
|             return eval(value, self.env) | ||||
|         except: | ||||
|             return value | ||||
|  | ||||
|     def process_field(self, field): | ||||
|         field = field.replace("[", ".").replace("]", "") | ||||
|         if field.startswith(self.parameter + "."): | ||||
|             return field[len(self.parameter) + 1:] | ||||
|         else: | ||||
|             return field | ||||
|  | ||||
|     def visitCall(self, node): | ||||
|         """ | ||||
|         Handle LIKE() | ||||
|         """ | ||||
|         return [self.process_field(self.visit(node.args[0])),  | ||||
|                 self.visit(node.func), | ||||
|                 self.process_value(self.visit(node.args[1]))] | ||||
|  | ||||
|     def visitStr(self, node): | ||||
|         return node.s | ||||
|  | ||||
|     def visitlist(self, list): | ||||
|         return [self.visit(node) for node in list] | ||||
|  | ||||
|     def visitCompare(self, node): | ||||
|         return [self.process_field(self.visit(node.left)),  | ||||
|                 " ".join(self.visit(node.ops)),  | ||||
|                 self.process_value(self.visit(node.comparators[0]))] | ||||
|  | ||||
|     def visitAttribute(self, node): | ||||
|         return "%s.%s" % (self.visit(node.value), node.attr) | ||||
|  | ||||
|     def get_boolean_op(self, node): | ||||
|         if isinstance(node, _ast.And): | ||||
|             return "AND" | ||||
|         elif isinstance(node, _ast.Or): | ||||
|             return "OR" | ||||
|         else: | ||||
|             raise Exception("invalid boolean") | ||||
|  | ||||
|     def visitNotEq(self, node): | ||||
|         return "!=" | ||||
|  | ||||
|     def visitLtE(self, node): | ||||
|         return "<=" | ||||
|  | ||||
|     def visitGtE(self, node): | ||||
|         return ">=" | ||||
|  | ||||
|     def visitEq(self, node): | ||||
|         return "==" | ||||
|  | ||||
|     def visitBoolOp(self, node): | ||||
|         """ | ||||
|         BoolOp: boolean operator | ||||
|         """ | ||||
|         op = self.get_boolean_op(node.op) | ||||
|         values = list(node.values) | ||||
|         return [op, self.process_expression( | ||||
|             [self.visit(value) for value in values])] | ||||
|  | ||||
|     def visitLambda(self, node): | ||||
|         self.parameter = self.visit(node.args)[0] | ||||
|         return self.visit(node.body) | ||||
|  | ||||
|     def visitarguments(self, node): | ||||
|         return [self.visit(arg) for arg in node.args] | ||||
|  | ||||
|     def visitarg(self, node): | ||||
|         return node.arg | ||||
|  | ||||
|     def visitSubscript(self, node): | ||||
|         return "%s[%s]" % (self.visit(node.value),  | ||||
|                           self.visit(node.slice)) | ||||
|  | ||||
|     def visitIndex(self, node): | ||||
|         return self.visit(node.value) | ||||
|  | ||||
| def make_env(closure): | ||||
|     """ | ||||
|     Create an environment from the closure. | ||||
|     """ | ||||
|     env = copy.copy(closure.__globals__) | ||||
|     if closure.__closure__: | ||||
|         for i in range(len(closure.__closure__)): | ||||
|             env[closure.__code__.co_freevars[i]] = closure.__closure__[i].cell_contents | ||||
|     return env | ||||
|  | ||||
| def eval_where(closure): | ||||
|     """ | ||||
|     Given a closure, parse and evaluate it. | ||||
|     Return a WHERE expression. | ||||
|     """ | ||||
|     parser = ParseFilter() | ||||
|     parser.env = make_env(closure) | ||||
|     ast_top = decompile_func(closure) | ||||
|     result = parser.visit(ast_top) | ||||
|     return result | ||||
|  | ||||
| @@ -123,12 +123,12 @@ class BSDDBTest(unittest.TestCase): | ||||
|         self.assertTrue(len(result) == 60, len(result)) | ||||
|  | ||||
|     def test_queryset_2(self): | ||||
|         result = list(self.db.Person.filter(gramps_id__LIKE="I000%").select()) | ||||
|         result = list(self.db.Person.where(lambda person: LIKE(person.gramps_id, "I000%")).select()) | ||||
|         self.assertTrue(len(result) == 10, len(result)) | ||||
|  | ||||
|     def test_queryset_3(self): | ||||
|         result = list(self.db.Family | ||||
|                       .filter(mother_handle__gramps_id__LIKE="I003%") | ||||
|                       .where(lambda family: LIKE(family.mother_handle.gramps_id, "I003%")) | ||||
|                       .select()) | ||||
|         self.assertTrue(len(result) == 6, result) | ||||
|  | ||||
| @@ -138,7 +138,7 @@ class BSDDBTest(unittest.TestCase): | ||||
|  | ||||
|     def test_queryset_4b(self): | ||||
|         result = list(self.db.Family | ||||
|                       .filter(mother_handle__event_ref_list__ref__gramps_id='E0156') | ||||
|                       .where(lambda family: family.mother_handle.event_ref_list.ref.gramps_id == 'E0156') | ||||
|                       .select()) | ||||
|         self.assertTrue(len(result) == 1, len(result)) | ||||
|  | ||||
| @@ -154,9 +154,8 @@ class BSDDBTest(unittest.TestCase): | ||||
|                         [r["mother_handle.event_ref_list.0"] for r in result]) | ||||
|  | ||||
|     def test_queryset_7(self): | ||||
|         from gramps.gen.db import NOT | ||||
|         result = list(self.db.Family | ||||
|                       .filter(NOT(mother_handle__event_ref_list__0=None)) | ||||
|                       .where(lambda family: family.mother_handle.event_ref_list[0] != None) | ||||
|                       .select()) | ||||
|         self.assertTrue(len(result) == 21, len(result)) | ||||
|  | ||||
| @@ -188,22 +187,28 @@ class BSDDBTest(unittest.TestCase): | ||||
|         self.assertTrue(result == 60, result) | ||||
|  | ||||
|     def test_tag_1(self): | ||||
|         self.db.Person.filter(gramps_id="I0001").tag("Test") | ||||
|         result = self.db.Person.filter(tag_list__name="Test").count() | ||||
|         self.db.Person.where(lambda person: person.gramps_id == "I0001").tag("Test") | ||||
|         result = self.db.Person.where(lambda person: person.tag_list.name == "Test").count() | ||||
|         self.assertTrue(result == 1, result) | ||||
|  | ||||
|     # def test_filter_1(self): | ||||
|     #     from gramps.gen.filters.rules.person import (IsDescendantOf, | ||||
|     #                                                  IsAncestorOf) | ||||
|     #     from gramps.gen.filters import GenericFilter | ||||
|     #     filter = GenericFilter() | ||||
|     #     filter.set_logical_op("or") | ||||
|     #     filter.add_rule(IsDescendantOf([self.db.get_default_person().gramps_id, | ||||
|     #                                     True])) | ||||
|     #     filter.add_rule(IsAncestorOf([self.db.get_default_person().gramps_id, | ||||
|     #                                   True])) | ||||
|     #     result = self.db.Person.filter(filter).count() | ||||
|     #     self.assertTrue(result == 15, result) | ||||
|     def test_filter_1(self): | ||||
|         from gramps.gen.filters.rules.person import (IsDescendantOf, | ||||
|                                                      IsAncestorOf) | ||||
|         from gramps.gen.filters import GenericFilter | ||||
|         filter = GenericFilter() | ||||
|         filter.set_logical_op("or") | ||||
|         filter.add_rule(IsDescendantOf([self.db.get_default_person().gramps_id, | ||||
|                                         True])) | ||||
|         filter.add_rule(IsAncestorOf([self.db.get_default_person().gramps_id, | ||||
|                                       True])) | ||||
|         result = self.db.Person.filter(filter).count() | ||||
|         self.assertTrue(result == 15, result) | ||||
|         filter.where = lambda person: person.private == True | ||||
|         result = self.db.Person.filter(filter).count() | ||||
|         self.assertTrue(result == 1, result) | ||||
|         filter.where = lambda person: person.private != True | ||||
|         result = self.db.Person.filter(filter).count() | ||||
|         self.assertTrue(result == 14, result) | ||||
|  | ||||
|     def test_filter_2(self): | ||||
|         result = self.db.Person.filter(lambda p: p.private).count() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user