Skip to content

Commit

Permalink
Results wrapper (#8)
Browse files Browse the repository at this point in the history
* Add an option to locate results, and be able to directly pass a sorting index.

* Fix an issue if you pass None for start.

* Add wrapper.

* Update and fix tests.

* update changelog
  • Loading branch information
janwijbrand committed Jul 17, 2017
1 parent 31e353b commit 38f5a87
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 30 deletions.
6 changes: 5 additions & 1 deletion CHANGES.txt
Expand Up @@ -4,8 +4,12 @@ CHANGES
2.5 (unreleased)
----------------

- Nothing changed yet.
- `sort_field` can be a index name or an object providing `IIndexSort` itself.

- `searchResults()` accepts optional parameter `locate_to` and `wrapper`. The
`locate_to` is used as the `__parent__` for the location proxy put arround
the resulting objects. The `wrapper` is a callable callback that should
accept one argument for its parameter.

2.4 (2017-06-22)
----------------
Expand Down
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -67,6 +67,7 @@ def read(*rnames):
'zope.index',
'zope.interface',
'zope.intid',
'zope.location',
],
tests_require=tests_require,
extras_require={'test': tests_require},
Expand Down
59 changes: 43 additions & 16 deletions src/hurry/query/query.py
Expand Up @@ -35,6 +35,7 @@
from zope.intid.interfaces import IIntIds
from zope.index.interfaces import IIndexSort
from zope.index.text.parsetree import ParseError
from zope.location.location import located, LocationProxy
from hurry.query import interfaces

import transaction
Expand Down Expand Up @@ -98,17 +99,37 @@ def reset(self):
transaction_cache = Cache(transaction.manager)


class Locator(object):

def __init__(self, container, get):
self.container = container
self.get = get

def __call__(self, oid):
contained = self.get(oid)
return located(
LocationProxy(contained), self.container, contained.__name__)


class Results(object):
implements(interfaces.IResults)

def __init__(self, context, all_results, selected_results):
def __init__(self, context, all_results, selected_results,
wrapper=None, locate_to=None):
self.context = context
self.locate_to = locate_to
self.wrapper = wrapper
self.__all = all_results
self.__selected = selected_results

@Lazy
def get(self):
return getUtility(IIntIds, '', self.context).getObject
get = getUtility(IIntIds, '', self.context).getObject
if self.wrapper is not None:
get = (lambda get: lambda id: self.wrapper(get(id)))(get)
if self.locate_to is not None:
return Locator(self.locate_to, get)
return get

@property
def total(self):
Expand Down Expand Up @@ -143,7 +164,7 @@ def __len__(self):
return 0

def __iter__(self):
raise StopIteration()
return iter([])


no_results = NoResults()
Expand Down Expand Up @@ -227,7 +248,8 @@ class Query(object):

def searchResults(
self, query, context=None, sort_field=None, limit=None,
reverse=False, start=0, caching=False, timing=HURRY_QUERY_TIMING):
reverse=False, start=0, caching=False, timing=HURRY_QUERY_TIMING,
wrapper=None, locate_to=None):

if context is None:
context = getSiteManager()
Expand All @@ -249,7 +271,7 @@ def searchResults(
if not all_results:
if timer is not None:
timer.report(over=timing)
return Results(context, [], [])
return no_results

if timer is not None:
timer.start_post()
Expand All @@ -259,17 +281,21 @@ def searchResults(
# Like in zope.catalog's searchResults we require the given
# index to sort on to provide IIndexSort. We bail out if
# the index does not.
catalog_name, index_name = sort_field
catalog = getUtility(ICatalog, catalog_name, context)
index = catalog[index_name]
if not IIndexSort.providedBy(index):
raise ValueError(
'Index {} in catalog {} does not support '
'sorting.'.format(index_name, catalog_name))
if not IIndexSort.providedBy(sort_field):
assert isinstance(sort_field, tuple) and len(sort_field) == 2
catalog_name, index_name = sort_field
catalog = getUtility(ICatalog, catalog_name, context)
sort_field = catalog[index_name]
if not IIndexSort.providedBy(sort_field):
raise ValueError(
'Index {} in catalog {} does not support '
'sorting.'.format(index_name, catalog_name))
sort_limit = None
if limit is not None:
sort_limit = start + limit
selected_results = index.sort(
if limit:
sort_limit = limit
if start:
sort_limit += start
selected_results = sort_field.sort(
all_results,
limit=sort_limit,
reverse=reverse)
Expand Down Expand Up @@ -298,7 +324,8 @@ def searchResults(
timer.end_post()
timer.report(over=timing)

return Results(context, all_results, selected_results)
return Results(
context, all_results, selected_results, wrapper, locate_to)


class Term(object):
Expand Down
77 changes: 64 additions & 13 deletions src/hurry/query/query.txt
Expand Up @@ -37,6 +37,8 @@ And its implementation::
... self.t2 = t2
... def __cmp__(self, other):
... return cmp(self.id, other.id)
... def __repr__(self):
... return '<Content "{}">'.format(self.id)

The id attribute is just so we can identify objects we find again
easily. By including the __cmp__ method we make sure search results
Expand Down Expand Up @@ -494,39 +496,49 @@ performing any sorting here ourselves.
>>> def displayResult(q, context=None, **kw):
... query = getUtility(IQuery)
... r = query.searchResults(q, context, **kw)
... return [e.id for e in r]
... return [e for e in r]

Without using sorting in the query itself, the resultset has an undefined
order. We "manually" sort the results here to have something testable.

>>> f1 = ('catalog1', 'f1')
>>> [r for r in sorted(displayResult(Eq(f1, 'a')))]
[1, 2, 6]
[<Content "1">, <Content "2">, <Content "6">]

Now we sort on the f2 index.

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), sort_field=('catalog1', 'f2'))
[6, 1, 2]
[<Content "6">, <Content "1">, <Content "2">]

Reverse the order.

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), sort_field=('catalog1', 'f2'), reverse=True)
[2, 1, 6]
[<Content "2">, <Content "1">, <Content "6">]

