Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tests/test_xtuml/test_metamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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'))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking, it should be possible to to make the where_clause and order_by argument less independent on the order in the parameter list. Something like this:

def select_many(kind, set_modifiers*):
s = map(apply_set_operator, set_modifiers)
return s

then you could type:
q = m.select_many('S_DT', order_by('Name', 'DT_ID'), where(...))
or
q = m.select_many('S_DT', where(...), order_by('Name', 'DT_ID'))
or
q = m.select_many('S_DT', order_by('Name', 'DT_ID'))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm. I will give this some thought. Of course the difficulty is how to implement the 'apply_set_operator' part

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map() might have been a bad idea. I slapped together a gist demonstrating one way of doing it. needs some cleaning though...

See https://gist.github.com/john-tornblom/21eca04788c3980778039bf91866e7dd

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
Expand Down
2 changes: 2 additions & 0 deletions xtuml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 114 additions & 58 deletions xtuml/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -643,33 +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):
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)

return QuerySet(s)
return QuerySet(s)

def _find_assoc_links(self, kind, rel_id, phrase=''):
key = (kind.upper(), rel_id, phrase)
Expand Down Expand Up @@ -705,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):
Expand Down Expand Up @@ -773,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):
Expand Down Expand Up @@ -877,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):
Expand Down Expand Up @@ -1067,6 +1074,53 @@ def cardinality(instance_or_set):
return len(instance_or_set)


class OrderBy(list):
'''
Helper class to create a tuple of key values for sorting an
instance set.
'''
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 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.

Usage example:

>>> from xtuml import order_by
>>> m = xtuml.load_metamodel('db.sql')
>>> inst = m.select_many('My_Class', order_by('Name', 'Number'))
'''
return OrderBy(attrs, reverse=False)


def reverse_order_by(*attrs):
'''
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_Class', reverse_order_by('Name', 'Number'))
'''
return OrderBy(attrs, reverse=True)


class MetaModel(object):
'''
A metamodel contains metaclasses with associations between them.
Expand Down Expand Up @@ -1203,31 +1257,33 @@ 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, *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:

>>> m = xtuml.load_metamodel('db.sql')
>>> 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(*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:

>>> m = xtuml.load_metamodel('db.sql')
>>> 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
Expand Down