From 218960412c0db1de579fe7dcbbfa5b3b767c1401 Mon Sep 17 00:00:00 2001 From: Levi Starrett Date: Tue, 12 Jun 2018 15:49:44 -0400 Subject: [PATCH 1/4] add ability to order query sets for select_many --- tests/test_xtuml/test_metamodel.py | 19 ++++++++++ xtuml/__init__.py | 2 + xtuml/meta.py | 60 ++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/tests/test_xtuml/test_metamodel.py b/tests/test_xtuml/test_metamodel.py index 4b0e817..7268553 100644 --- a/tests/test_xtuml/test_metamodel.py +++ b/tests/test_xtuml/test_metamodel.py @@ -21,6 +21,8 @@ import xtuml from bridgepoint import ooaofooa from xtuml import where_eq as where +from xtuml import order_by +from xtuml import reverse_order_by class TestModel(unittest.TestCase): @@ -66,6 +68,23 @@ def test_select_any_where(self): m = self.metamodel inst = m.select_any('S_DT', where(Name='void')) self.assertEqual(inst.Name, 'void') + + def test_select_many_ordered_by(self): + m = self.metamodel + q = m.select_many('S_DT', None, order_by('Name', 'DT_ID')) + prev_inst = None + for inst in q: + if not prev_inst is None: + self.assertTrue( + getattr(inst, 'Name') > getattr(prev_inst, 'Name') or ( + getattr(inst, 'Name') == getattr(prev_inst, 'Name') and getattr(inst, 'DT_ID') >= getattr(prev_inst, 'DT_ID') ) ) + prev_inst = inst + + def test_select_many_reverse_ordered_by(self): + m = self.metamodel + q1 = m.select_many('S_DT', None, order_by('Name', 'DT_ID')) + q2 = m.select_many('S_DT', None, reverse_order_by('Name', 'DT_ID')) + self.assertEqual(q1, reversed(q2)) def test_empty(self): m = self.metamodel diff --git a/xtuml/__init__.py b/xtuml/__init__.py index f4b01a7..9ca7f97 100644 --- a/xtuml/__init__.py +++ b/xtuml/__init__.py @@ -78,6 +78,8 @@ from .meta import sort_reflexive from .meta import get_metaclass from .meta import get_metamodel +from .meta import order_by +from .meta import reverse_order_by from .consistency_check import check_association_integrity from .consistency_check import check_uniqueness_constraint diff --git a/xtuml/meta.py b/xtuml/meta.py index 2a78945..107662a 100644 --- a/xtuml/meta.py +++ b/xtuml/meta.py @@ -363,6 +363,12 @@ class QuerySet(xtuml.OrderedSet): ''' An ordered set which holds instances that match queries. ''' + def __init__(self, iterable=None, ordered_by=None): + if not ordered_by is None: + super(QuerySet, self).__init__(sorted(iterable, ordered_by)) + else: + super(QuerySet, self).__init__(iterable) + @property def first(self): ''' @@ -657,7 +663,7 @@ def select_one(self, where_clause=None): return next(s, None) - def select_many(self, where_clause=None): + def select_many(self, where_clause=None, ordered_by=None): ''' Select several instances from the instance pool. Optionally, a conditional *where-clause* in the form of a function may be provided. @@ -669,7 +675,7 @@ def select_many(self, where_clause=None): else: s = iter(self.storage) - return QuerySet(s) + return QuerySet(s, ordered_by) def _find_assoc_links(self, kind, rel_id, phrase=''): key = (kind.upper(), rel_id, phrase) @@ -1067,6 +1073,52 @@ def cardinality(instance_or_set): return len(instance_or_set) +class OrderBy(tuple): + ''' + Helper class to create a tuple of key values for sorting an + instance set. + ''' + def __call__(self, x, y): + for attr in self: + if not hasattr(x, attr): + raise MetaException("Class '%s' has no attribute '%s'" % (get_metaclass(x).kind, attr)) + if not hasattr(y, attr): + raise MetaException("Class '%s' has no attribute '%s'" % (get_metaclass(y).kind, attr)) + + return cmp( tuple(getattr(x, attr) for attr in self), tuple(getattr(y, attr) for attr in self) ) + + +def order_by(*attrs): + ''' + Return a selection ordering comparator that will order an instance + set based on attribute names passed. When ordering on multiple + attributes is specified, the set will be sorted by the first + attribute and then within each value of this, by the second + attribute and so on. + + Usage example: + + >>> from xtuml import order_by + >>> m = xtuml.load_metamodel('db.sql') + >>> inst = m.select_many('My_Modeled_Class', None, order_by( 'Name', 'Number' ) ) + ''' + return OrderBy(attrs) + + +def reverse_order_by(*attrs): + ''' + Return a selection ordering comparator with the same behavior as + the order_by function but reversed order. + + Usage example: + + >>> from xtuml import reverse_order_by + >>> m = xtuml.load_metamodel('db.sql') + >>> inst = m.select_many('My_Modeled_Class', None, reverse_order_by( 'Name', 'Number' ) ) + ''' + return lambda x, y: order_by(*attrs)(x, y) * -1; + + class MetaModel(object): ''' A metamodel contains metaclasses with associations between them. @@ -1203,7 +1255,7 @@ def define_unique_identifier(self, kind, name, *named_attributes): metaclass.indices[name] = tuple(named_attributes) metaclass.identifying_attributes |= set(named_attributes) - def select_many(self, kind, where_clause=None): + def select_many(self, kind, where_clause=None, ordered_by=None): ''' Query the metamodel for a set of instances of some *kind*. Optionally, a conditional *where-clause* in the form of a function may be provided. @@ -1214,7 +1266,7 @@ def select_many(self, kind, where_clause=None): >>> inst_set = m.select_many('My_Class', lambda sel: sel.number > 5) ''' metaclass = self.find_metaclass(kind) - return metaclass.select_many(where_clause) + return metaclass.select_many(where_clause, ordered_by) def select_one(self, kind, where_clause=None): ''' From fd7d8590466da32f6d744e1cffb0c5d61dc6e33d Mon Sep 17 00:00:00 2001 From: Levi Starrett Date: Wed, 13 Jun 2018 15:52:30 -0400 Subject: [PATCH 2/4] address order_by review --- xtuml/meta.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/xtuml/meta.py b/xtuml/meta.py index 107662a..db485ba 100644 --- a/xtuml/meta.py +++ b/xtuml/meta.py @@ -363,9 +363,9 @@ class QuerySet(xtuml.OrderedSet): ''' An ordered set which holds instances that match queries. ''' - def __init__(self, iterable=None, ordered_by=None): - if not ordered_by is None: - super(QuerySet, self).__init__(sorted(iterable, ordered_by)) + def __init__(self, iterable=None, order_by=None): + if not order_by is None: + super(QuerySet, self).__init__(sorted(iterable, order_by)) else: super(QuerySet, self).__init__(iterable) @@ -663,7 +663,7 @@ def select_one(self, where_clause=None): return next(s, None) - def select_many(self, where_clause=None, ordered_by=None): + def select_many(self, where_clause=None, order_by=None): ''' Select several instances from the instance pool. Optionally, a conditional *where-clause* in the form of a function may be provided. @@ -675,7 +675,7 @@ def select_many(self, where_clause=None, ordered_by=None): else: s = iter(self.storage) - return QuerySet(s, ordered_by) + return QuerySet(s, order_by) def _find_assoc_links(self, kind, rel_id, phrase=''): key = (kind.upper(), rel_id, phrase) @@ -1085,7 +1085,7 @@ def __call__(self, x, y): if not hasattr(y, attr): raise MetaException("Class '%s' has no attribute '%s'" % (get_metaclass(y).kind, attr)) - return cmp( tuple(getattr(x, attr) for attr in self), tuple(getattr(y, attr) for attr in self) ) + return cmp(tuple(getattr(x, attr) for attr in self), tuple(getattr(y, attr) for attr in self)) def order_by(*attrs): @@ -1100,7 +1100,7 @@ def order_by(*attrs): >>> from xtuml import order_by >>> m = xtuml.load_metamodel('db.sql') - >>> inst = m.select_many('My_Modeled_Class', None, order_by( 'Name', 'Number' ) ) + >>> inst = m.select_many('My_Modeled_Class', None, order_by('Name', 'Number')) ''' return OrderBy(attrs) @@ -1114,7 +1114,7 @@ def reverse_order_by(*attrs): >>> from xtuml import reverse_order_by >>> m = xtuml.load_metamodel('db.sql') - >>> inst = m.select_many('My_Modeled_Class', None, reverse_order_by( 'Name', 'Number' ) ) + >>> inst = m.select_many('My_Modeled_Class', None, reverse_order_by('Name', 'Number')) ''' return lambda x, y: order_by(*attrs)(x, y) * -1; @@ -1255,7 +1255,7 @@ def define_unique_identifier(self, kind, name, *named_attributes): metaclass.indices[name] = tuple(named_attributes) metaclass.identifying_attributes |= set(named_attributes) - def select_many(self, kind, where_clause=None, ordered_by=None): + def select_many(self, kind, where_clause=None, order_by=None): ''' Query the metamodel for a set of instances of some *kind*. Optionally, a conditional *where-clause* in the form of a function may be provided. @@ -1266,7 +1266,7 @@ def select_many(self, kind, where_clause=None, ordered_by=None): >>> inst_set = m.select_many('My_Class', lambda sel: sel.number > 5) ''' metaclass = self.find_metaclass(kind) - return metaclass.select_many(where_clause, ordered_by) + return metaclass.select_many(where_clause, order_by) def select_one(self, kind, where_clause=None): ''' From 9c97f195063a4788bc3914aaa536d5c7d89e0eee Mon Sep 17 00:00:00 2001 From: Levi Starrett Date: Wed, 13 Jun 2018 16:20:29 -0400 Subject: [PATCH 3/4] simplified evaluation of order_by --- xtuml/meta.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/xtuml/meta.py b/xtuml/meta.py index db485ba..1cfe9c8 100644 --- a/xtuml/meta.py +++ b/xtuml/meta.py @@ -363,12 +363,6 @@ class QuerySet(xtuml.OrderedSet): ''' An ordered set which holds instances that match queries. ''' - def __init__(self, iterable=None, order_by=None): - if not order_by is None: - super(QuerySet, self).__init__(sorted(iterable, order_by)) - else: - super(QuerySet, self).__init__(iterable) - @property def first(self): ''' @@ -674,8 +668,11 @@ def select_many(self, where_clause=None, order_by=None): s = filter(where_clause, self.storage) else: s = iter(self.storage) + + if order_by: + s = sorted(s, order_by) - return QuerySet(s, order_by) + return QuerySet(s) def _find_assoc_links(self, kind, rel_id, phrase=''): key = (kind.upper(), rel_id, phrase) From c0056d1759899cb279c92781a6b8cec09ee196f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20T=C3=B6rnblom?= Date: Fri, 15 Jun 2018 01:02:05 +0200 Subject: [PATCH 4/4] meta: generalize where_eq() and order_by() into the concept query operators. This also fixes python3 issues due to deprecation of the cmp key to sorted() --- docs/api-reference.rst | 2 + xtuml/meta.py | 163 +++++++++++++++++++++-------------------- 2 files changed, 87 insertions(+), 78 deletions(-) diff --git a/docs/api-reference.rst b/docs/api-reference.rst index 3678efe..941b6bc 100644 --- a/docs/api-reference.rst +++ b/docs/api-reference.rst @@ -27,6 +27,8 @@ Metamodel Operations .. autofunction:: xtuml.delete .. autofunction:: xtuml.cardinality .. autofunction:: xtuml.where_eq +.. autofunction:: xtuml.order_by +.. autofunction:: xtuml.reverse_order_by .. autofunction:: xtuml.sort_reflexive .. autofunction:: xtuml.get_metamodel .. autofunction:: xtuml.get_metaclass diff --git a/xtuml/meta.py b/xtuml/meta.py index 1cfe9c8..d7ecf86 100644 --- a/xtuml/meta.py +++ b/xtuml/meta.py @@ -133,6 +133,27 @@ def _is_null(instance, name): return False +def apply_query_operators(iterable, ops): + ''' + Apply a series of query operators to a sequence of instances, e.g. + where_eq(), order_by() or filter functions. + ''' + for op in ops: + if isinstance(op, WhereEqual): + iterable = op(iterable) + + elif isinstance(op, OrderBy): + iterable = op(iterable) + + elif isinstance(op, dict): + iterable = WhereEqual(op)(iterable) + + else: + iterable = filter(op, iterable) + + return iterable + + class Association(object): ''' An association connects two metaclasses to each other via two directed @@ -643,36 +664,26 @@ def delete(self, instance, disconnect=True): for other in link[instance]: unrelate(instance, other, link.rel_id, link.phrase) - def select_one(self, where_clause=None): + def select_one(self, *args): ''' - Select a single instance from the instance pool. Optionally, a - conditional *where-clause* in the form of a function may be provided. + Select a single instance from the instance pool. Query operators such as + where_eq(), order_by() or filter functions may be passed as optional + arguments. ''' - if isinstance(where_clause, dict): - s = self.query(where_clause) - elif where_clause: - s = iter(filter(where_clause, self.storage)) - else: - s = iter(self.storage) - - return next(s, None) + s = apply_query_operators(self.storage, args) + return next(iter(s), None) - def select_many(self, where_clause=None, order_by=None): + def select_many(self, *args): ''' - Select several instances from the instance pool. Optionally, - a conditional *where-clause* in the form of a function may be provided. + Select several instances from the instance pool. Query operators such as + where_eq(), order_by() or filter functions may be passed as optional + arguments. ''' - if isinstance(where_clause, dict): - s = self.query(where_clause) - elif where_clause: - s = filter(where_clause, self.storage) + s = apply_query_operators(self.storage, args) + if isinstance(s, QuerySet): + return s else: - s = iter(self.storage) - - if order_by: - s = sorted(s, order_by) - - return QuerySet(s) + return QuerySet(s) def _find_assoc_links(self, kind, rel_id, phrase=''): key = (kind.upper(), rel_id, phrase) @@ -708,13 +719,7 @@ def query(self, dictonary_of_values): Query the instance pool for instances with attributes that match a given *dictonary of values*. ''' - items = collections.deque(dictonary_of_values.items()) - for inst in iter(self.storage): - for name, value in iter(items): - if getattr(inst, name) != value: - break - else: - yield inst + return WhereEqual(dictonary_of_values)(self.storage) class NavChain(object): @@ -776,32 +781,29 @@ def __getitem__(self, args): return self.nav(self._kind, relid, phrase) - def __call__(self, where_clause=None): + def __call__(self, *args): ''' - The navigation chain is invoked. Optionally, a conditional - *where-clause* in the form of a function may be provided, e.g - + The navigation chain is invoked. Query operators such as where_eq(), + order_by() or filter functions may be passed as optional arguments, e.g. + >>> chain(lambda selected: selected.Name == 'test') ''' handle = self.handle or list() - if where_clause: - handle = filter(where_clause, handle) - - return QuerySet(handle) + handle = apply_query_operators(handle, args) + if isinstance(handle, QuerySet): + return handle + else: + return QuerySet(handle) class NavOneChain(NavChain): ''' A navigation chain that yeilds an instance, or None. ''' - def __call__(self, where_clause=None): - handle = self.handle or iter([]) - if not where_clause: - return next(handle, None) - - for inst in handle: - if where_clause(inst): - return inst + def __call__(self, *args): + handle = self.handle or list() + handle = apply_query_operators(handle, args) + return next(iter(handle), None) def navigate_one(instance): @@ -880,12 +882,14 @@ class WhereEqual(dict): Helper class to create a dictonary of values for queries using python keyword arguments to *where_eq()* ''' - def __call__(self, selected): - for name in self: - if getattr(selected, name) != self.get(name): - return False - - return True + def __call__(self, s): + items = collections.deque(self.items()) + for inst in iter(s): + for name, value in iter(items): + if getattr(inst, name) != value: + break + else: + yield inst def where_eq(**kwargs): @@ -1070,25 +1074,26 @@ def cardinality(instance_or_set): return len(instance_or_set) -class OrderBy(tuple): +class OrderBy(list): ''' Helper class to create a tuple of key values for sorting an instance set. ''' - def __call__(self, x, y): - for attr in self: - if not hasattr(x, attr): - raise MetaException("Class '%s' has no attribute '%s'" % (get_metaclass(x).kind, attr)) - if not hasattr(y, attr): - raise MetaException("Class '%s' has no attribute '%s'" % (get_metaclass(y).kind, attr)) - - return cmp(tuple(getattr(x, attr) for attr in self), tuple(getattr(y, attr) for attr in self)) + reverse = False + + def __init__(self, attrs, reverse=False): + list.__init__(self, attrs) + self.reverse = reverse + + def __call__(self, s): + key = lambda el: [getattr(el, name) for name in self] + return sorted(s, key=key, reverse=self.reverse) def order_by(*attrs): ''' - Return a selection ordering comparator that will order an instance - set based on attribute names passed. When ordering on multiple + Return a query ordering operator that will order an instance + set based on attribute names passed. When ordering on multiple attributes is specified, the set will be sorted by the first attribute and then within each value of this, by the second attribute and so on. @@ -1097,23 +1102,23 @@ def order_by(*attrs): >>> from xtuml import order_by >>> m = xtuml.load_metamodel('db.sql') - >>> inst = m.select_many('My_Modeled_Class', None, order_by('Name', 'Number')) + >>> inst = m.select_many('My_Class', order_by('Name', 'Number')) ''' - return OrderBy(attrs) + return OrderBy(attrs, reverse=False) def reverse_order_by(*attrs): ''' - Return a selection ordering comparator with the same behavior as - the order_by function but reversed order. + Return a reversed query ordering operator with the same behavior as + order_by() but reversed order. Usage example: >>> from xtuml import reverse_order_by >>> m = xtuml.load_metamodel('db.sql') - >>> inst = m.select_many('My_Modeled_Class', None, reverse_order_by('Name', 'Number')) + >>> inst = m.select_many('My_Class', reverse_order_by('Name', 'Number')) ''' - return lambda x, y: order_by(*attrs)(x, y) * -1; + return OrderBy(attrs, reverse=True) class MetaModel(object): @@ -1252,10 +1257,11 @@ def define_unique_identifier(self, kind, name, *named_attributes): metaclass.indices[name] = tuple(named_attributes) metaclass.identifying_attributes |= set(named_attributes) - def select_many(self, kind, where_clause=None, order_by=None): + def select_many(self, kind, *args): ''' - Query the metamodel for a set of instances of some *kind*. Optionally, - a conditional *where-clause* in the form of a function may be provided. + Query the metamodel for a set of instances of some *kind*. Query + operators such as where_eq(), order_by() or filter functions may be + passed as optional arguments. Usage example: @@ -1263,12 +1269,13 @@ def select_many(self, kind, where_clause=None, order_by=None): >>> inst_set = m.select_many('My_Class', lambda sel: sel.number > 5) ''' metaclass = self.find_metaclass(kind) - return metaclass.select_many(where_clause, order_by) + return metaclass.select_many(*args) - def select_one(self, kind, where_clause=None): + def select_one(self, kind, *args): ''' - Query the metamodel for a single instance of some *kind*. Optionally, a - conditional *where-clause* in the form of a function may be provided. + Query the metamodel for a single instance of some *kind*. Query + operators such as where_eq(), order_by() or filter functions may be + passed as optional arguments. Usage example: @@ -1276,7 +1283,7 @@ def select_one(self, kind, where_clause=None): >>> inst = m.select_one('My_Class', lambda sel: sel.name == 'Test') ''' metaclass = self.find_metaclass(kind) - return metaclass.select_one(where_clause) + return metaclass.select_one(*args) # Backwards compatibility with older versions of pyxtuml select_any = select_one