We can limit the amount of found items.

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), sort_field=('catalog1', 'f2'), limit=2)
[6, 1]
[<Content "6">, <Content "1">]

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), sort_field=('catalog1', 'f2'), limit=2, start=1)
[<Content "1">, <Content "2">]

We can limit the reversed resultset too.

>>> f1 = ('catalog1', 'f1')
>>> displayResult(
... Eq(f1, 'a'), sort_field=('catalog1', 'f2'), limit=2, reverse=True)
[2, 1]
[<Content "2">, <Content "1">]

You can directly pass the index as a sort field instead of a tuple:

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), sort_field=catalog['f2'])
[<Content "6">, <Content "1">, <Content "2">]

Whenever a field is used for sorting that does not support is, an error is
raised.
Expand All @@ -544,20 +556,59 @@ the tested index is deterministic enough to be used as a proper test).

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), limit=2)
[1, 2]
[<Content "1">, <Content "2">]

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), start=1)
[2, 6]
[<Content "2">, <Content "6">]

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), start=1, limit=1)
[2]
[<Content "2">]

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), limit=2, reverse=True)
[6, 2]
[<Content "6">, <Content "2">]


Wrapper
-------

You can define a wrapper to be called on each result:

>>> from zope.location import Location
>>> class Wrapper(Location):
... def __init__(self, parent):
... self.parent = parent
... def __repr__(self):
... return '<Wrapper "{}">'.format(self.parent.id)

>>> f1 = ('catalog1', 'f1')
>>> displayResult(Eq(f1, 'a'), wrapper=Wrapper)
[<Wrapper "1">, <Wrapper "2">, <Wrapper "6">]

Locate to
---------

You can define a location where the results should be located with a proxy:

>>> def displayParent(q, context=None, **kw):
... query = getUtility(IQuery)
... r = query.searchResults(q, context, **kw)
... return [(e.__parent__, e) or None for e in r]

>>> f1 = ('catalog1', 'f1')
>>> displayParent(Eq(f1, 'a'), limit=2)
[(None, <Content "1">), (None, <Content "2">)]

>>> parent = Content('parent')
>>> displayParent(Eq(f1, 'a'), limit=2, locate_to=parent)
[(<Content "parent">, <Content "1">), (<Content "parent">, <Content "2">)]

This can be used with a wrapper:

>>> displayParent(Eq(f1, 'a'), limit=2, wrapper=Wrapper, locate_to=parent)
[(<Content "parent">, <Wrapper "1">), (<Content "parent">, <Wrapper "2">)]

Text index
----------
Expand All @@ -567,7 +618,7 @@ You can search on text, here all the items that contains better::
>>> from hurry.query import Text
>>> t1 = ('catalog1', 't')
>>> displayResult(Text(t1, 'better'))
[1, 2, 3, 4]
[<Content "1">, <Content "2">, <Content "3">, <Content "4">]

Invalid text query returns an empty results::

Expand All @@ -583,12 +634,12 @@ have a as f1::

>>> from hurry.query import Difference
>>> displayResult(Difference(Text(t1, 'better'), Eq(f1, 'a')))
[3, 4]
[<Content "3">, <Content "4">]


There is a special term that allows to mix objects with catalog
queries::

>>> from hurry.query import Objects
>>> displayResult(Objects(content))
[1, 2, 3, 4, 5, 6]
[<Content "1">, <Content "2">, <Content "3">, <Content "4">, <Content "5">, <Content "6">]

0 comments on commit 38f5a87

Please sign in to comment.