lazr.batchnavigator-1.2.10/ 0000755 0001750 0000144 00000000000 11634122075 014361 5 ustar abel users lazr.batchnavigator-1.2.10/README.txt 0000644 0001750 0000144 00000001355 11620231465 016062 0 ustar abel users A helper to navigate batched results in a web page.
..
This file is part of lazr.batchnavigator.
lazr.batchnavigator is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, version 3 of the License.
lazr.batchnavigator 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.batchnavigator. If not, see
.
lazr.batchnavigator-1.2.10/setup.py 0000755 0001750 0000144 00000004771 11620231465 016106 0 ustar abel users #!/usr/bin/env python
# Copyright 2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
import ez_setup
ez_setup.use_setuptools()
import sys
from setuptools import setup, find_packages
# generic helpers primarily for the long_description
def generate(*docname_or_string):
res = []
for value in docname_or_string:
if value.endswith('.txt'):
f = open(value)
value = f.read()
f.close()
res.append(value)
if not value.endswith('\n'):
res.append('')
return '\n'.join(res)
# end generic helpers
__version__ = open("src/lazr/batchnavigator/version.txt").read().strip()
setup(
name='lazr.batchnavigator',
version=__version__,
namespace_packages=['lazr'],
packages=find_packages('src'),
package_dir={'':'src'},
include_package_data=True,
zip_safe=False,
maintainer='LAZR Developers',
maintainer_email='lazr-users@lists.launchpad.net',
description=open('README.txt').readline().strip(),
long_description=generate(
'src/lazr/batchnavigator/README.txt',
'src/lazr/batchnavigator/NEWS.txt'),
license='LGPL v3',
install_requires=[
'fixtures',
'setuptools',
'testtools',
'zope.cachedescriptors',
'zope.interface',
'zope.publisher',
],
url='https://launchpad.net/lazr.batchnavigator',
download_url= 'https://launchpad.net/lazr.batchnavigator/+download',
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
"Operating System :: OS Independent",
"Programming Language :: Python"],
extras_require=dict(
docs=['Sphinx',
'z3c.recipe.sphinxdoc']
),
test_suite='lazr.yourpkg.tests',
)
lazr.batchnavigator-1.2.10/ez_setup.py 0000644 0001750 0000144 00000022466 11620231465 016602 0 ustar abel users #!python
"""Bootstrap setuptools installation
If you want to use setuptools in your package's setup.py, just include this
file in the same directory with it, and add this to the top of your setup.py::
from ez_setup import use_setuptools
use_setuptools()
If you want to require a specific version of setuptools, set a download
mirror, or use an alternate download directory, you can do so by supplying
the appropriate options to ``use_setuptools()``.
This file can also be run as a script to install or upgrade setuptools.
"""
import sys
DEFAULT_VERSION = "0.6c8"
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
md5_data = {
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
}
import sys, os
def _validate_md5(egg_name, data):
if egg_name in md5_data:
from md5 import md5
digest = md5(data).hexdigest()
if digest != md5_data[egg_name]:
print >>sys.stderr, (
"md5 validation of %s failed! (Possible download problem?)"
% egg_name
)
sys.exit(2)
return data
def use_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
download_delay=15, min_version=None
):
"""Automatically find/download setuptools and make it available on sys.path
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end with
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
it is not already available. If `download_delay` is specified, it should
be the number of seconds that will be paused before initiating a download,
should one be required. If an older version of setuptools is installed,
this routine will print a message to ``sys.stderr`` and raise SystemExit in
an attempt to abort the calling script.
"""
# Work around a hack in the ez_setup.py file from simplejson==1.7.3.
if min_version:
version = min_version
was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
def do_download():
egg = download_setuptools(version, download_base, to_dir, download_delay)
sys.path.insert(0, egg)
import setuptools; setuptools.bootstrap_install_from = egg
try:
import pkg_resources
except ImportError:
return do_download()
try:
pkg_resources.require("setuptools>="+version); return
except pkg_resources.VersionConflict, e:
if was_imported:
print >>sys.stderr, (
"The required version of setuptools (>=%s) is not available, and\n"
"can't be installed while this script is running. Please install\n"
" a more recent version first, using 'easy_install -U setuptools'."
"\n\n(Currently using %r)"
) % (version, e.args[0])
sys.exit(2)
else:
del pkg_resources, sys.modules['pkg_resources'] # reload ok
return do_download()
except pkg_resources.DistributionNotFound:
return do_download()
def download_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
delay = 15
):
"""Download setuptools from a specified location and return its filename
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end
with a '/'). `to_dir` is the directory where the egg will be downloaded.
`delay` is the number of seconds to pause before an actual download attempt.
"""
import urllib2, shutil
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
url = download_base + egg_name
saveto = os.path.join(to_dir, egg_name)
src = dst = None
if not os.path.exists(saveto): # Avoid repeated downloads
try:
from distutils import log
if delay:
log.warn("""
---------------------------------------------------------------------------
This script requires setuptools version %s to run (even to display
help). I will attempt to download it for you (from
%s), but
you may need to enable firewall access for this script first.
I will start the download in %d seconds.
(Note: if this machine does not have network access, please obtain the file
%s
and place it in this directory before rerunning this script.)
---------------------------------------------------------------------------""",
version, download_base, delay, url
); from time import sleep; sleep(delay)
log.warn("Downloading %s", url)
src = urllib2.urlopen(url)
# Read/write all in one block, so we don't create a corrupt file
# if the download is interrupted.
data = _validate_md5(egg_name, src.read())
dst = open(saveto,"wb"); dst.write(data)
finally:
if src: src.close()
if dst: dst.close()
return os.path.realpath(saveto)
def main(argv, version=DEFAULT_VERSION):
"""Install or upgrade setuptools and EasyInstall"""
try:
import setuptools
except ImportError:
egg = None
try:
egg = download_setuptools(version, delay=0)
sys.path.insert(0,egg)
from setuptools.command.easy_install import main
return main(list(argv)+[egg]) # we're done here
finally:
if egg and os.path.exists(egg):
os.unlink(egg)
else:
if setuptools.__version__ == '0.0.1':
print >>sys.stderr, (
"You have an obsolete version of setuptools installed. Please\n"
"remove it from your system entirely before rerunning this script."
)
sys.exit(2)
req = "setuptools>="+version
import pkg_resources
try:
pkg_resources.require(req)
except pkg_resources.VersionConflict:
try:
from setuptools.command.easy_install import main
except ImportError:
from easy_install import main
main(list(argv)+[download_setuptools(delay=0)])
sys.exit(0) # try to force an exit
else:
if argv:
from setuptools.command.easy_install import main
main(argv)
else:
print "Setuptools version",version,"or greater has been installed."
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
def update_md5(filenames):
"""Update our built-in md5 registry"""
import re
from md5 import md5
for name in filenames:
base = os.path.basename(name)
f = open(name,'rb')
md5_data[base] = md5(f.read()).hexdigest()
f.close()
data = [" %r: %r,\n" % it for it in md5_data.items()]
data.sort()
repl = "".join(data)
import inspect
srcfile = inspect.getsourcefile(sys.modules[__name__])
f = open(srcfile, 'rb'); src = f.read(); f.close()
match = re.search("\nmd5_data = {\n([^}]+)}", src)
if not match:
print >>sys.stderr, "Internal error!"
sys.exit(2)
src = src[:match.start(1)] + repl + src[match.end(1):]
f = open(srcfile,'w')
f.write(src)
f.close()
if __name__=='__main__':
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
update_md5(sys.argv[2:])
else:
main(sys.argv[1:])
lazr.batchnavigator-1.2.10/setup.cfg 0000644 0001750 0000144 00000000073 11634122075 016202 0 ustar abel users [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
lazr.batchnavigator-1.2.10/PKG-INFO 0000644 0001750 0000144 00000065354 11634122075 015473 0 ustar abel users Metadata-Version: 1.0
Name: lazr.batchnavigator
Version: 1.2.10
Summary: A helper to navigate batched results in a web page.
Home-page: https://launchpad.net/lazr.batchnavigator
Author: LAZR Developers
Author-email: lazr-users@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/lazr.batchnavigator/+download
Description: ..
This file is part of lazr.batchnavigator.
lazr.batchnavigator is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, version 3 of the License.
lazr.batchnavigator 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.batchnavigator. If not, see
.
Batch Navigation
****************
Batch navigation provides a way to navigate batch results in a web
page by providing URL links to the next, previous and numbered pages
of results.
It uses four query/POST arguments to control the batching:
- memo: A record of the underlying storage index pointer for the position of
the batch.
- direction: Indicates whether the memo is at the start or end of the batch.
- start: Cosmetic - used to calculate the apparent location (but note that
due to the concurrent nature of repeated visits to batches that the
true offset may differ - however the collection won't skip or show
items twice. For compatibility with saved URLs, if memo and
direction are both missing then start is used to do list slicing
into the collection.
- batch: Controls the amount of items we are showing per batch. It will only
appear if it's different from the default value set when the batch
is created.
These values can be overriden in the request, unless you also pass
force_start=True, which will make the start argument (again, defaulting to 0)
always chosen.
Imports:
>>> from lazr.batchnavigator import BatchNavigator, ListRangeFactory
>>> from zope.publisher.browser import TestRequest
>>> from zope.publisher.http import HTTPCharsets
>>> from zope.component import getSiteManager
>>> sm = getSiteManager()
>>> sm.registerAdapter(HTTPCharsets)
>>> def build_request(query_string_args=None, method='GET'):
... if query_string_args is None:
... query_string = ''
... else:
... if getattr(query_string_args, 'items', None) is not None:
... query_string_args = query_string_args.items()
... query_string = "&".join(
... ["%s=%s" % (k,v) for k,v in query_string_args])
... request = TestRequest(SERVER_URL='http://www.example.com/foo',
... method=method,
... environ={'QUERY_STRING': query_string})
... request.processInputs()
... return request
A dummy request object:
Some sample data.
>>> reindeer = ['Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet',
... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']
Because slicing large collections can be very expensive, BatchNavigator offers
a non-slice protocol for determining the edge of batches. The range_factory
supplies an object implementing IRangeFactory and manages this protocol.
ListRangeFactory is a simple included implementation which BatchNavigator will
use if no range_factory is supplied.
>>> _ = BatchNavigator(reindeer, build_request(),
... range_factory=ListRangeFactory(reindeer))
For the examples in the documentation we let BatchNavigator construct a
range_factory implicitly:
>>> safe_reindeer = reindeer
>>> safe_reindeer_batch_navigator = BatchNavigator(
... safe_reindeer, build_request(), size=3)
An important feature of lazr.batchnavigator is its reluctance to
invoke len() on an underlying data set. len() can be an expensive
operation that provides little benefit, so this library tries hard to
avoid calling len() unless it's absolutely necessary. To show this
off, we'll define a subclass of Python's list type that explodes when
len() is invoked on it.
>>> class ListWithExplosiveLen(list):
... """A list subclass that doesn't like its len() being called."""
... def __len__(self):
... raise RuntimeError
Unless otherwise stated, we will use this list exclusively throughout
this test, to verify that len() is never called unless we want it to
be.
>>> explosive_reindeer = ListWithExplosiveLen(reindeer)
>>> reindeer_batch_navigator = BatchNavigator(
... explosive_reindeer, build_request(), size=3)
The BatchNavigator implements IBatchNavigator. We need to use the
'safe' batch navigator here, because verifyObject probes all methods
of the object it's passed, including __len__.
>>> from zope.interface.verify import verifyObject
>>> from lazr.batchnavigator.interfaces import IBatchNavigator
>>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator)
True
The BatchNavigator class provides IBatchNavigatorFactory. This can be used
to register a batch navigator factory as a utility, for instance.
>>> from lazr.batchnavigator.interfaces import IBatchNavigatorFactory
>>> verifyObject(IBatchNavigatorFactory, BatchNavigator)
True
You can ask the navigator for the chunk of results currently being shown
(e.g. to iterate over them for rendering in ZPT):
>>> list(reindeer_batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
You can ask for the first, previous, next and last results' links:
>>> reindeer_batch_navigator.firstBatchURL()
''
>>> reindeer_batch_navigator.prevBatchURL()
''
>>> reindeer_batch_navigator.nextBatchURL()
'http://www.example.com/foo?memo=3&start=3'
There's no way to get the URL to the final batch without knowing the
length of the entire list, so we'll use the safe batch navigator to
demonstrate lastBatchURL():
>>> safe_reindeer_batch_navigator.lastBatchURL()
'http://www.example.com/foo?direction=backwards&start=6'
The next link will be empty when there are no further results:
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
>>> last_reindeer_batch_navigator.nextBatchURL()
''
The first and previous link should appear even when we start at a point between 0
and the batch size:
>>> request = build_request({"start": "2", "batch": "3"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
Here, we can see too that the batch argument appears as part of the URL.
That's because the request asked for a different size than the default
one when we create the Batch object, by default, it's 5.
>>> last_reindeer_batch_navigator.firstBatchURL()
'http://www.example.com/foo?batch=3'
>>> last_reindeer_batch_navigator.prevBatchURL()
'http://www.example.com/foo?batch=3&direction=backwards&memo=2'
This all works with other values in the query string, too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'})
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3)
>>> safe_reindeer_batch_navigator_with_qs = BatchNavigator(
... safe_reindeer, request, size=3)
In this case, we created the BatchNavigator with a default size of '3' and
the request is asking exactly that number of items per batch, and thus, we
don't need to show 'batch' as part of the URL.
>>> reindeer_batch_navigator_with_qs.firstBatchURL()
'http://www.example.com/foo?fnorb=bar'
>>> reindeer_batch_navigator_with_qs.prevBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&memo=3'
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=6&start=6'
(Again, there's no way to get the last batch without knowing the size
of the entire list.)
>>> safe_reindeer_batch_navigator_with_qs.lastBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&start=6'
The ``force_start`` argument allows you to ignore the start value in the
request. This can be useful when, for instance, a filter has changed, and the
desired behavior is to restart at 0.
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3, force_start=True)
>>> reindeer_batch_navigator_with_qs.currentBatch().start
0
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=3&start=3'
>>> reindeer[:3] == list(reindeer_batch_navigator_with_qs.currentBatch())
True
We ensure that batch arguments supplied in the URL are observed
for POST operations too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'}, method='POST')
>>> reindeer_batch_navigator_post_with_qs = BatchNavigator(
... reindeer, request)
>>> reindeer_batch_navigator_post_with_qs.start
3
>>> reindeer_batch_navigator_post_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&batch=3&memo=6&start=6'
We ensure that multiple size and batch arguments supplied in the URL don't
blow up the application. The first one is preferred.
>>> request = build_request(
... [('batch', '1'), ('batch', '7'), ('start', '2'), ('start', '10')])
>>> navigator = BatchNavigator(reindeer, request=request)
>>> navigator.nextBatchURL()
'http://www.example.com/foo?batch=1&memo=3&start=3'
The batch argument must be positive. Other numbers are ignored, and the
default batch size is used instead.
>>> from cgi import parse_qs
>>> request = build_request({'batch': '0'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
>>> request = build_request({'batch': '-1'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
=============
Empty Batches
=============
You can also create an empty batch that will not have any items:
>>> null_batch_navigator = BatchNavigator(
... None, build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
>>> null_batch_navigator = BatchNavigator(
... [], build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
TODO:
- blowing up when start is beyond end
- orphans
- overlap
====================================
Supporting Results Without a __len__
====================================
Some result objects do not implement __len__ because generally Python code
assumes that __len__ is cheap. SQLObject and Storm result sets both have this
behavior, for instance, so that it is cleat that getting the length is a non-
trivial operation.
To support these objects, the batch looks for __len__ on the result set. If
it does not exist, it adapts the result to
zope.interface.common.sequence.IFiniteSequence and uses that __len__.
>>> class ExampleResultSet(object):
... def __init__(self, results):
... self.stub_results = results
... def count(self):
... # imagine this actually returned
... return len(self.stub_results)
... def __getitem__(self, ix):
... return self.stub_results[ix] # also works with slices
... def __iter__(self):
... return iter(self.stub_results)
...
>>> from zope.interface import implements
>>> from zope.component import adapts, getSiteManager
>>> from zope.interface.common.sequence import IFiniteSequence
>>> class ExampleAdapter(ExampleResultSet):
... adapts(ExampleResultSet)
... implements(IFiniteSequence)
... def __len__(self):
... return self.stub_results.count()
...
>>> sm = getSiteManager()
>>> sm.registerAdapter(ExampleAdapter)
>>> example = ExampleResultSet(safe_reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> example_batch_navigator.currentBatch().total()
9
========================
Only Gets What Is Needed
========================
It's also important for performance of batching large result sets that the
batch only gets a slice of the results, rather than accessing the entirety.
>>> class ExampleResultSet(ExampleResultSet):
... def __init__(self, results):
... super(ExampleResultSet, self).__init__(results)
... self.getitem_history = []
... def __getitem__(self, ix):
... self.getitem_history.append(ix)
... return super(ExampleResultSet, self).__getitem__(ix)
...
>>> example = ExampleResultSet(reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> reindeer[:3] == list(example_batch_navigator.currentBatch())
True
>>> example.getitem_history
[slice(0, 4, None)]
Note that although the batch is of the size requested, the underlying
list contains one more item than is necessary. This is to make it easy
to determine whether a given batch is the final one in the list,
without having to explicitly look up the length of the list
(potentially an expensive operation).
=========================
Adding callback functions
=========================
Sometimes it is useful to have a function called with the batched
values once they have been determined. This is the case when there
are subsequent queries that are needed to be executed for each batch,
and it is undesirable or overly expensive to execute the query for
every value in the entire result set.
The callback function must define two parameters. The first is the
batch navigator object itself, and the second it the current batch.
The callback function is called once and only once when the
BatchNavigator is constructed, and the current batch is determined.
>>> def print_callback(context, batch):
... for item in batch:
... print item
>>> reindeer_batch_navigator = BatchNavigator(
... reindeer, build_request(), size=3, callback=print_callback)
Dasher
Dancer
Prancer
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(
... reindeer, request=request, callback=print_callback)
Vixen
Comet
Cupid
Donner
Blitzen
Rudolph
Most likely, the callback function will be bound to a view class.
By providing the batch navigator itself as the context for the
callback allows the addition of extra member variables. This is
useful as the BatchNavigator becomes the context in page templates
that are batched.
>>> class ReindeerView:
... def constructReindeerFromAtoms(self, context, batch):
... # some significantly slow process
... view.built_reindeer = list(batch)
... def batchedReindeer(self):
... return BatchNavigator(
... reindeer, build_request(), size=3,
... callback=self.constructReindeerFromAtoms)
>>> view = ReindeerView()
>>> batch_navigator = view.batchedReindeer()
>>> print view.built_reindeer
['Dasher', 'Dancer', 'Prancer']
>>> print list(batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
==================
Maximum batch size
==================
Since the batch size is exposed in the URL, it's possible for users to
tweak the batch parameter to retrieve more results. Since that may
potentially exhaust server resources, an upper limit is put on the batch
size. If the requested batch parameter is higher than this, an
InvalidBatchSizeError is raised.
>>> class DemoBatchNavigator(BatchNavigator):
... max_batch_size = 5
...
>>> request = build_request({"start": "0", "batch": "20"})
>>> DemoBatchNavigator(reindeer, request=request )
Traceback (most recent call last):
...
InvalidBatchSizeError: Maximum for "batch" parameter is 5.
==============
URL parameters
==============
Normally, any parameters passed in the current page's URL are
reproduced in the batch navigator's links. A "transient" parameter is
one that was only relevant for the current page request and shouldn't be
passed on to subsequent ones.
In this next batch navigator, two parameters occur in the page's URL:
"noisy" and "quiet."
>>> request_parameters = {
... 'quiet': 'ssht',
... 'noisy': 'HELLO',
... }
>>> request_with_parameters = build_request(request_parameters)
One parameter, "quiet," is transient. There is another transient
parameter called "absent," but it's not passed in our ongoing page
request.
>>> def build_navigator(list):
... return BatchNavigator(
... list, request_with_parameters, size=3,
... transient_parameters=['quiet', 'absent'])
>>> navigator_with_parameters = build_navigator(reindeer)
>>> safe_navigator_with_parameters = build_navigator(safe_reindeer)
Of these three parameters, only "noisy" recurs in the links produced by
the batch navigator.
>>> navigator_with_parameters.nextBatchURL()
'http://www.example.com/foo?noisy=HELLO&memo=3&start=3'
>>> safe_navigator_with_parameters.lastBatchURL()
'http://www.example.com/foo?noisy=HELLO&direction=backwards&start=6'
The transient parameter is omitted, and the one that was never passed in
in the first place does not magically appear.
==============
Batch headings
==============
The batched values are usually one kind of object such as bugs. The
BatchNavigator's heading property contains a description of the objects
for display.
>>> safe_reindeer_batch_navigator.heading
'results'
There is a special case for when there is only one item in the batch,
the singular version of the heading is returned.
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.heading
'result'
(Accessing .heading causes len() to be called on the underlying list,
which is why we have to use the safe batch navigator. In theory, this
could be optimized, but there's no real point, since the heading is
invariably preceded by the actual length of the underlying list,
eg. "10 results". Since len() is called anyway, and its value is
cached, a second len() won't hurt performance.)
The heading can be set by passing a singular and a plural version of
the heading. The batch navigation will return the appropriate
header based on the total items in the batch.
>>> navigator = BatchNavigator(safe_reindeer, request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bugs'
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bug'
(Cleanup)
>>> sm.unregisterAdapter(HTTPCharsets)
True
>>> sm.unregisterAdapter(ExampleAdapter)
True
===============
Other Documents
===============
.. toctree::
:glob:
*
docs/*
============================
NEWS for lazr.batchnavigator
============================
1.2.10 (2011-09-14)
- delegate the calculation of the rough length of a result set to
IRangeFactory.
1.2.9 (2011-08-25)
- When a backwards batch is at first too short and when another chunk
from the result set is added, _Batch,sliced_list() does no longer
use the memo value for the already retrived chunk.
- don't use the parameter start to determine if a previous/next batch
exists; don't rely on len(resultset) and to determine the real size
of a batch.
- Avoid negative start index on empty result sets.
1.2.7 (2011-07-18)
==================
- retrieve slices of the result set in class _Batch only via methods
of the range factory.
1.2.6 (2011-07-28)
==================
- fixed an error in handling backwards batches which return less elements
than expected.
- URL-encode all query parameters in BatchNavigator.generateBatchURL()
1.2.5 (2011-07-13)
==================
- Permit changing all variable names with a single prefix.
1.2.4 (2011-04-11)
==================
- Permit overriding determineSize to control how the batch default and concrete
sizes are determined in subclasses.
- Listify (once we have sliced) rather than assuming batched slices will honour
the complete list protocol.
1.2.3 (2011-04-06)
==================
- Add IRangeFactory and the ability to use backend database hints for efficient
retrieval of pages.
- Remove terrible-scaling getBatchURLs method.
1.2.2 (2010-08-19)
==================
- Make len() cheap to call when the current batch is the last (or
only) batch.
- Avoid calling len() when generating navigator URLs.
1.2.1 (2010-08-12)
==================
- fix a bug in the len() of a batch when the batch had previously been
iterated over
1.2.0 (2010-08-05)
==================
- avoid calling len() on the underlying sequence when possible
- return None for endNumber when the batch is out of range
1.1.1 (2010-05-10)
==================
- Ignore negative batch sizes
1.1 (2009-08-31)
================
- Remove build dependencies on bzr and egg_info
- remove sys.path hack in setup.py for __version__
1.0 (2009-03-24)
================
- Initial release on PyPI
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
lazr.batchnavigator-1.2.10/src/ 0000755 0001750 0000144 00000000000 11634122075 015150 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/ 0000755 0001750 0000144 00000000000 11634122075 016120 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/__init__.py 0000644 0001750 0000144 00000001641 11620231465 020232 0 ustar abel users # Copyright 2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator.
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
# this is a namespace package
try:
import pkg_resources
pkg_resources.declare_namespace(__name__)
except ImportError:
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/ 0000755 0001750 0000144 00000000000 11634122075 021114 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/ 0000755 0001750 0000144 00000000000 11634122075 023150 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/README.txt 0000644 0001750 0000144 00000000553 11620231465 024650 0 ustar abel users This code is a very old fork of batching code from the Zope project. The
z3c.batching package (http://pypi.python.org/pypi/z3c.batching) appears to
have evolved from the same, or very similar, source. Ideally, we will be able
to change lazr.batchnavigator to depend on z3c.batching, perhaps contributing
to that package any necessary changes for our usage needs.
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/__init__.py 0000644 0001750 0000144 00000000000 11620231465 025246 0 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/interfaces.py 0000644 0001750 0000144 00000005076 11631660274 025662 0 ustar abel users ##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Batching Support
$Id$
"""
from zope.interface import Attribute
from zope.interface.common.mapping import IItemMapping
class IBatch(IItemMapping):
"""A Batch represents a sub-list of the full enumeration.
The Batch constructor takes a list (or any list-like object) of elements,
a starting index and the size of the batch. From this information all
other values are calculated.
"""
def __len__():
"""Return the length of the batch. This might be different than the
passed in batch size, since we could be at the end of the list and
there are not enough elements left to fill the batch completely."""
def __iter__():
"""Creates an iterator for the contents of the batch (not the entire
list)."""
def __getitem__(index):
"""Return the element at the given offset."""
def __contains__(key):
"""Checks whether the key (in our case an index) exists."""
def nextBatch():
"""Return the next batch. If there is no next batch, return None."""
def prevBatch():
"""Return the previous batch. If there is no previous batch, return
None."""
def first():
"""Return the first element of the batch."""
def last():
"""Return the last element of the batch."""
def total():
"""Return the length of the list (not the batch)."""
def startNumber():
"""Give the start **number** of the batch, which is 1 more than the
start index passed in."""
def endNumber():
"""Give the end **number** of the batch, which is 1 more than the
final index."""
sliced_list = Attribute(
"A sliced list as returned by IRangeFactory.sliced_list.")
trueSize = Attribute("The actual size of this batch.")
has_previous_batch = Attribute(
"True, if this batch has a predecessor, else False.")
has_next_batch = Attribute(
"True, if this batch has a successor, else False.")
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/batch.py 0000644 0001750 0000144 00000034407 11631712667 024624 0 ustar abel users ##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
# Some parts Copyright 2005 Canonical Ltd.
#
##############################################################################
"""
Zope-derived Batching Support
*Do not use this. It is now an internal implementation detail.*
The intent is to get rid of this in favor of moving to extending z3c.batching.
"""
from zope.interface import implements
from zope.interface.common.sequence import IFiniteSequence
from interfaces import IBatch
from zope.cachedescriptors.property import Lazy
# The base batch size, which can be overridden by users of _Batch such
# as BatchNavigator. In Launchpad, we override it via a config option.
BATCH_SIZE = 50
class _PartialBatch:
"""A helper batch implementation.
_Batch.sliced_list() below needs to retrieve a second chunk of
data when a call of range_factory.getSlice() for a backwards batch
returns less elements than requested because the start of the result
set is reached.
In this case, another (forwards) batch must be retrieved. _Batch
must not assume that the memo value used to retrieve the first, too
small, result set can be used to retrieve the additional data. (The
class RangeFactoryWithValueBasedEndpointMemos in
tests/test_z3batching.py is an example where this assumption fails.)
Instead, _Batch.sliced_list() must retrieve the endpoint memos for
the partial data and use them to retrieve the missing part of the
result set. Since
- IRangeFactory.getEndpointMemos(batch) is free to use any
property of IBatch,
- a call like self.range_factory.getEndpointMemos(self) in
_Batch.sliced_list() leads to infinite recursions if the
range factory wants to access sliced_list,
we use this helper class for the getEndpointMemos() call in
_Batch.sliced_list().
"""
implements(IBatch)
def __init__(self, sliced_list):
self.start = 0
self.trueSize = len(sliced_list)
self.sliced_list = sliced_list
self.size = len(sliced_list)
def __len__(self):
"""See `IBatch`."""
return len(self.sliced_list)
def __iter__(self):
"""See `IBatch`."""
return iter(self.sliced_list)
def __getitem__(self, index):
"""See `IBatch`."""
return self.sliced_list[index]
def __contains__(self, key):
"""See `IBatch`."""
return 0 <= key < len(self.sliced_list)
def nextBatch(self):
"""See `IBatch`."""
raise NotImplementedError
def prevBatch(self):
"""See `IBatch`."""
raise NotImplementedError
def first(self):
"""See `IBatch`."""
return self.sliced_list[0]
def last(self):
"""See `IBatch`."""
return self.sliced_list[-1]
def total(self):
"""See `IBatch`."""
return len(self.sliced_list)
def startNumber(self):
"""See `IBatch`."""
return 1
def endNumber(self):
"""See `IBatch`."""
return len(self.sliced_list) + 1
# The values do not matter at all. Just make verifyObject() happy.
has_previous_batch = None
has_next_batch = None
class _Batch(object):
implements(IBatch)
def _get_length(self, results):
# If this batch's contents is smaller than the batch size, it is the
# last batch and we can deduce the underlying data's size.
if (self._sliced_list is not None
and len(self._sliced_list) <= self.size):
return self.start + len(self._sliced_list)
return self.range_factory.rough_length
@property
def listlength(self):
if self._listlength is None:
self._listlength = self._get_length(self.list)
return self._listlength
@Lazy
def trueSize(self):
"""Return the actual size of this batch."""
length = len(self.sliced_list)
if length >= self.size:
# This batch is full (and there might be another batch
# afterwards). Return .size
return self.size
else:
# This batch is not full. Return its (user-visible)
# length.
return length
def __init__(self, results, range_factory, start=0, size=None,
range_forwards=None, range_memo=None, _listlength=None):
"""Create a _Batch.
:param start: Cosmetic indicator of the start of the batch. Has *no*
effect on returned data.
:param range_factory: A factory used to construct efficient views of
the results.
:param range_forwards: True if the range memo is at or before the start
of the batch.
:param range_memo: An endpoint memo from the range factory describing
how to get a slice from the factory which will have offset 0
contain the first element of the batch (or the list, if not
range_forwards). '' for the extreme edge of the range, None to
have the start parameter take precedence and cause actual
slicing of results.
"""
if results is None:
results = []
self.list = results
if _listlength is not None:
self._listlength = _listlength
else:
self._listlength = None
self._sliced_list = None
if size is None:
size = BATCH_SIZE
self.size = size
self.start = start
self.end = start + size
self.range_factory = range_factory
self.range_memo = range_memo
if range_forwards is None:
range_forwards = True
self.range_forwards = range_forwards
self.is_first_batch = False
def __len__(self):
assert self.trueSize >= 0, ('The number of items in a batch should '
'never be negative.')
return self.trueSize
def __getitem__(self, key):
if key >= self.trueSize:
raise IndexError, 'batch index out of range'
# When self.start is negative (IOW, when we are batching over an
# empty list) we need to raise IndexError here; otherwise, the
# attempt to slice below will, on objects which don't implement
# __len__, raise a mysterious exception directly from python.
if self.start < 0:
raise IndexError, 'batch is empty'
# We delegate to self.sliced_list because:
# - usually folk will be walking the batch and sliced_list optimises
# that.
# - if they aren't, the overhead of duplicating one batch of results in
# memory is small
return self.sliced_list[key]
@Lazy
def sliced_list(self):
# We use Lazy here to avoid self.__iter__ giving us new objects every
# time; in certain cases (such as when database access is necessary)
# this can be expensive.
#
if self.range_memo is None:
# Legacyy mode - use the slice protocol on results.
sliced = self.range_factory.getSliceByIndex(self.start, self.end+1)
else:
# The range factory gives us a partition on results starting from
# the previous actual result.
if self.range_memo is '' and not self.range_forwards:
# As a special case, when getting the last batch (range_memo
# None, range_forwards False) we calculate the number of items
# we would expect from the size of the collection. This is done
# because that is what the older code did when range_factory
# was added.
size = self.listlength % self.size
if not size:
size = self.size
else:
# We get one more item than we need, so that we can detect
# cases where the underlying list ends on a batch boundary
# without having to check the total size of the list (an
# expensive operation).
size = self.size + 1
sliced = self.range_factory.getSlice(size, self.range_memo,
self.range_forwards)
if not self.range_forwards:
sliced.reverse()
if self.range_memo is not '' and len(sliced) <= size:
if len(sliced) == size:
# sliced is meant to have an extra element *after* the
# content for end-of-collection-detection, but the
# partitioner has just walked backwards, so shuffle things
# around to suit. If we didn't get as much data as we asked
# for we are at the beginning of the collection and all
# elements are needed.
sliced = sliced[1:] + sliced[:1]
else:
# If we got fewer than expected walking backwards, the
# range memo may have constrained us. So we need to get
# some more results:
needed = size - len(sliced)
partial = _PartialBatch(sliced)
extra_memo = (
self.range_factory.getEndpointMemos(partial))
extra = self.range_factory.getSlice(needed,
extra_memo[1], forwards=True)
sliced = sliced + extra
self.is_first_batch = True
# This is the first time we get an inkling of (approximately)
# how many items are in the list. This is the appropriate time
# to handle edge cases.
self._sliced_list = sliced
return sliced
def __iter__(self):
sliced = self.sliced_list
if len(sliced) > self.size:
# The slice contains more than a full batch, indicating
# that there is another batch beyond this one. But we
# don't actually want to return the item from the next
# batch.
sliced = sliced[:-1]
else:
# The slice contains a full batch or less, indicating that
# there is no batch beyond this one. In this case we want
# to return the full slice.
pass
return iter(sliced)
def first(self):
return self[0]
def last(self):
return self[self.trueSize-1]
def __contains__(self, item):
return item in self.__iter__()
@property
def has_next_batch(self):
"""See `IBatch`."""
# self.sliced_list tries to return one more object than the
# batch size. If it returns the batch size, or fewer, then
# this batch encompasses the end of the list, for forward
# batching.
# In the case of backward batching, an empty memo value
# indicates that this is the last batch.
if self.range_forwards:
return len(self.sliced_list) > self.size
else:
return self.range_memo != ''
def nextBatch(self):
if not self.has_next_batch:
return None
start = self.start + self.size
memos = self.range_factory.getEndpointMemos(self)
return _Batch(self.list, self.range_factory, start, self.size,
range_memo=memos[1], range_forwards=True,
_listlength=self._listlength)
@property
def has_previous_batch(self):
"""See `IBatch`."""
if self.range_memo is None:
# If no range memo is specified, sliced_list() falls back
# to slicing by index. This happens for old URLs, for example.
return self.start > 0
if self.range_forwards:
# For forward batching, we are at the first batch if the memo
# value is empty.
return self.range_memo != ''
else:
# For backwards batching, we can rely on the flag set in
# sliced_list. sliced_list is a @Lazy property, so make
# sure that it has been evaluated.
self.sliced_list
return not self.is_first_batch
def prevBatch(self):
if not self.has_previous_batch:
return None
# The only case in which we should /not/ offer a previous batch
# is when we are already at position zero, which also happens
# when the list is empty.
if self.start <= 0 and self.range_memo is None:
return None
start = self.start - self.size
if start < 0:
# This situation happens, for instance, when you have a
# 20-item batch and you manually set your start to 15;
# in this case, hopping back one batch would be starting at
# -5, which doesn't really make sense.
start = 0
memos = self.range_factory.getEndpointMemos(self)
return _Batch(self.list, self.range_factory, start, self.size,
range_memo=memos[0], range_forwards=False,
_listlength=self._listlength)
def firstBatch(self):
return _Batch(self.list, self.range_factory, 0, size=self.size,
range_memo='', range_forwards=True, _listlength=self._listlength)
def lastBatch(self):
# Return the last possible batch for this dataset, at the
# correct offset.
last_index = self.listlength - 1
last_batch_start = last_index - (last_index % self.size)
if last_batch_start < 0:
last_batch_start = 0
return _Batch(self.list, self.range_factory, last_batch_start,
size=self.size, range_memo='', range_forwards=False,
_listlength=self._listlength)
def total(self):
return self.listlength
def startNumber(self):
return self.start+1
def endNumber(self):
# If this batch is completely empty, it makes no sense to ask for an
# "endNumber" so return None.
if not len(self.sliced_list):
return None
return self.start + min(self.size, len(self.sliced_list))
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/LICENSE.txt 0000644 0001750 0000144 00000004202 11620231465 024770 0 ustar abel users Zope Public License (ZPL) Version 2.1
-------------------------------------
A copyright notice accompanies this license document that
identifies the copyright holders.
This license has been certified as open source. It has also
been designated as GPL compatible by the Free Software
Foundation (FSF).
Redistribution and use in source and binary forms, with or
without modification, are permitted provided that the
following conditions are met:
1. Redistributions in source code must retain the
accompanying copyright notice, this list of conditions,
and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying
copyright notice, this list of conditions, and the
following disclaimer in the documentation and/or other
materials provided with the distribution.
3. Names of the copyright holders must not be used to
endorse or promote products derived from this software
without prior written permission from the copyright
holders.
4. The right to distribute this software or to use it for
any purpose does not give you the right to use
Servicemarks (sm) or Trademarks (tm) of the copyright
holders. Use of them is covered by separate agreement
with the copyright holders.
5. If any files are modified, you must cause the modified
files to carry prominent notices stating that you changed
the files and the date of any change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS''
AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
NO EVENT SHALL THE COPYRIGHT HOLDERS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE. lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/COPYRIGHT.txt 0000644 0001750 0000144 00000000742 11620231465 025263 0 ustar abel users Copyright (c) 2004-2009 Zope Corporation and Contributors, and Canonical Ltd.
All Rights Reserved.
This software is subject to the provisions of the Zope Public License,
Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
FOR A PARTICULAR PURPOSE.
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/README.txt 0000644 0001750 0000144 00000046113 11620231465 022616 0 ustar abel users ..
This file is part of lazr.batchnavigator.
lazr.batchnavigator is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, version 3 of the License.
lazr.batchnavigator 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.batchnavigator. If not, see
.
Batch Navigation
****************
Batch navigation provides a way to navigate batch results in a web
page by providing URL links to the next, previous and numbered pages
of results.
It uses four query/POST arguments to control the batching:
- memo: A record of the underlying storage index pointer for the position of
the batch.
- direction: Indicates whether the memo is at the start or end of the batch.
- start: Cosmetic - used to calculate the apparent location (but note that
due to the concurrent nature of repeated visits to batches that the
true offset may differ - however the collection won't skip or show
items twice. For compatibility with saved URLs, if memo and
direction are both missing then start is used to do list slicing
into the collection.
- batch: Controls the amount of items we are showing per batch. It will only
appear if it's different from the default value set when the batch
is created.
These values can be overriden in the request, unless you also pass
force_start=True, which will make the start argument (again, defaulting to 0)
always chosen.
Imports:
>>> from lazr.batchnavigator import BatchNavigator, ListRangeFactory
>>> from zope.publisher.browser import TestRequest
>>> from zope.publisher.http import HTTPCharsets
>>> from zope.component import getSiteManager
>>> sm = getSiteManager()
>>> sm.registerAdapter(HTTPCharsets)
>>> def build_request(query_string_args=None, method='GET'):
... if query_string_args is None:
... query_string = ''
... else:
... if getattr(query_string_args, 'items', None) is not None:
... query_string_args = query_string_args.items()
... query_string = "&".join(
... ["%s=%s" % (k,v) for k,v in query_string_args])
... request = TestRequest(SERVER_URL='http://www.example.com/foo',
... method=method,
... environ={'QUERY_STRING': query_string})
... request.processInputs()
... return request
A dummy request object:
Some sample data.
>>> reindeer = ['Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet',
... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']
Because slicing large collections can be very expensive, BatchNavigator offers
a non-slice protocol for determining the edge of batches. The range_factory
supplies an object implementing IRangeFactory and manages this protocol.
ListRangeFactory is a simple included implementation which BatchNavigator will
use if no range_factory is supplied.
>>> _ = BatchNavigator(reindeer, build_request(),
... range_factory=ListRangeFactory(reindeer))
For the examples in the documentation we let BatchNavigator construct a
range_factory implicitly:
>>> safe_reindeer = reindeer
>>> safe_reindeer_batch_navigator = BatchNavigator(
... safe_reindeer, build_request(), size=3)
An important feature of lazr.batchnavigator is its reluctance to
invoke len() on an underlying data set. len() can be an expensive
operation that provides little benefit, so this library tries hard to
avoid calling len() unless it's absolutely necessary. To show this
off, we'll define a subclass of Python's list type that explodes when
len() is invoked on it.
>>> class ListWithExplosiveLen(list):
... """A list subclass that doesn't like its len() being called."""
... def __len__(self):
... raise RuntimeError
Unless otherwise stated, we will use this list exclusively throughout
this test, to verify that len() is never called unless we want it to
be.
>>> explosive_reindeer = ListWithExplosiveLen(reindeer)
>>> reindeer_batch_navigator = BatchNavigator(
... explosive_reindeer, build_request(), size=3)
The BatchNavigator implements IBatchNavigator. We need to use the
'safe' batch navigator here, because verifyObject probes all methods
of the object it's passed, including __len__.
>>> from zope.interface.verify import verifyObject
>>> from lazr.batchnavigator.interfaces import IBatchNavigator
>>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator)
True
The BatchNavigator class provides IBatchNavigatorFactory. This can be used
to register a batch navigator factory as a utility, for instance.
>>> from lazr.batchnavigator.interfaces import IBatchNavigatorFactory
>>> verifyObject(IBatchNavigatorFactory, BatchNavigator)
True
You can ask the navigator for the chunk of results currently being shown
(e.g. to iterate over them for rendering in ZPT):
>>> list(reindeer_batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
You can ask for the first, previous, next and last results' links:
>>> reindeer_batch_navigator.firstBatchURL()
''
>>> reindeer_batch_navigator.prevBatchURL()
''
>>> reindeer_batch_navigator.nextBatchURL()
'http://www.example.com/foo?memo=3&start=3'
There's no way to get the URL to the final batch without knowing the
length of the entire list, so we'll use the safe batch navigator to
demonstrate lastBatchURL():
>>> safe_reindeer_batch_navigator.lastBatchURL()
'http://www.example.com/foo?direction=backwards&start=6'
The next link will be empty when there are no further results:
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
>>> last_reindeer_batch_navigator.nextBatchURL()
''
The first and previous link should appear even when we start at a point between 0
and the batch size:
>>> request = build_request({"start": "2", "batch": "3"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
Here, we can see too that the batch argument appears as part of the URL.
That's because the request asked for a different size than the default
one when we create the Batch object, by default, it's 5.
>>> last_reindeer_batch_navigator.firstBatchURL()
'http://www.example.com/foo?batch=3'
>>> last_reindeer_batch_navigator.prevBatchURL()
'http://www.example.com/foo?batch=3&direction=backwards&memo=2'
This all works with other values in the query string, too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'})
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3)
>>> safe_reindeer_batch_navigator_with_qs = BatchNavigator(
... safe_reindeer, request, size=3)
In this case, we created the BatchNavigator with a default size of '3' and
the request is asking exactly that number of items per batch, and thus, we
don't need to show 'batch' as part of the URL.
>>> reindeer_batch_navigator_with_qs.firstBatchURL()
'http://www.example.com/foo?fnorb=bar'
>>> reindeer_batch_navigator_with_qs.prevBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&memo=3'
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=6&start=6'
(Again, there's no way to get the last batch without knowing the size
of the entire list.)
>>> safe_reindeer_batch_navigator_with_qs.lastBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&start=6'
The ``force_start`` argument allows you to ignore the start value in the
request. This can be useful when, for instance, a filter has changed, and the
desired behavior is to restart at 0.
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3, force_start=True)
>>> reindeer_batch_navigator_with_qs.currentBatch().start
0
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=3&start=3'
>>> reindeer[:3] == list(reindeer_batch_navigator_with_qs.currentBatch())
True
We ensure that batch arguments supplied in the URL are observed
for POST operations too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'}, method='POST')
>>> reindeer_batch_navigator_post_with_qs = BatchNavigator(
... reindeer, request)
>>> reindeer_batch_navigator_post_with_qs.start
3
>>> reindeer_batch_navigator_post_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&batch=3&memo=6&start=6'
We ensure that multiple size and batch arguments supplied in the URL don't
blow up the application. The first one is preferred.
>>> request = build_request(
... [('batch', '1'), ('batch', '7'), ('start', '2'), ('start', '10')])
>>> navigator = BatchNavigator(reindeer, request=request)
>>> navigator.nextBatchURL()
'http://www.example.com/foo?batch=1&memo=3&start=3'
The batch argument must be positive. Other numbers are ignored, and the
default batch size is used instead.
>>> from cgi import parse_qs
>>> request = build_request({'batch': '0'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
>>> request = build_request({'batch': '-1'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
=============
Empty Batches
=============
You can also create an empty batch that will not have any items:
>>> null_batch_navigator = BatchNavigator(
... None, build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
>>> null_batch_navigator = BatchNavigator(
... [], build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
TODO:
- blowing up when start is beyond end
- orphans
- overlap
====================================
Supporting Results Without a __len__
====================================
Some result objects do not implement __len__ because generally Python code
assumes that __len__ is cheap. SQLObject and Storm result sets both have this
behavior, for instance, so that it is cleat that getting the length is a non-
trivial operation.
To support these objects, the batch looks for __len__ on the result set. If
it does not exist, it adapts the result to
zope.interface.common.sequence.IFiniteSequence and uses that __len__.
>>> class ExampleResultSet(object):
... def __init__(self, results):
... self.stub_results = results
... def count(self):
... # imagine this actually returned
... return len(self.stub_results)
... def __getitem__(self, ix):
... return self.stub_results[ix] # also works with slices
... def __iter__(self):
... return iter(self.stub_results)
...
>>> from zope.interface import implements
>>> from zope.component import adapts, getSiteManager
>>> from zope.interface.common.sequence import IFiniteSequence
>>> class ExampleAdapter(ExampleResultSet):
... adapts(ExampleResultSet)
... implements(IFiniteSequence)
... def __len__(self):
... return self.stub_results.count()
...
>>> sm = getSiteManager()
>>> sm.registerAdapter(ExampleAdapter)
>>> example = ExampleResultSet(safe_reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> example_batch_navigator.currentBatch().total()
9
========================
Only Gets What Is Needed
========================
It's also important for performance of batching large result sets that the
batch only gets a slice of the results, rather than accessing the entirety.
>>> class ExampleResultSet(ExampleResultSet):
... def __init__(self, results):
... super(ExampleResultSet, self).__init__(results)
... self.getitem_history = []
... def __getitem__(self, ix):
... self.getitem_history.append(ix)
... return super(ExampleResultSet, self).__getitem__(ix)
...
>>> example = ExampleResultSet(reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> reindeer[:3] == list(example_batch_navigator.currentBatch())
True
>>> example.getitem_history
[slice(0, 4, None)]
Note that although the batch is of the size requested, the underlying
list contains one more item than is necessary. This is to make it easy
to determine whether a given batch is the final one in the list,
without having to explicitly look up the length of the list
(potentially an expensive operation).
=========================
Adding callback functions
=========================
Sometimes it is useful to have a function called with the batched
values once they have been determined. This is the case when there
are subsequent queries that are needed to be executed for each batch,
and it is undesirable or overly expensive to execute the query for
every value in the entire result set.
The callback function must define two parameters. The first is the
batch navigator object itself, and the second it the current batch.
The callback function is called once and only once when the
BatchNavigator is constructed, and the current batch is determined.
>>> def print_callback(context, batch):
... for item in batch:
... print item
>>> reindeer_batch_navigator = BatchNavigator(
... reindeer, build_request(), size=3, callback=print_callback)
Dasher
Dancer
Prancer
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(
... reindeer, request=request, callback=print_callback)
Vixen
Comet
Cupid
Donner
Blitzen
Rudolph
Most likely, the callback function will be bound to a view class.
By providing the batch navigator itself as the context for the
callback allows the addition of extra member variables. This is
useful as the BatchNavigator becomes the context in page templates
that are batched.
>>> class ReindeerView:
... def constructReindeerFromAtoms(self, context, batch):
... # some significantly slow process
... view.built_reindeer = list(batch)
... def batchedReindeer(self):
... return BatchNavigator(
... reindeer, build_request(), size=3,
... callback=self.constructReindeerFromAtoms)
>>> view = ReindeerView()
>>> batch_navigator = view.batchedReindeer()
>>> print view.built_reindeer
['Dasher', 'Dancer', 'Prancer']
>>> print list(batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
==================
Maximum batch size
==================
Since the batch size is exposed in the URL, it's possible for users to
tweak the batch parameter to retrieve more results. Since that may
potentially exhaust server resources, an upper limit is put on the batch
size. If the requested batch parameter is higher than this, an
InvalidBatchSizeError is raised.
>>> class DemoBatchNavigator(BatchNavigator):
... max_batch_size = 5
...
>>> request = build_request({"start": "0", "batch": "20"})
>>> DemoBatchNavigator(reindeer, request=request )
Traceback (most recent call last):
...
InvalidBatchSizeError: Maximum for "batch" parameter is 5.
==============
URL parameters
==============
Normally, any parameters passed in the current page's URL are
reproduced in the batch navigator's links. A "transient" parameter is
one that was only relevant for the current page request and shouldn't be
passed on to subsequent ones.
In this next batch navigator, two parameters occur in the page's URL:
"noisy" and "quiet."
>>> request_parameters = {
... 'quiet': 'ssht',
... 'noisy': 'HELLO',
... }
>>> request_with_parameters = build_request(request_parameters)
One parameter, "quiet," is transient. There is another transient
parameter called "absent," but it's not passed in our ongoing page
request.
>>> def build_navigator(list):
... return BatchNavigator(
... list, request_with_parameters, size=3,
... transient_parameters=['quiet', 'absent'])
>>> navigator_with_parameters = build_navigator(reindeer)
>>> safe_navigator_with_parameters = build_navigator(safe_reindeer)
Of these three parameters, only "noisy" recurs in the links produced by
the batch navigator.
>>> navigator_with_parameters.nextBatchURL()
'http://www.example.com/foo?noisy=HELLO&memo=3&start=3'
>>> safe_navigator_with_parameters.lastBatchURL()
'http://www.example.com/foo?noisy=HELLO&direction=backwards&start=6'
The transient parameter is omitted, and the one that was never passed in
in the first place does not magically appear.
==============
Batch headings
==============
The batched values are usually one kind of object such as bugs. The
BatchNavigator's heading property contains a description of the objects
for display.
>>> safe_reindeer_batch_navigator.heading
'results'
There is a special case for when there is only one item in the batch,
the singular version of the heading is returned.
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.heading
'result'
(Accessing .heading causes len() to be called on the underlying list,
which is why we have to use the safe batch navigator. In theory, this
could be optimized, but there's no real point, since the heading is
invariably preceded by the actual length of the underlying list,
eg. "10 results". Since len() is called anyway, and its value is
cached, a second len() won't hurt performance.)
The heading can be set by passing a singular and a plural version of
the heading. The batch navigation will return the appropriate
header based on the total items in the batch.
>>> navigator = BatchNavigator(safe_reindeer, request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bugs'
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bug'
(Cleanup)
>>> sm.unregisterAdapter(HTTPCharsets)
True
>>> sm.unregisterAdapter(ExampleAdapter)
True
===============
Other Documents
===============
.. toctree::
:glob:
*
docs/*
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/NEWS.txt 0000644 0001750 0000144 00000004275 11634121714 022440 0 ustar abel users ============================
NEWS for lazr.batchnavigator
============================
1.2.10 (2011-09-14)
- delegate the calculation of the rough length of a result set to
IRangeFactory.
1.2.9 (2011-08-25)
- When a backwards batch is at first too short and when another chunk
from the result set is added, _Batch,sliced_list() does no longer
use the memo value for the already retrived chunk.
- don't use the parameter start to determine if a previous/next batch
exists; don't rely on len(resultset) and to determine the real size
of a batch.
- Avoid negative start index on empty result sets.
1.2.7 (2011-07-18)
==================
- retrieve slices of the result set in class _Batch only via methods
of the range factory.
1.2.6 (2011-07-28)
==================
- fixed an error in handling backwards batches which return less elements
than expected.
- URL-encode all query parameters in BatchNavigator.generateBatchURL()
1.2.5 (2011-07-13)
==================
- Permit changing all variable names with a single prefix.
1.2.4 (2011-04-11)
==================
- Permit overriding determineSize to control how the batch default and concrete
sizes are determined in subclasses.
- Listify (once we have sliced) rather than assuming batched slices will honour
the complete list protocol.
1.2.3 (2011-04-06)
==================
- Add IRangeFactory and the ability to use backend database hints for efficient
retrieval of pages.
- Remove terrible-scaling getBatchURLs method.
1.2.2 (2010-08-19)
==================
- Make len() cheap to call when the current batch is the last (or
only) batch.
- Avoid calling len() when generating navigator URLs.
1.2.1 (2010-08-12)
==================
- fix a bug in the len() of a batch when the batch had previously been
iterated over
1.2.0 (2010-08-05)
==================
- avoid calling len() on the underlying sequence when possible
- return None for endNumber when the batch is out of range
1.1.1 (2010-05-10)
==================
- Ignore negative batch sizes
1.1 (2009-08-31)
================
- Remove build dependencies on bzr and egg_info
- remove sys.path hack in setup.py for __version__
1.0 (2009-03-24)
================
- Initial release on PyPI
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/_batchnavigator.py 0000644 0001750 0000144 00000033670 11631712667 024643 0 ustar abel users # Copyright 2004-2010 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
__metaclass__ = type
import urllib
import cgi
from zope.interface import implements, classProvides
from zope.interface.common.sequence import IFiniteSequence
from zope.cachedescriptors.property import Lazy
from lazr.batchnavigator.z3batching.batch import _Batch
from lazr.batchnavigator.interfaces import (
IBatchNavigator,
IBatchNavigatorFactory,
IRangeFactory,
InvalidBatchSizeError,
)
__all__ = [
'BatchNavigator',
'ListRangeFactory',
]
class BatchNavigator:
# subclasses can override
_batch_factory = _Batch
implements(IBatchNavigator)
classProvides(IBatchNavigatorFactory)
start_variable_name = 'start'
batch_variable_name = 'batch'
memo_variable_name = 'memo'
direction_variable_name = 'direction'
# Set to e.g. 'active' to make the variable names become 'active_start',
# 'active_batch' etc.
variable_name_prefix = ''
# The size the batch navigator was constructed with
default_size = None
# The size used if no specific size was supplied to the constructor.
default_batch_size = 50
max_batch_size = 300
# We want subclasses to be able to hide the 'Last' link from
# users. They may want to do this for really large result sets;
# for example, batches with over a hundred thousand items.
show_last_link = True
# The default heading describing the kind of objects in the batch.
# Sub-classes can override this to be more specific.
default_singular_heading = 'result'
default_plural_heading = 'results'
transient_parameters = None
@Lazy
def query_string_parameters(self):
query_string = self.request.get('QUERY_STRING', '')
# Just in case QUERY_STRING is in the environment explicitly as
# None (Some tests seem to do this, but not sure if it can ever
# happen outside of tests.)
if query_string is None:
query_string = ''
return cgi.parse_qs(query_string, keep_blank_values=True)
def __init__(self, results, request, start=0, size=None, callback=None,
transient_parameters=None, force_start=False,
range_factory=None):
"See `IBatchNavigatorFactory.__call__`"
self.request = request
self._update_variable_names()
local = (self.batch_variable_name, self.start_variable_name,
self.memo_variable_name, self.direction_variable_name)
self.transient_parameters = set(local)
if transient_parameters is not None:
self.transient_parameters.update(transient_parameters)
# For backwards compatibility (as in the past a work-around has been
# to include the url batch params in hidden fields within posted
# forms), if the request is a POST request, and either the 'start'
# or 'batch' params are included then revert to the default behaviour
# of using the request (which automatically gets the params from the
# request.form dict).
if request.method == 'POST' and (
self.start_variable_name in request.form or
self.batch_variable_name in request.form):
batch_params_source = request
else:
# We grab the request variables directly from the requests
# query_string_parameters so that they will be recognized
# even during post operations.
batch_params_source = dict(
(k, v[0]) for k, v
in self.query_string_parameters.items() if k in local)
# In this code we ignore invalid request variables since it
# probably means the user finger-fumbled it in the request. We
# could raise UnexpectedFormData, but is there a good reason?
def param_var(name):
return batch_params_source.get(name, None)
# -- start
request_start = param_var(self.start_variable_name)
if force_start or request_start is None:
self.start = start
else:
try:
self.start = int(request_start)
except (ValueError, TypeError):
self.start = start
# -- size
size = self.determineSize(size, batch_params_source)
# -- direction
direction = param_var(self.direction_variable_name)
if direction == 'backwards':
direction = False
else:
direction = None
# -- memo
memo = param_var(self.memo_variable_name)
if direction is not None and memo is None:
# Walking backwards from the end - the only case where we generate
# a url with no memo but a direction (and the only case where we
# need it: from the start with no memo is equivalent to a simple
# list slice anyway).
memo = ''
if range_factory is None:
range_factory = ListRangeFactory(results)
self.batch = self._batch_factory(results, range_factory,
start=self.start, size=size, range_forwards=direction,
range_memo=memo)
if callback is not None:
callback(self, self.batch)
self.setHeadings(
self.default_singular_heading, self.default_plural_heading)
def determineSize(self, size, batch_params_source):
"""Determine the default and user requested batch sizes.
This function should assign the default size for the batch to
self.default_size. The base class implementation uses the size passed
to the constructor, but other implementations may choose to clamp it or
force a particular default size.
:param size: Size passed to the constructor.
:param batch_params_source: User parameters dict.
:return: The size to be used for this batch.
"""
self.default_size = size
request_size = self._getRequestedSize(batch_params_source)
if request_size is not None:
size = request_size
if size is None:
size = self.default_batch_size
return size
def _getRequestedSize(self, batch_params_source):
"""Figure out what batch size the user requested, if any.
Sizes that are not positive numbers are ignored.
:return: An acceptable batch size requested by the user, or
None.
:raise: `InvalidBatchSizeError` if the requested size exceeds
`max_batch_size`.
"""
size_string = batch_params_source.get(self.batch_variable_name, None)
if size_string is None:
return None
try:
request_size = int(size_string)
except (ValueError, TypeError):
return None
if request_size <= 0:
return None
if request_size > self.max_batch_size:
raise InvalidBatchSizeError(
'Maximum for "%s" parameter is %d.' %
(self.batch_variable_name,
self.max_batch_size))
return request_size
@property
def heading(self):
"""See `IBatchNavigator`"""
if self.batch.total() == 1:
return self._singular_heading
return self._plural_heading
def setHeadings(self, singular, plural):
"""See `IBatchNavigator`"""
self._singular_heading = singular
self._plural_heading = plural
def getCleanQueryParams(self, params=None):
"""Removes batch nav params if present and returns a sequence
of key-values pairs.
If ``params`` is None, uses the current query_string_params.
"""
if params is None:
params = []
for k, v in self.query_string_parameters.items():
params.extend((k, item) for item in v)
else:
try:
params = params.items()
except AttributeError:
pass
# We need the doseq=True because some url params are for multi-value
# fields.
return [
(key, value) for (key, value) in sorted(params)
if key not in self.transient_parameters]
def getCleanQueryString(self, params=None):
"""Removes batch nav params if present and returns a query
string.
If ``params`` is None, uses the current query_string_params.
"""
# We need the doseq=True because some url params are for multi-value
# fields.
return urllib.urlencode(self.getCleanQueryParams(params), doseq=True)
def generateBatchURL(self, batch, backwards=False):
url = ""
if batch is None:
return url
params = self.getCleanQueryParams()
size = batch.size
if size != self.default_size:
# The current batch size should only be part of the URL if it's
# different from the default batch size.
params.append((self.batch_variable_name, size))
if backwards:
params.append((self.direction_variable_name, "backwards"))
if batch.range_memo:
params.append((self.memo_variable_name, batch.range_memo))
start = batch.startNumber() - 1
if start:
params.append((self.start_variable_name, start))
base_url = str(self.request.URL)
return "%s?%s" % (base_url, urllib.urlencode(params))
def firstBatchURL(self):
batch = self.batch.firstBatch()
if not self.batch.has_previous_batch:
# We are already on the first batch.
batch = None
return self.generateBatchURL(batch)
def prevBatchURL(self):
return self.generateBatchURL(self.batch.prevBatch(), backwards=True)
def nextBatchURL(self):
return self.generateBatchURL(self.batch.nextBatch())
def lastBatchURL(self):
batch = self.batch.lastBatch()
if not self.batch.has_next_batch:
# We are already on the last batch.
batch = None
return self.generateBatchURL(batch, backwards=True)
def currentBatch(self):
return self.batch
def _update_variable_names(self):
"""Update self.x_variable_name with self.variable_name_prefix.
This gives the concrete instance the same prefix for all variables.
"""
prefix = self.variable_name_prefix or ''
if prefix:
prefix += '_'
for varname in ('start', 'batch', 'memo', 'direction'):
attrname = varname + '_variable_name'
setattr(self, attrname, prefix + getattr(self, attrname))
class ListRangeFactory:
"""Implements an IRangeFactory for lists (and list-like objects).
This uses the slice protocol index as its memos: 'up to and not including
low', 'from this point and up'.
"""
implements(IRangeFactory)
def __init__(self, results):
self.results = results
def getEndpointMemos(self, batch):
"""See `IRangeFactory`
Most implementations will want to use batch.sliced_list to retrieve
database keys.
"""
end_idx = batch.trueSize + batch.start
return (str(batch.start), str(end_idx))
def getSlice(self, size, endpoint_memo=None, forwards=True):
"""See `IRangeFactory`"""
if not size:
return []
if not forwards:
size = -size
if endpoint_memo:
try:
offset = int(endpoint_memo)
except ValueError:
raise InvalidBatchSizeError('not an int')
offset_plus_size = offset + size
if offset_plus_size < 1:
offset_plus_size = 0
else:
offset = None
offset_plus_size = size
if forwards:
return list(self.results[offset:offset_plus_size])
else:
if offset is None:
# SQL mapped result sets will blow up on [-N:None] slices, so
# get the total size and create absolute references.
# This is only used for the 'Last' link in a collection, and
# any nontrivial collection shouldn't be using ListRangeFactory
# unless indexing is cheap (which implies counting is cheap).
# If this becomes an issue, a shared length cache can be built
# with _Batch, but because other implementations of
# IRangeFactory would need to honour that protocol, it should
# be avoided unless needed.
if getattr(self.results, '__len__', None) is None:
self.results = IFiniteSequence(self.results)
total_length = len(self.results)
offset = total_length
# Storm raises an exception for a negative initial offset.
offset_plus_size = max(offset + offset_plus_size, 0)
result = list(self.results[offset_plus_size:offset])
result.reverse()
return result
def getSliceByIndex(self, start, end):
"""See `IRangeFactory`"""
if self.results is not None:
return list(self.results[start:end])
else:
return []
@Lazy
def rough_length(self):
"""See `IRangeFactory`"""
results = self.results
# LBYL: we don't want to mask exceptions that len might raise, and we
# don't have chained exceptions yet.
if getattr(results, '__len__', None) is None:
results = IFiniteSequence(results)
return len(results)
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/__init__.py 0000644 0001750 0000144 00000002224 11620231465 023224 0 ustar abel users # Copyright 2004-2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
"""Functions for working with generic syntax URIs."""
import pkg_resources
__version__ = pkg_resources.resource_string(
"lazr.batchnavigator", "version.txt").strip()
# While we generally frown on "*" imports, this, combined with the fact we
# only test code from this module, means that we can verify what has been
# exported.
from lazr.batchnavigator._batchnavigator import *
from lazr.batchnavigator._batchnavigator import __all__
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/interfaces.py 0000644 0001750 0000144 00000014010 11631712667 023616 0 ustar abel users # Copyright 2004-2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
from zope.interface import Interface, Attribute
__metaclass__ = type
class InvalidBatchSizeError(AssertionError):
"""Received a batch parameter that was invalid (e.g. exceeds our configured max size)."""
class IRangeFactory(Interface):
"""A helper used to construct better backend queries for batches.
IRangeFactory instances are paired with a result object that the
BatchNavigator was constructed with. Generally they will be curried
as part of determining what backend query to run for the thing being
batched.
"""
def getEndpointMemos(batch):
"""Converts a batch to a pair of (lower, upper) endpoint memos.
:return: A tuple of bytestrings suitable for inclusion in URLs.
"""
def getSlice(size, endpoint_memo='', forwards=True):
"""Returns a slice of the results starting after endpoint_memo.
Note that endpoint_memo should be a simple string and is untrusted.
:param size: The maximum number of entries to return in the slice.
:param endpoint_memo: An endpoint memo as returned by getEndpointMemos.
If None or '', the edge of the results is used.
:param forwards: If True, slice forwards from endpoint_memo. Otherwise
slice backwards from endpoint_memo (e.g. ascending indices in the
result will indicate earlier items in the results collection).
:return: An object honouring the tuple protocol, just like 'results' in
the IBatchNavigatorFactory constructor call. If forwards was not
True, the order will be reversed vs the 'results' object, otherwise
it is identical. The object should be fully materialized from any
backing store and have no more than size elements in it.
"""
def getSliceByIndex(start, end):
"""Return a slice of the results.
:param start: The index of the first element contained in the
result.
:param end: The index of the first element not contained in the
result.
"""
rough_length = Attribute(
"""The total length of the result set.
The value does not have to be accurate; it should only be
used for informational purposes.
""")
class IBatchNavigator(Interface):
"""A batch navigator for a specified set of results."""
batch = Attribute("The IBatch for which navigation links are provided.")
heading = Attribute(
"The heading describing the kind of objects in the batch.")
def setHeadings(singular, plural):
"""Set the heading for singular and plural results."""
def prevBatchURL():
"""Return a URL to the previous chunk of results."""
def nextBatchURL():
"""Return a URL to the next chunk of results."""
class IBatchNavigatorFactory(Interface):
def __call__(results, request, start=0, size=None, callback=None,
transient_parameters=None, force_start=False,
range_factory=None):
"""Constructs an `IBatchNavigator` instance.
:param results: is an iterable of results.
:param request: Expected to confirm to
zope.publisher.interfaces.IRequest.
The following variables are pulled out of the request to control
the batch navigator:
- memo: A record of the underlying storage index pointer for the
position of the batch.
- direction: Indicates whether the memo is at the start or end of
the batch.
- start: Cosmetic - used to calculate the apparent location (but
note that due to the concurrent nature of repeated visits to
batches that the true offset may differ - however the
collection won't skip or show items twice. For compatibility
with saved URLs, if memo and direction are both missing then
start is used to do list slicing into the collection.
- batch: Controls the amount of items we are showing per batch.
It will only appear if it's different from the default value
set when the batch is created.
:param size: is the default batch size, to fall back to if the
request does not specify one. If no size can be determined
from arguments or request, the launchpad.default_batch_size
config option is used.
:param callback: is called, if defined, at the end of object
construction with the defined batch as determined by the
start and request parameters.
:param transient_parameters: optional sequence of parameter
names that should not be passed on in links generated by
the batch navigator. Use this for parameters that had
meaning when this page was requested, but should not be kept
around for other page requests in the batch.
:param force_start: if True, the given `start` argument has
precedence over the start value in the request, if any.
:param range_factory: An IRangeFactory paired with results, or None.
If None, a default IRangeFactory is constructed which simply adapts
results.
:raises InvalidBatchSizeError: if the requested batch size is higher
than the maximum allowed.
"""
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/ 0000755 0001750 0000144 00000000000 11634122075 022256 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/test_z3batching.py 0000644 0001750 0000144 00000075606 11631712667 025752 0 ustar abel users ##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Batch tests.
"""
import operator
import unittest
from zope.interface.verify import verifyObject, verifyClass
from lazr.batchnavigator.z3batching.batch import (
_Batch,
BATCH_SIZE,
_PartialBatch,
)
from lazr.batchnavigator.z3batching.interfaces import IBatch
from lazr.batchnavigator import ListRangeFactory
class ListWithExplosiveLen(list):
"""A list subclass that doesn't like its len() being called."""
def __len__(self):
raise RuntimeError
class ListAllergicToNegativeIndexes(list):
"""A list subclass that doesn't like negative indexes."""
def __getitem__(self, index):
if index < 0:
raise RuntimeError('Negative indexes are not allowed.')
class ListWithIncorrectLength(list):
"""A list subclass returning a too small length value.
The goal of configurable range factories is to avoid SQL queries
involving very expensive counting of result rows.
One way to avoid a SELECT COUNT(*) FROM ... is to use the number
of results reported by EXPLAIN SELECT ... FROM ...
These results are not necessarily precise, hence class _Batch
must not rely on the length reported by a sequence. This class
can be used in tests to ensure that result sets with bad length
information are processed properly.
"""
def __init__(self, length, data=[]):
super(ListWithIncorrectLength, self).__init__(data)
self._length = length
def __len__(self):
return self._length
def __getslice__(self, start, end):
return super(ListWithIncorrectLength, self).__getslice__(start, end)
class RangeFactoryWithValueBasedEndpointMemos:
"""A RangeFactory which uses data values from a batch as endpoint memos.
"""
def __init__(self, results):
self.results = results
def getEndpointMemos(self, batch):
"""See `IRangeFactory`."""
return batch[0], batch[-1]
getEndpointMemosFromSlice = getEndpointMemos
def getSlice(self, size, endpoint_memo='', forwards=True):
"""See `IRangeFactory`."""
if size == 0:
return []
if endpoint_memo == '':
if forwards:
return self.results[:size]
else:
sliced = self.results[-size:]
sliced.reverse()
return sliced
if forwards:
index = 0
while (index < len(self.results) and
endpoint_memo >= self.results[index]):
index += 1
return self.results[index:index+size]
else:
index = len(self.results) - 1
while (index >= 0 and endpoint_memo < self.results[index]):
index -= 1
if index < 0:
return []
start_index = max(0, index - size)
sliced = self.results[start_index:index]
sliced.reverse()
return sliced
def getSliceByIndex(self, start, end):
"""See `IRangeFactory`."""
return self.results[start:end]
@property
def rough_length(self):
"""See `IRangeFactory`."""
return len(self.results)
class RangeFactoryRecordingResultLengthCalls:
"""A RangeFactory surrogate which records each access to rough_length."""
def __init__(self):
self.rough_length_accesses = 0
@property
def rough_length(self):
self.rough_length_accesses += 1
return 42
class TestingInfrastructureTest(unittest.TestCase):
def test_ListWithExplosiveLen(self):
# For some of the tests we want to be sure len() of the underlying
# collection is never called, so we've created a subclass of list that
# raises an exception if asked for its length.
self.assertRaises(RuntimeError, len, ListWithExplosiveLen([1,2,3]))
def test_ListAllergicToNegativeIndexes(self):
# Some of the tests want to show that the underlying collection is
# never accessed with a negative index, so we have a subclass of list
# that raises an exception if accessed in that way.
self.assertRaises(
RuntimeError,
operator.getitem, ListAllergicToNegativeIndexes([1,2,3]), -1)
def test_ListWithIncorrectLength(self):
# Calling len(ListWithIncorrectLength) returns a bogus value.
self.assertEqual(0, len(ListWithIncorrectLength(0, [1,])))
self.assertEqual(2, len(ListWithIncorrectLength(2, [1,])))
# Slicing works for positive indexes, even if len() returns a too
# small value.
weird_list = ListWithIncorrectLength(2, [0, 1, 2, 3])
self.assertEqual([2, 3], weird_list[2:4])
# But it returns odd data with negative indexes. (This does not
# matter much because even ListRangeFactory does not use
# negative indexes.)
self.assertEqual([0], weird_list[-3:-1])
# Slicing with positive indexes works for a too large len() value.
weird_list = ListWithIncorrectLength(6, [0, 1, 2, 3])
self.assertEqual([2, 3], weird_list[2:4])
# Slicing with negative indexes returns wrong results,
self.assertEqual([], weird_list[-2:-1])
self.assertEqual([3], weird_list[-3:-2])
def test_RangeFactoryWithValueBased_getEndpointMemos(self):
data = [str(value) for value in range(10)]
self.assertEqual(
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], data)
factory = RangeFactoryWithValueBasedEndpointMemos(data)
# The endpoint memo values are the values of the first and last
# elelemnt of a batch.
batch = data[:3]
self.assertEqual(('0', '2'), factory.getEndpointMemos(batch))
batch = data[4:8]
self.assertEqual(('4', '7'), factory.getEndpointMemos(batch))
# getSlice() called with an empty memo value returns the
# first elements if forwards is True...
self.assertEqual(
['0', '1', '2'],
factory.getSlice(size=3, endpoint_memo='', forwards=True))
# ...and the last elements if forwards is False.
self.assertEqual(
['9', '8', '7'],
factory.getSlice(size=3, endpoint_memo='', forwards=False))
# A forwards slice starts with a value larger than the
# given memo value.
self.assertEqual(
['6', '7'],
factory.getSlice(size=2, endpoint_memo='5', forwards=True))
# A backwards slice starts with a value smaller than the
# given memo value.
self.assertEqual(
['4', '3'],
factory.getSlice(size=2, endpoint_memo='5', forwards=False))
# A slice is smaller than requested if the end of the results
# is reached.
self.assertEqual(
['8', '9'],
factory.getSlice(size=3, endpoint_memo='7', forwards=True))
self.assertEqual(
[], factory.getSlice(size=3, endpoint_memo='A', forwards=True))
self.assertEqual(
[], factory.getSlice(size=3, endpoint_memo=' ', forwards=False))
class RecordingFactory(ListRangeFactory):
def __init__(self, results):
ListRangeFactory.__init__(self, results)
self.calls = []
def getEndpointMemos(self, batch):
self.calls.append('getEndpointMemos')
return ListRangeFactory.getEndpointMemos(self, batch)
def getSlice(self, size, memo, forwards):
self.calls.append(('getSlice', size, memo, forwards))
return ListRangeFactory.getSlice(self, size, memo, forwards)
class BatchTest(unittest.TestCase):
def getData(self):
return ['one', 'two', 'three', 'four', 'five', 'six',
'seven', 'eight', 'nine', 'ten']
def test_Interface(self):
self.failUnless(IBatch.providedBy(self.getBatch()))
def getBatch(self, start=0, size=None, range_forwards=None,
range_memo=None):
return _Batch(self.getData(), ListRangeFactory(self.getData()), start,
size, range_forwards, range_memo)
def getRecordingBatch(self, start=0, size=None, memo=None, forwards=True):
range_factory = RecordingFactory(self.getData())
batch = _Batch(range_factory.results, range_factory, start=start,
size=size, range_forwards=forwards, range_memo=memo)
return batch
def test_constructor(self):
self.getBatch(9, 3)
# A start that's larger than the length should still construct a
# working (but empty) batch.
self.getBatch(99, 3)
# If no size is provided a default is used.
_Batch(self.getData(), ListRangeFactory(self.getData()))
def test_default_batch_size(self):
# If we don't specify a batch size, a default one is chosen for us.
batch = _Batch(self.getData(), ListRangeFactory(self.getData()))
self.assertEqual(batch.size, BATCH_SIZE)
def test__len__(self):
batch = self.getBatch(0, 3)
self.assertEqual(len(batch), 3)
batch = self.getBatch(9, 3)
self.assertEqual(len(batch), 1)
batch = self.getBatch(99, 3)
self.assertEqual(len(batch), 0)
# If the entire set of contents fits into a single batch, the result
# of len() is correct.
batch = self.getBatch(size=999)
self.assertEqual(len(batch), 10)
# If the entire set of contents fits into a single batch, and we've
# iterated over the items, the result of len() is correct.
batch = self.getBatch(size=999)
list(batch)
self.assertEqual(len(batch), 10)
def test__getitem__(self):
batch = self.getBatch(0, 3)
self.assertEqual(batch[0], 'one')
self.assertEqual(batch[1], 'two')
self.assertEqual(batch[2], 'three')
batch = self.getBatch(3, 3)
self.assertEqual(batch[0], 'four')
self.assertEqual(batch[1], 'five')
self.assertEqual(batch[2], 'six')
batch = self.getBatch(9, 3)
self.assertRaises(IndexError, batch.__getitem__, 3)
# If the batch is "off the end" and we've materialized the underlying
# list (by iterating over the batch) we will get an IndexError if we
# pass in a negative item index.
batch = self.getBatch(99, 3)
list(iter(batch))
self.assertRaises(IndexError, batch.__getitem__, -1)
def test__iter__(self):
batch = self.getBatch(0, 3)
self.assertEqual(list(iter(batch)), ['one', 'two', 'three'])
batch = self.getBatch(9, 3)
self.assertEqual(list(iter(batch)), ['ten'])
batch = self.getBatch(99, 3)
self.assertEqual(list(iter(batch)), [])
def test__getitem__does_not_use_negative_indices(self):
# Some collections don't implement __len__, so slicing them doesn't
# work, therefore we want to be sure negative slices are not passed
# through to the underlying collection.
data = ListAllergicToNegativeIndexes(self.getData())
batch = _Batch(data, ListRangeFactory(data), 99, 3)
list(iter(batch))
self.assertRaises(IndexError, batch.__getitem__, -1)
def test__contains__(self):
batch = self.getBatch(0, 3)
self.assert_(batch.__contains__('one'))
self.assert_(batch.__contains__('two'))
self.assert_(batch.__contains__('three'))
self.assert_(not batch.__contains__('four'))
batch = self.getBatch(6, 3)
self.assert_(not batch.__contains__('one'))
self.assert_(batch.__contains__('seven'))
self.assert_(not batch.__contains__('ten'))
def test_last_batch_len_optimization(self):
# If the current batch is known to be the last batch, then we can
# calculate the total number of items without calling len() on the
# underlying collection.
data = ListWithExplosiveLen(self.getData())
batch = _Batch(data, ListRangeFactory(data), 0, 20)
# If we get the total number of items before accessing the underlying
# data, we'll call len() on the collection object.
self.assertRaises(RuntimeError, lambda: batch.listlength)
# If instead we do something that materialized the slice of the
# underlying collection that this batch represents...
next(iter(batch))
# ... then no exception is raised and we get the number of items.
self.assertEqual(batch.listlength, 10)
def test_firstBatch(self):
"""Check that the link to the first batch works.
This first batch will be always pointing to the first available batch
and, its main difference with the 'prev' and 'next' batches is, that
will not be None ever.
"""
# first batch when we are at the beginning of the batch.
first = self.getBatch(0, 3).firstBatch()
self.assertEqual(list(iter(first)), ['one', 'two', 'three'])
# first batch when we are in the second set of items of the batch.
first = self.getBatch(3, 3).firstBatch()
self.assertEqual(list(iter(first)), ['one', 'two', 'three'])
# first batch when we are in the third set of items of the batch.
first = self.getBatch(6, 3).firstBatch()
self.assertEqual(list(iter(first)), ['one', 'two', 'three'])
# first batch when we are at the end of the batch.
first = self.getBatch(9, 3).firstBatch()
self.assertEqual(list(iter(first)), ['one', 'two', 'three'])
# first batch when we get a request for an out of range item.
first = self.getBatch(99, 3).firstBatch()
self.assertEqual(list(iter(first)), ['one', 'two', 'three'])
def test_nextBatch(self):
next = self.getBatch(0, 3).nextBatch()
self.assertEqual(list(iter(next)), ['four', 'five', 'six'])
nextnext = next.nextBatch()
self.assertEqual(list(iter(nextnext)), ['seven', 'eight', 'nine'])
next = self.getBatch(9, 3).nextBatch()
self.assertEqual(next, None)
next = self.getBatch(99, 3).nextBatch()
self.assertEqual(next, None)
def test_nextBatch__backwards_batching(self):
# The "first backwards" batch has no next batch.
batch = self.getBatch(9, 3, range_forwards=False, range_memo='')
self.assertTrue(batch.nextBatch() is None)
# previous batches of this batch have a batch.
batch = batch.prevBatch()
self.assertFalse(batch.nextBatch() is None)
def test_prevBatch(self):
prev = self.getBatch(9, 3).prevBatch()
self.assertEqual(list(iter(prev)), ['seven', 'eight', 'nine'])
prevprev = prev.prevBatch()
self.assertEqual(list(iter(prevprev)), ['four', 'five', 'six'])
prev = self.getBatch(0, 3).prevBatch()
self.assertEqual(prev, None)
prev = self.getBatch(2, 3).prevBatch()
self.assertEqual(list(iter(prev)), ['one', 'two', 'three'])
# If we create a batch that's out of range, and don't get its
# length or any of its data, its previous batch will also be
# out of range.
out_of_range = self.getBatch(99, 3)
out_of_range_2 = out_of_range.prevBatch()
self.assertEqual(out_of_range_2.start, 96)
def test_lastBatch(self):
"""Check that the link to the last batch works.
This last batch will be always pointing to the last available batch
and, its main difference with the 'prev' and 'next' batches is, that
will not be None ever.
"""
# last batch when we are at the beginning of the batch.
last = self.getBatch(0, 3).lastBatch()
self.assertEqual(list(iter(last)), ['ten'])
# last batch when we are in the second set of items of the batch.
last = self.getBatch(3, 3).lastBatch()
self.assertEqual(list(iter(last)), ['ten'])
# last batch when we are in the third set of items of the batch.
last = self.getBatch(6, 3).lastBatch()
self.assertEqual(list(iter(last)), ['ten'])
# last batch when we are at the end of the batch.
last = self.getBatch(9, 3).lastBatch()
self.assertEqual(list(iter(last)), ['ten'])
# last batch when we get a request for an out of range item.
last = self.getBatch(99, 3).lastBatch()
self.assertEqual(list(iter(last)), ['ten'])
# We are going to test now the same, but when we get a request of 5
# items per batch because we had a bug in the way we calculate the
# last batch set that was only happening when we were using a batch
# size that is multiple of the item list length.
# last batch when we are at the beginning of the batch.
last = self.getBatch(0, 5).lastBatch()
self.assertEqual(
list(iter(last)), ['six', 'seven', 'eight', 'nine', 'ten'])
# last batch when we are in the second set of items of the batch.
last = self.getBatch(5, 5).lastBatch()
self.assertEqual(
list(iter(last)), ['six', 'seven', 'eight', 'nine', 'ten'])
# last batch when we get a request for an out of range item.
last = self.getBatch(99, 5).lastBatch()
self.assertEqual(
list(iter(last)), ['six', 'seven', 'eight', 'nine', 'ten'])
def test_batchRoundTrip(self):
batch = self.getBatch(0, 3).nextBatch()
self.assertEqual(list(iter(batch.nextBatch().prevBatch())),
list(iter(batch)))
def test_first_last(self):
batch = self.getBatch(0, 3)
self.assertEqual(batch.first(), 'one')
self.assertEqual(batch.last(), 'three')
batch = self.getBatch(9, 3)
self.assertEqual(batch.first(), 'ten')
self.assertEqual(batch.last(), 'ten')
batch = self.getBatch(99, 3)
self.assertRaises(IndexError, batch.first)
self.assertRaises(IndexError, batch.last)
def test_total(self):
batch = self.getBatch(0, 3)
self.assertEqual(batch.total(), 10)
batch = self.getBatch(6, 3)
self.assertEqual(batch.total(), 10)
batch = self.getBatch(99, 3)
self.assertEqual(batch.total(), 10)
def test_trueSize_of_full_batch(self):
# The .trueSize property reports how many items are in the batch.
batch = self.getBatch(0, 3)
self.assertEqual(batch.trueSize, 3)
def test_trueSize_of_last_batch(self):
# If the current batch contains fewer than a full batch, .trueSize
# reports how many items are in the batch.
batch = self.getBatch(9, 3)
self.assertEqual(batch.trueSize, 1)
def test_optimized_trueSize_of_full_batch(self):
# If the unerlying items in the batch have been materialized, there is
# a code path that avoids calling len() on the underlying collection.
data = ListWithExplosiveLen(self.getData())
batch = _Batch(data, ListRangeFactory(data), 0, 3)
next(iter(batch)) # Materialize the items.
self.assertEqual(batch.trueSize, 3)
def test_optimized_trueSize_of_last_batch(self):
# If the current batch contains fewer than a full batch, .trueSize
# reports how many items are in the batch.
data = ListWithExplosiveLen(self.getData())
batch = _Batch(data, ListRangeFactory(data), 9, 3)
next(iter(batch)) # Materialize the items.
self.assertEqual(batch.trueSize, 1)
def test_startNumber(self):
batch = self.getBatch(0, 3)
self.assertEqual(batch.startNumber(), 1)
batch = self.getBatch(9, 3)
self.assertEqual(batch.startNumber(), 10)
batch = self.getBatch(99, 3)
self.assertEqual(batch.startNumber(), 100)
def test_endNumber(self):
# If a batch of size 3 starts at index 0, the human-friendly numbering
# of the last item should be 3.
batch = self.getBatch(0, 3)
self.assertEqual(batch.endNumber(), 3)
# If a batch of size 3 starts at index 9 and has just 1 item in it
# (because there are 10 items total), the human-friendly numbering
# of the last item should be 10.
batch = self.getBatch(9, 3)
self.assertEqual(batch.endNumber(), 10)
# If the batch start exceeds the number number of items, then the
# endNumber will be None to indicate that it makes no sense to ask.
batch = self.getBatch(99, 3)
self.assertEqual(batch.endNumber(), None)
def test_sliced_list_uses_range_factory(self):
batch = self.getRecordingBatch(memo='', forwards=False)
list(batch)
self.assertEqual([('getSlice', 10, '', False)], batch.range_factory.calls)
def test_firstBatch_generates_memo_None(self):
batch = self.getRecordingBatch(start=3, size=3, memo='3')
first = batch.firstBatch()
self.assertEqual('', first.range_memo)
self.assertEqual(True, first.range_forwards)
self.assertEqual(0, first.start)
self.assertEqual(3, first.size)
self.assertEqual(['one', 'two', 'three'], list(first))
def test_lastBatch_generates_memo_None_backwards(self):
batch = self.getRecordingBatch(start=3, size=3, memo='3')
last = batch.lastBatch()
self.assertEqual('', last.range_memo)
self.assertEqual(False, last.range_forwards)
# len() is known, and size() is known, so the last batch starts at
# len-size
self.assertEqual(9, last.start)
self.assertEqual(3, last.size)
self.assertEqual(['ten'], list(last))
def test_nextBatch_generates_upper_memo(self):
batch = self.getRecordingBatch(start=3, size=3, memo='3')
next = batch.nextBatch()
self.assertEqual('6', next.range_memo)
self.assertEqual(True, next.range_forwards)
self.assertEqual(6, next.start)
self.assertEqual(3, next.size)
self.assertEqual(['seven', 'eight', 'nine'], list(next))
def test_prevBatch_generates_lower_memo_backwards(self):
batch = self.getRecordingBatch(start=6, size=3, memo='3')
prev = batch.prevBatch()
self.assertEqual('6', prev.range_memo)
self.assertEqual(False, prev.range_forwards)
# len() is known, and size() is known, so the prev batch starts at
# len-size
self.assertEqual(3, prev.start)
self.assertEqual(3, prev.size)
self.assertEqual(['four', 'five', 'six'], list(prev))
def test_backwards_memo_inside_first_batch(self):
# If data is retrieved backwards from the memo point and if
# there is less data available than requested, additional
# data stored "after the endpoint" is returned.
batch = self.getRecordingBatch(start=0, size=5, memo=3, forwards=False)
self.assertEqual(['one', 'two', 'three', 'four', 'five'], list(batch))
def test_trueSize__with_bad_list_length(self):
# Even if the len(resultset) claims to have only 2 elements,
# batches starting at offsets 1 and 2 have two elements.
weird_list = ListWithIncorrectLength(2, [0, 1, 2, 3])
batch = _Batch(
weird_list, range_factory=ListRangeFactory(weird_list),
size=2, start=1)
self.assertEqual(2, batch.trueSize)
self.assertEqual([1, 2, 3], batch.sliced_list)
batch = _Batch(
weird_list, range_factory=ListRangeFactory(weird_list),
size=2, start=2)
self.assertEqual(2, batch.trueSize)
self.assertEqual([2, 3], batch.sliced_list)
def test_nextBatch__too_small_results_length(self):
# Batch.nextBatch() is not None even if len(results)
# indicates that there should not be a next batch.
weird_list = ListWithIncorrectLength(3, [0, 1, 2, 3, 4])
batch = _Batch(
weird_list, range_factory=ListRangeFactory(weird_list),
size=3)
self.assertTrue(batch.nextBatch() is not None)
def test_nextBatch__bad_results_length(self):
# Even if len(results) claims that there are only 6 results,
# batch.nextBatch() is not None
weird_list = ListWithIncorrectLength(6, [0, 1, 2, 3, 4, 5, 6])
batch = _Batch(
weird_list, range_factory=ListRangeFactory(weird_list),
size=3, range_memo='3', start=3, range_forwards=True)
self.assertEqual([3, 4, 5, 6], batch.sliced_list)
self.assertEqual([6], batch.nextBatch().sliced_list)
def test_prevBatch__outdated_start_params(self):
# The start parameter of a backwards batch may reach zero
# even when the real begin of a result set is not yet reached.
data = [1, 2, 3, 4, 5, 6]
range_factory = RangeFactoryWithValueBasedEndpointMemos(data)
# We create the first backwards.
batch = _Batch(
data, range_factory=range_factory, size=3, range_memo='',
start=3, range_forwards=False)
self.assertEqual([4, 5, 6], batch.sliced_list)
self.assertEqual(3, batch.start)
# The previous batch is now the first batch.
batch = batch.prevBatch()
self.assertEqual(0, batch.start)
self.assertEqual(4, batch.range_memo)
self.assertEqual([1, 2, 3, 4], batch.sliced_list)
self.assertTrue(batch.prevBatch() is None)
# If we now insert another value at the start of the result set,
# and when we reload this batch with the same range parameters,
# we get the same data slice, but also a prevBtach()
# that is not None.
data = [0, 1, 2, 3, 4, 5, 6]
range_factory = RangeFactoryWithValueBasedEndpointMemos(data)
new_batch = _Batch(
data, range_factory=range_factory, size=3,
range_memo=batch.range_memo, start=batch.start,
range_forwards=False)
# the contents of the batch is still correct. (Note that the
# value of the last does not matter, see _Batch.sliced_list().)
self.assertEqual([1, 2, 3, 0], new_batch.sliced_list)
# And we get a previous batch.
self.assertEqual([0, 1, 2, 3], new_batch.prevBatch().sliced_list)
# Note that both new_batch and its previous batch claim to start
# at 0.
self.assertEqual(0, new_batch.start)
self.assertEqual(0, new_batch.prevBatch().start)
def test_last_backwards_batch_with_value_based_range_factory(self):
# Another slice is added in _Batch.sliced_list() when the
# regular slice of a backwards batch does not return the
# number of required elements. This works for range factories
# which are based on the values too.
data = [str(value) for value in range(10)]
range_factory = RangeFactoryWithValueBasedEndpointMemos(data)
batch = _Batch(
data, range_factory=range_factory, size=3, range_memo='1',
start=1, range_forwards=False)
self.assertEqual(['0', '1', '2', '3'], batch.sliced_list)
def test_PartialBatch(self):
# PartialBatch implements the full IBatch interface.
from zope.interface.common.mapping import IItemMapping
self.assertTrue(verifyClass(IBatch, _PartialBatch))
partial = _PartialBatch(sliced_list=range(3))
self.assertTrue(verifyObject(IBatch, partial))
# trueSize is the length of sliced_list
self.assertEqual(3, partial.trueSize)
# sliced_list is passed by the contrucotr parameter sliced_list
self.assertEqual([0, 1, 2], partial.sliced_list)
# __len__() returns the length of the sliced list
self.assertEqual(3, len(partial))
# __iter__() iterates over sliced_list
self.assertEqual([0, 1, 2], [element for element in partial])
# __contains__() works.
self.assertTrue(1 in partial)
self.assertFalse(3 in partial)
# prevBatch(), nextBatch() exost but are not implemented.
self.assertRaises(NotImplementedError, partial.prevBatch)
self.assertRaises(NotImplementedError, partial.nextBatch)
# first and last are implemented.
self.assertEqual(0, partial.first())
self.assertEqual(2, partial.last())
# total() return the length of sliced_list
self.assertEqual(3, partial.total())
# startNumber, endNumber() are implemented
self.assertEqual(1, partial.startNumber())
self.assertEqual(4, partial.endNumber())
def test_has_next_batch__forwards_with_memo_not_at_end(self):
# If a batch is not the last batch for a result set,
# has_next_batch is True. When the parameter range_memo
# is given, the value of start does not matter.
batch = self.getBatch(
start=7, size=3, range_forwards=True, range_memo="6")
self.assertTrue(batch.has_next_batch)
def test_has_next_batch__forwards_with_memo_at_end(self):
# If a batch is the last batch for a result set,
# has_next_batch is False. When the parameter range_memo
# is given, the value of start does not matter.
batch = self.getBatch(
start=6, size=3, range_forwards=True, range_memo="7")
self.assertFalse(batch.has_next_batch)
def test_has_next_batch__forwards_without_memo_not_at_end(self):
# If a batch is the last batch for a result set,
# has_next_batch is False. When the parameter range_memo
# is not given, the value of start is used.
batch = self.getBatch(start=6, size=3)
self.assertTrue(batch.has_next_batch)
def test_has_next_batch__forwards_without_memo_at_end(self):
# If a batch is the last batch for a result set,
# has_next_batch is False. When the parameter range_memo
# is not given, the value of start is used.
batch = self.getBatch(start=7, size=3)
self.assertFalse(batch.has_next_batch)
def test_has_previous_batch__backwards_with_memo_not_at_end(self):
# If a batch is not the first batch for a result set,
# has_previous_batch is True. The value of start does not
# matter.
batch = self.getBatch(
start=0, size=3, range_forwards=False, range_memo="4")
self.assertTrue(batch.has_previous_batch)
def test_has_previous_batch__backwards_with_memo_at_end(self):
# If a batch is not the first batch for a result set,
# has_previous_batch is False.The value of start does not
# matter.
batch = self.getBatch(
start=1, size=3, range_forwards=False, range_memo="3")
self.assertFalse(batch.has_previous_batch)
def test_batch_gets_listlength_from_factory(self):
# Batches get the length of the result set from the
# range factory.
range_factory = RangeFactoryRecordingResultLengthCalls()
batch = _Batch(range(3), range_factory)
listlength = batch.listlength
self.assertEqual(42, listlength)
self.assertEqual(1, range_factory.rough_length_accesses)
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/__init__.py 0000644 0001750 0000144 00000002326 11620231465 024371 0 ustar abel users # Copyright 2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
"The lazr.batchnavigator tests."
import unittest
from lazr.batchnavigator.tests import (
test_docs,
)
def test_suite():
# Discovery would be nice, but needs python2.7 for use with subunit &
# testrepository.
result = unittest.TestSuite()
result.addTests(test_docs.additional_tests())
modules = ['batchnavigator', 'z3batching']
result.addTests(unittest.TestLoader().loadTestsFromNames([
'lazr.batchnavigator.tests.test_' + name for name in modules]))
return result
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/test_docs.py 0000644 0001750 0000144 00000003435 11620231465 024623 0 ustar abel users # Copyright 2009 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
"Test harness for doctests."
__metaclass__ = type
__all__ = [
'additional_tests',
]
import atexit
import doctest
import os
import pkg_resources
import unittest
DOCTEST_FLAGS = (
doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_NDIFF)
def additional_tests():
"Run the doc tests (README.txt and docs/*, if any exist)"
doctest_files = [
os.path.abspath(
pkg_resources.resource_filename('lazr.batchnavigator', 'README.txt'))]
if pkg_resources.resource_exists('lazr.batchnavigator', 'docs'):
for name in pkg_resources.resource_listdir('lazr.batchnavigator', 'docs'):
if name.endswith('.txt'):
doctest_files.append(
os.path.abspath(
pkg_resources.resource_filename(
'lazr.batchnavigator', 'docs/%s' % name)))
kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS)
atexit.register(pkg_resources.cleanup_resources)
return unittest.TestSuite((
doctest.DocFileSuite(*doctest_files, **kwargs)))
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/test_batchnavigator.py 0000644 0001750 0000144 00000034514 11631712667 026703 0 ustar abel users # Copyright 2011 Canonical Ltd. All rights reserved.
#
# This file is part of lazr.batchnavigator
#
# lazr.batchnavigator is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# lazr.batchnavigator 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 Lesser General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with lazr.batchnavigator. If not, see .
"""Unit tests for lazr.batchnavigator.BatchNavigator."""
import testtools
from testtools.matchers import Equals
from zope.interface.verify import verifyObject
from zope.publisher.browser import TestRequest
from lazr.batchnavigator import BatchNavigator, ListRangeFactory
from lazr.batchnavigator.interfaces import (
IRangeFactory,
InvalidBatchSizeError,
)
from lazr.batchnavigator.z3batching.batch import _Batch
from lazr.batchnavigator.tests.test_z3batching import RecordingFactory
SERVER_URL = 'http://www.example.com/foo'
def query_string(start=None, batch=None, memo=None, direction=None):
query_string_args = {}
params = locals()
def maybe(arg):
if params[arg] is not None:
query_string_args[arg] = params[arg]
maybe('batch')
maybe('direction')
maybe('start')
maybe('memo')
query_string = "&".join(["%s=%s" % item for item
in sorted(query_string_args.items())])
return query_string
def query_url(start=None, batch=None, memo=None, direction=None):
return SERVER_URL + '?' + query_string(start=start, batch=batch,
memo=memo,direction=direction)
def batchnav_request(start=None, batch=None, memo=None, direction=None):
return TestRequest(SERVER_URL=SERVER_URL,
method='GET', environ={'QUERY_STRING': query_string(start=start,
batch=batch, memo=memo,direction=direction)})
def sample_batchnav(start=None, batch=None, memo=None, direction=None):
collection = range(1, 11)
request = batchnav_request(start=start, batch=batch, memo=memo,
direction=direction)
range_factory = RecordingFactory(collection)
return BatchNavigator(collection, request, range_factory=range_factory)
class PickyListLikeCollection:
"""Collection that really hates slices with negative start values.
Some database-backed collections, e.g. Postgres via Storm, throw
exceptions if a slice is used that starts with a negative value. When
batch navigator is going backwards it is easy to pass such a slice. This
collection is used to ensure it is noticed if that happens.
"""
def __init__(self, collection):
self._collection = collection
def __iter__(self):
return iter(self._collection)
def __getitem__(self, index):
if (isinstance(index, slice) and
index.start is not None and index.start < 0):
raise RuntimeError
return self._collection.__getitem__(index)
def __len__(self):
return self._collection.__len__()
def empty_batchnav(start=None, batch=None, memo=None, direction=None):
collection = PickyListLikeCollection([])
request = batchnav_request(start=start, batch=batch, memo=memo,
direction=direction)
range_factory = RecordingFactory(collection)
return BatchNavigator(collection, request, range_factory=range_factory)
class EqualsQuery(Equals):
def __init__(self, start=None, batch=None, direction=None, memo=None):
Equals.__init__(self, query_url(start=start, batch=batch,
direction=direction, memo=memo))
class TestBatchFactory(testtools.TestCase):
def test_accepts_range_factory(self):
# A BatchNavigator can be instantiated with a RangeFactory.
results = [1, 2, 3, 4]
range_factory = ListRangeFactory(results)
request = batchnav_request()
batchnav = BatchNavigator(
results, request, range_factory=range_factory)
self.assertEqual(batchnav.batch.range_factory, range_factory)
def test_without_memo_direction_gets_non_factory_batch(self):
# memo and direction were added after the core was developed; for
# compatability with bookmarks when they are missing the old behaviour
# is invoked even when an explicit range factory is supplied
batchnav = sample_batchnav(start=3, batch=3)
self.assertEqual(range(4,7), list(batchnav.currentBatch()))
self.assertEqual([], batchnav.batch.range_factory.calls)
def test_without_memo_with_backwards_direction_gets_reverse_batch(self):
# When direction is specified backwards without memo the batch seeks
# backwards rather than doing a big list protocol slice.
batchnav = sample_batchnav(start=3, batch=3, direction='backwards')
self.assertEqual(range(10,11), list(batchnav.currentBatch()))
self.assertEqual([('getSlice', 1, '', False)],
batchnav.batch.range_factory.calls)
def test_memo_used_for_middle_batch_forwards(self):
# If we have a memo for a batch, it is used.
batchnav = sample_batchnav(start=3, batch=3, memo='3')
self.assertEqual(range(4,7), list(batchnav.currentBatch()))
self.assertEqual([('getSlice', 4, '3', True)],
batchnav.batch.range_factory.calls)
def test_memo_used_for_middle_batch_backwards(self):
# If we have a memo for a batch, it is used.
batchnav = sample_batchnav(start=3, batch=3, memo='6',
direction='backwards')
self.assertEqual(range(4,7), list(batchnav.currentBatch()))
self.assertEqual([('getSlice', 4, '6', False)],
batchnav.batch.range_factory.calls)
def test_lastBatchURL_sets_direction(self):
batchnav = sample_batchnav(batch=3)
self.assertThat(batchnav.lastBatchURL(),
EqualsQuery(start=9, batch=3, direction='backwards'))
def test_lastBatchURL_not_empty_for_bogus_start_value(self):
# lastBatchURL() is correct even when the start parameter
# is bogus: memo says that the batch begins at offset
# 6, while start points to the last element of the
# of the batch.
batchnav = sample_batchnav(start=9, batch=3, memo="6")
self.assertThat(batchnav.lastBatchURL(),
EqualsQuery(start=9, batch=3, direction='backwards'))
def test_firstBatchURL_is_trivial(self):
batchnav = sample_batchnav(start=3, batch=3, memo='3')
self.assertThat(batchnav.firstBatchURL(), EqualsQuery(batch=3))
def test_firstBatchURL_does_not_depend_on_start_parameter(self):
# nextBatchURL() is correct even when start has the (incorrect)
# value 0. Start = 0 implies that we are at the first batch.
# But this value may be wrong (see _Batch.__init__()), and
# firstBatchURL() does not rely on it.
batchnav = sample_batchnav(start=0, batch=3, memo='3')
self.assertThat(batchnav.firstBatchURL(), EqualsQuery(batch=3))
def test_nextBatchURL_has_memo(self):
batchnav = sample_batchnav(start=3, batch=3, memo='3')
self.assertThat(batchnav.nextBatchURL(),
EqualsQuery(start=6, batch=3, memo='6'))
def test_prevBatchURL_has_memo_direction(self):
batchnav = sample_batchnav(start=6, batch=3, memo='6')
self.assertThat(batchnav.prevBatchURL(),
EqualsQuery(start=3, batch=3, memo='6', direction='backwards'))
def test_variable_prefix_affects_all_params(self):
# subclassing and setting variable_name_prefix causes all the variable
# names to be adjusted.
collection = range(1, 11)
request = TestRequest(SERVER_URL=SERVER_URL,
method='GET', environ={
'QUERY_STRING': "prefix_start=6&prefix_batch=3&prefix_memo=6"})
range_factory = RecordingFactory(collection)
class PrefixBatchNavigator(BatchNavigator):
variable_name_prefix = 'prefix'
batchnav = PrefixBatchNavigator(collection, request,
range_factory=range_factory)
self.assertThat(batchnav.prevBatchURL(),
Equals(SERVER_URL + '?prefix_batch=3&prefix_direction=backwards'
'&prefix_memo=6&prefix_start=3'))
def test_range_factory_producing_url_unsafe_memos(self):
# Memo values containing characters with special meaning in URLs
# are properly escaped by generateBatchURL().
class WeirdRangeFactory(ListRangeFactory):
def getEndpointMemos(self, batch):
return ('start&/1', 'end&/2')
collection = range(1, 11)
request = TestRequest(
SERVER_URL=SERVER_URL, method='GET',
environ={'QUERY_STRING': "start=6&batch=3&memo=6"})
range_factory = WeirdRangeFactory(collection)
batchnav = BatchNavigator(
collection, request, range_factory=range_factory)
self.assertThat(
batchnav.nextBatchURL(),
Equals(SERVER_URL + '?batch=3&memo=end%26%2F2&start=9'))
def test_empty_collection(self):
batchnav = empty_batchnav(
start=2, batch=2)
self.assertEqual([], list(batchnav.currentBatch()))
def test_empty_collection_backwards(self):
batchnav = empty_batchnav(
start=2, batch=2, direction='backwards')
self.assertEqual([], list(batchnav.currentBatch()))
class TestListRangeFactory(testtools.TestCase):
def test_valid_object(self):
verifyObject(IRangeFactory, ListRangeFactory([]))
def test_getEndpointMemos_empty(self):
batch = _Batch([], ListRangeFactory([]))
self.assertEqual(('0', '0'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_one_item(self):
results = [0, 1, 2]
batch = _Batch(results, ListRangeFactory(results), start=1, size=1)
self.assertEqual(('1', '2'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_two_items(self):
results = [0, 1, 2, 3]
batch = _Batch(results, ListRangeFactory(results), start=1, size=2)
self.assertEqual(('1', '3'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_three_items(self):
results = [0, 1, 2, 3, 4]
batch = _Batch(results, ListRangeFactory(results), start=1, size=3)
self.assertEqual(('1', '4'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_start_border(self):
results = [0, 1]
batch = _Batch(results, ListRangeFactory(results), size=1)
self.assertEqual(('0', '1'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_end_border(self):
results = [0, 1]
batch = _Batch(results, ListRangeFactory(results), start=1, size=1)
self.assertEqual(('1', '2'),
batch.range_factory.getEndpointMemos(batch))
def test_getEndpointMemos_last_batch_end_border(self):
results = [0, 1, 2, 3]
batch = _Batch(results, ListRangeFactory(results), start=4, size=1)
self.assertEqual(('4', '4'),
batch.range_factory.getEndpointMemos(batch))
def test_getslice_bad_memo(self):
self.assertRaises(InvalidBatchSizeError,
ListRangeFactory([]).getSlice, 3, 'foo')
def test_getslice_next_end(self):
# at the end, crickets...
results = [0, 1, 2, 3, 4, 5]
_slice = ListRangeFactory(results).getSlice(3, '6')
self.assertEqual([], _slice)
def test_getslice_before_start(self):
# at the end, crickets...
results = [0, 1, 2, 3, 4, 5]
_slice = list(ListRangeFactory(results).getSlice(3, '0', forwards=False))
self.assertEqual([], _slice)
def test_getslice_before_end(self):
results = [0, 1, 2, 3, 4, 5]
_slice = list(ListRangeFactory(results).getSlice(3, '6', forwards=False))
self.assertEqual([5, 4, 3], _slice)
def test_getslice_next(self):
# The slice returned starts where indicated but continues on.
results = [0, 1, 2, 3, 4, 5]
_slice = ListRangeFactory(results).getSlice(3, '3')
self.assertEqual([3, 4, 5], _slice)
def test_getslice_before_middle(self):
# Going backwards does not include the anchor (because going forwards
# includes it)
results = [0, 1, 2, 3, 4, 5]
_slice = list(ListRangeFactory(results).getSlice(3, '3', forwards=False))
self.assertEqual([2, 1, 0], _slice)
def test_getSliceByIndex(self):
# getSliceByIndex returns the slice of the result limited by
# the indices start, end.
results = [0, 1, 2, 3, 4, 5]
_slice = ListRangeFactory(results).getSliceByIndex(2, 5)
self.assertEqual([2, 3, 4], _slice)
def test_getSliceByIndex__no_result_set(self):
# If no result set is present, getSliceByIndex() returns an
# empty list.
_slice = ListRangeFactory(None).getSliceByIndex(2, 5)
self.assertEqual([], _slice)
def test_picky_collection_ok(self):
p = PickyListLikeCollection(range(5))
self.assertEqual(range(5)[0:2], p[0:2])
def test_picky_collection_bad(self):
p = PickyListLikeCollection([])
# It would be nice to demonstrate p[-10:2] but it cannot be done
# directly since assertRaises needs individual parameters. The
# following is the equivalent.
self.assertRaises(RuntimeError,
p.__getitem__, slice(-10, 2, None))
def test_getSlice_empty_result_set_forwards(self):
results = PickyListLikeCollection([])
_slice = ListRangeFactory(results).getSlice(5, forwards=True)
self.assertEqual([], _slice)
def test_getSlice_empty_result_set_backwards(self):
results = PickyListLikeCollection([])
_slice = ListRangeFactory(results).getSlice(5, forwards=False)
self.assertEqual([], _slice)
def test_getSliceByIndex_empty_result_set(self):
results = PickyListLikeCollection([])
self.assertRaises(
RuntimeError,
ListRangeFactory(results).getSliceByIndex, -1, 1)
def test_rough_length(self):
# ListRangeFactory.rough_length returns the length of the
# result set.
factory = ListRangeFactory(range(4))
self.assertEqual(4, factory.rough_length)
lazr.batchnavigator-1.2.10/src/lazr/batchnavigator/version.txt 0000644 0001750 0000144 00000000007 11634121747 023344 0 ustar abel users 1.2.10
lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/ 0000755 0001750 0000144 00000000000 11634122075 022605 5 ustar abel users lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/namespace_packages.txt 0000644 0001750 0000144 00000000005 11634122075 027133 0 ustar abel users lazr
lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/dependency_links.txt 0000644 0001750 0000144 00000000001 11634122075 026653 0 ustar abel users
lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/requires.txt 0000644 0001750 0000144 00000000165 11634122075 025207 0 ustar abel users fixtures
setuptools
testtools
zope.cachedescriptors
zope.interface
zope.publisher
[docs]
Sphinx
z3c.recipe.sphinxdoc lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/SOURCES.txt 0000644 0001750 0000144 00000002066 11634122075 024475 0 ustar abel users README.txt
ez_setup.py
setup.py
src/lazr/__init__.py
src/lazr.batchnavigator.egg-info/PKG-INFO
src/lazr.batchnavigator.egg-info/SOURCES.txt
src/lazr.batchnavigator.egg-info/dependency_links.txt
src/lazr.batchnavigator.egg-info/namespace_packages.txt
src/lazr.batchnavigator.egg-info/not-zip-safe
src/lazr.batchnavigator.egg-info/requires.txt
src/lazr.batchnavigator.egg-info/top_level.txt
src/lazr/batchnavigator/NEWS.txt
src/lazr/batchnavigator/README.txt
src/lazr/batchnavigator/__init__.py
src/lazr/batchnavigator/_batchnavigator.py
src/lazr/batchnavigator/interfaces.py
src/lazr/batchnavigator/version.txt
src/lazr/batchnavigator/tests/__init__.py
src/lazr/batchnavigator/tests/test_batchnavigator.py
src/lazr/batchnavigator/tests/test_docs.py
src/lazr/batchnavigator/tests/test_z3batching.py
src/lazr/batchnavigator/z3batching/COPYRIGHT.txt
src/lazr/batchnavigator/z3batching/LICENSE.txt
src/lazr/batchnavigator/z3batching/README.txt
src/lazr/batchnavigator/z3batching/__init__.py
src/lazr/batchnavigator/z3batching/batch.py
src/lazr/batchnavigator/z3batching/interfaces.py lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/PKG-INFO 0000644 0001750 0000144 00000065354 11634122075 023717 0 ustar abel users Metadata-Version: 1.0
Name: lazr.batchnavigator
Version: 1.2.10
Summary: A helper to navigate batched results in a web page.
Home-page: https://launchpad.net/lazr.batchnavigator
Author: LAZR Developers
Author-email: lazr-users@lists.launchpad.net
License: LGPL v3
Download-URL: https://launchpad.net/lazr.batchnavigator/+download
Description: ..
This file is part of lazr.batchnavigator.
lazr.batchnavigator is free software: you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, version 3 of the License.
lazr.batchnavigator 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public License
along with lazr.batchnavigator. If not, see
.
Batch Navigation
****************
Batch navigation provides a way to navigate batch results in a web
page by providing URL links to the next, previous and numbered pages
of results.
It uses four query/POST arguments to control the batching:
- memo: A record of the underlying storage index pointer for the position of
the batch.
- direction: Indicates whether the memo is at the start or end of the batch.
- start: Cosmetic - used to calculate the apparent location (but note that
due to the concurrent nature of repeated visits to batches that the
true offset may differ - however the collection won't skip or show
items twice. For compatibility with saved URLs, if memo and
direction are both missing then start is used to do list slicing
into the collection.
- batch: Controls the amount of items we are showing per batch. It will only
appear if it's different from the default value set when the batch
is created.
These values can be overriden in the request, unless you also pass
force_start=True, which will make the start argument (again, defaulting to 0)
always chosen.
Imports:
>>> from lazr.batchnavigator import BatchNavigator, ListRangeFactory
>>> from zope.publisher.browser import TestRequest
>>> from zope.publisher.http import HTTPCharsets
>>> from zope.component import getSiteManager
>>> sm = getSiteManager()
>>> sm.registerAdapter(HTTPCharsets)
>>> def build_request(query_string_args=None, method='GET'):
... if query_string_args is None:
... query_string = ''
... else:
... if getattr(query_string_args, 'items', None) is not None:
... query_string_args = query_string_args.items()
... query_string = "&".join(
... ["%s=%s" % (k,v) for k,v in query_string_args])
... request = TestRequest(SERVER_URL='http://www.example.com/foo',
... method=method,
... environ={'QUERY_STRING': query_string})
... request.processInputs()
... return request
A dummy request object:
Some sample data.
>>> reindeer = ['Dasher', 'Dancer', 'Prancer', 'Vixen', 'Comet',
... 'Cupid', 'Donner', 'Blitzen', 'Rudolph']
Because slicing large collections can be very expensive, BatchNavigator offers
a non-slice protocol for determining the edge of batches. The range_factory
supplies an object implementing IRangeFactory and manages this protocol.
ListRangeFactory is a simple included implementation which BatchNavigator will
use if no range_factory is supplied.
>>> _ = BatchNavigator(reindeer, build_request(),
... range_factory=ListRangeFactory(reindeer))
For the examples in the documentation we let BatchNavigator construct a
range_factory implicitly:
>>> safe_reindeer = reindeer
>>> safe_reindeer_batch_navigator = BatchNavigator(
... safe_reindeer, build_request(), size=3)
An important feature of lazr.batchnavigator is its reluctance to
invoke len() on an underlying data set. len() can be an expensive
operation that provides little benefit, so this library tries hard to
avoid calling len() unless it's absolutely necessary. To show this
off, we'll define a subclass of Python's list type that explodes when
len() is invoked on it.
>>> class ListWithExplosiveLen(list):
... """A list subclass that doesn't like its len() being called."""
... def __len__(self):
... raise RuntimeError
Unless otherwise stated, we will use this list exclusively throughout
this test, to verify that len() is never called unless we want it to
be.
>>> explosive_reindeer = ListWithExplosiveLen(reindeer)
>>> reindeer_batch_navigator = BatchNavigator(
... explosive_reindeer, build_request(), size=3)
The BatchNavigator implements IBatchNavigator. We need to use the
'safe' batch navigator here, because verifyObject probes all methods
of the object it's passed, including __len__.
>>> from zope.interface.verify import verifyObject
>>> from lazr.batchnavigator.interfaces import IBatchNavigator
>>> verifyObject(IBatchNavigator, safe_reindeer_batch_navigator)
True
The BatchNavigator class provides IBatchNavigatorFactory. This can be used
to register a batch navigator factory as a utility, for instance.
>>> from lazr.batchnavigator.interfaces import IBatchNavigatorFactory
>>> verifyObject(IBatchNavigatorFactory, BatchNavigator)
True
You can ask the navigator for the chunk of results currently being shown
(e.g. to iterate over them for rendering in ZPT):
>>> list(reindeer_batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
You can ask for the first, previous, next and last results' links:
>>> reindeer_batch_navigator.firstBatchURL()
''
>>> reindeer_batch_navigator.prevBatchURL()
''
>>> reindeer_batch_navigator.nextBatchURL()
'http://www.example.com/foo?memo=3&start=3'
There's no way to get the URL to the final batch without knowing the
length of the entire list, so we'll use the safe batch navigator to
demonstrate lastBatchURL():
>>> safe_reindeer_batch_navigator.lastBatchURL()
'http://www.example.com/foo?direction=backwards&start=6'
The next link will be empty when there are no further results:
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
>>> last_reindeer_batch_navigator.nextBatchURL()
''
The first and previous link should appear even when we start at a point between 0
and the batch size:
>>> request = build_request({"start": "2", "batch": "3"})
>>> last_reindeer_batch_navigator = BatchNavigator(reindeer, request=request)
Here, we can see too that the batch argument appears as part of the URL.
That's because the request asked for a different size than the default
one when we create the Batch object, by default, it's 5.
>>> last_reindeer_batch_navigator.firstBatchURL()
'http://www.example.com/foo?batch=3'
>>> last_reindeer_batch_navigator.prevBatchURL()
'http://www.example.com/foo?batch=3&direction=backwards&memo=2'
This all works with other values in the query string, too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'})
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3)
>>> safe_reindeer_batch_navigator_with_qs = BatchNavigator(
... safe_reindeer, request, size=3)
In this case, we created the BatchNavigator with a default size of '3' and
the request is asking exactly that number of items per batch, and thus, we
don't need to show 'batch' as part of the URL.
>>> reindeer_batch_navigator_with_qs.firstBatchURL()
'http://www.example.com/foo?fnorb=bar'
>>> reindeer_batch_navigator_with_qs.prevBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&memo=3'
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=6&start=6'
(Again, there's no way to get the last batch without knowing the size
of the entire list.)
>>> safe_reindeer_batch_navigator_with_qs.lastBatchURL()
'http://www.example.com/foo?fnorb=bar&direction=backwards&start=6'
The ``force_start`` argument allows you to ignore the start value in the
request. This can be useful when, for instance, a filter has changed, and the
desired behavior is to restart at 0.
>>> reindeer_batch_navigator_with_qs = BatchNavigator(
... reindeer, request, size=3, force_start=True)
>>> reindeer_batch_navigator_with_qs.currentBatch().start
0
>>> reindeer_batch_navigator_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&memo=3&start=3'
>>> reindeer[:3] == list(reindeer_batch_navigator_with_qs.currentBatch())
True
We ensure that batch arguments supplied in the URL are observed
for POST operations too:
>>> request = build_request({'fnorb': 'bar',
... 'start': '3',
... 'batch': '3'}, method='POST')
>>> reindeer_batch_navigator_post_with_qs = BatchNavigator(
... reindeer, request)
>>> reindeer_batch_navigator_post_with_qs.start
3
>>> reindeer_batch_navigator_post_with_qs.nextBatchURL()
'http://www.example.com/foo?fnorb=bar&batch=3&memo=6&start=6'
We ensure that multiple size and batch arguments supplied in the URL don't
blow up the application. The first one is preferred.
>>> request = build_request(
... [('batch', '1'), ('batch', '7'), ('start', '2'), ('start', '10')])
>>> navigator = BatchNavigator(reindeer, request=request)
>>> navigator.nextBatchURL()
'http://www.example.com/foo?batch=1&memo=3&start=3'
The batch argument must be positive. Other numbers are ignored, and the
default batch size is used instead.
>>> from cgi import parse_qs
>>> request = build_request({'batch': '0'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
>>> request = build_request({'batch': '-1'})
>>> navigator = BatchNavigator(range(99), request=request)
>>> print 'batch' in parse_qs(navigator.nextBatchURL())
False
=============
Empty Batches
=============
You can also create an empty batch that will not have any items:
>>> null_batch_navigator = BatchNavigator(
... None, build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
>>> null_batch_navigator = BatchNavigator(
... [], build_request(), size=3)
>>> null_batch_navigator.firstBatchURL()
''
>>> null_batch_navigator.nextBatchURL()
''
>>> null_batch_navigator.prevBatchURL()
''
>>> null_batch_navigator.lastBatchURL()
''
TODO:
- blowing up when start is beyond end
- orphans
- overlap
====================================
Supporting Results Without a __len__
====================================
Some result objects do not implement __len__ because generally Python code
assumes that __len__ is cheap. SQLObject and Storm result sets both have this
behavior, for instance, so that it is cleat that getting the length is a non-
trivial operation.
To support these objects, the batch looks for __len__ on the result set. If
it does not exist, it adapts the result to
zope.interface.common.sequence.IFiniteSequence and uses that __len__.
>>> class ExampleResultSet(object):
... def __init__(self, results):
... self.stub_results = results
... def count(self):
... # imagine this actually returned
... return len(self.stub_results)
... def __getitem__(self, ix):
... return self.stub_results[ix] # also works with slices
... def __iter__(self):
... return iter(self.stub_results)
...
>>> from zope.interface import implements
>>> from zope.component import adapts, getSiteManager
>>> from zope.interface.common.sequence import IFiniteSequence
>>> class ExampleAdapter(ExampleResultSet):
... adapts(ExampleResultSet)
... implements(IFiniteSequence)
... def __len__(self):
... return self.stub_results.count()
...
>>> sm = getSiteManager()
>>> sm.registerAdapter(ExampleAdapter)
>>> example = ExampleResultSet(safe_reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> example_batch_navigator.currentBatch().total()
9
========================
Only Gets What Is Needed
========================
It's also important for performance of batching large result sets that the
batch only gets a slice of the results, rather than accessing the entirety.
>>> class ExampleResultSet(ExampleResultSet):
... def __init__(self, results):
... super(ExampleResultSet, self).__init__(results)
... self.getitem_history = []
... def __getitem__(self, ix):
... self.getitem_history.append(ix)
... return super(ExampleResultSet, self).__getitem__(ix)
...
>>> example = ExampleResultSet(reindeer)
>>> example_batch_navigator = BatchNavigator(
... example, build_request(), size=3)
>>> reindeer[:3] == list(example_batch_navigator.currentBatch())
True
>>> example.getitem_history
[slice(0, 4, None)]
Note that although the batch is of the size requested, the underlying
list contains one more item than is necessary. This is to make it easy
to determine whether a given batch is the final one in the list,
without having to explicitly look up the length of the list
(potentially an expensive operation).
=========================
Adding callback functions
=========================
Sometimes it is useful to have a function called with the batched
values once they have been determined. This is the case when there
are subsequent queries that are needed to be executed for each batch,
and it is undesirable or overly expensive to execute the query for
every value in the entire result set.
The callback function must define two parameters. The first is the
batch navigator object itself, and the second it the current batch.
The callback function is called once and only once when the
BatchNavigator is constructed, and the current batch is determined.
>>> def print_callback(context, batch):
... for item in batch:
... print item
>>> reindeer_batch_navigator = BatchNavigator(
... reindeer, build_request(), size=3, callback=print_callback)
Dasher
Dancer
Prancer
>>> request = build_request({"start": "3", "batch": "20"})
>>> last_reindeer_batch_navigator = BatchNavigator(
... reindeer, request=request, callback=print_callback)
Vixen
Comet
Cupid
Donner
Blitzen
Rudolph
Most likely, the callback function will be bound to a view class.
By providing the batch navigator itself as the context for the
callback allows the addition of extra member variables. This is
useful as the BatchNavigator becomes the context in page templates
that are batched.
>>> class ReindeerView:
... def constructReindeerFromAtoms(self, context, batch):
... # some significantly slow process
... view.built_reindeer = list(batch)
... def batchedReindeer(self):
... return BatchNavigator(
... reindeer, build_request(), size=3,
... callback=self.constructReindeerFromAtoms)
>>> view = ReindeerView()
>>> batch_navigator = view.batchedReindeer()
>>> print view.built_reindeer
['Dasher', 'Dancer', 'Prancer']
>>> print list(batch_navigator.currentBatch())
['Dasher', 'Dancer', 'Prancer']
==================
Maximum batch size
==================
Since the batch size is exposed in the URL, it's possible for users to
tweak the batch parameter to retrieve more results. Since that may
potentially exhaust server resources, an upper limit is put on the batch
size. If the requested batch parameter is higher than this, an
InvalidBatchSizeError is raised.
>>> class DemoBatchNavigator(BatchNavigator):
... max_batch_size = 5
...
>>> request = build_request({"start": "0", "batch": "20"})
>>> DemoBatchNavigator(reindeer, request=request )
Traceback (most recent call last):
...
InvalidBatchSizeError: Maximum for "batch" parameter is 5.
==============
URL parameters
==============
Normally, any parameters passed in the current page's URL are
reproduced in the batch navigator's links. A "transient" parameter is
one that was only relevant for the current page request and shouldn't be
passed on to subsequent ones.
In this next batch navigator, two parameters occur in the page's URL:
"noisy" and "quiet."
>>> request_parameters = {
... 'quiet': 'ssht',
... 'noisy': 'HELLO',
... }
>>> request_with_parameters = build_request(request_parameters)
One parameter, "quiet," is transient. There is another transient
parameter called "absent," but it's not passed in our ongoing page
request.
>>> def build_navigator(list):
... return BatchNavigator(
... list, request_with_parameters, size=3,
... transient_parameters=['quiet', 'absent'])
>>> navigator_with_parameters = build_navigator(reindeer)
>>> safe_navigator_with_parameters = build_navigator(safe_reindeer)
Of these three parameters, only "noisy" recurs in the links produced by
the batch navigator.
>>> navigator_with_parameters.nextBatchURL()
'http://www.example.com/foo?noisy=HELLO&memo=3&start=3'
>>> safe_navigator_with_parameters.lastBatchURL()
'http://www.example.com/foo?noisy=HELLO&direction=backwards&start=6'
The transient parameter is omitted, and the one that was never passed in
in the first place does not magically appear.
==============
Batch headings
==============
The batched values are usually one kind of object such as bugs. The
BatchNavigator's heading property contains a description of the objects
for display.
>>> safe_reindeer_batch_navigator.heading
'results'
There is a special case for when there is only one item in the batch,
the singular version of the heading is returned.
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.heading
'result'
(Accessing .heading causes len() to be called on the underlying list,
which is why we have to use the safe batch navigator. In theory, this
could be optimized, but there's no real point, since the heading is
invariably preceded by the actual length of the underlying list,
eg. "10 results". Since len() is called anyway, and its value is
cached, a second len() won't hurt performance.)
The heading can be set by passing a singular and a plural version of
the heading. The batch navigation will return the appropriate
header based on the total items in the batch.
>>> navigator = BatchNavigator(safe_reindeer, request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bugs'
>>> navigator = BatchNavigator(['only-one'], request=request)
>>> navigator.setHeadings('bug', 'bugs')
>>> navigator.heading
'bug'
(Cleanup)
>>> sm.unregisterAdapter(HTTPCharsets)
True
>>> sm.unregisterAdapter(ExampleAdapter)
True
===============
Other Documents
===============
.. toctree::
:glob:
*
docs/*
============================
NEWS for lazr.batchnavigator
============================
1.2.10 (2011-09-14)
- delegate the calculation of the rough length of a result set to
IRangeFactory.
1.2.9 (2011-08-25)
- When a backwards batch is at first too short and when another chunk
from the result set is added, _Batch,sliced_list() does no longer
use the memo value for the already retrived chunk.
- don't use the parameter start to determine if a previous/next batch
exists; don't rely on len(resultset) and to determine the real size
of a batch.
- Avoid negative start index on empty result sets.
1.2.7 (2011-07-18)
==================
- retrieve slices of the result set in class _Batch only via methods
of the range factory.
1.2.6 (2011-07-28)
==================
- fixed an error in handling backwards batches which return less elements
than expected.
- URL-encode all query parameters in BatchNavigator.generateBatchURL()
1.2.5 (2011-07-13)
==================
- Permit changing all variable names with a single prefix.
1.2.4 (2011-04-11)
==================
- Permit overriding determineSize to control how the batch default and concrete
sizes are determined in subclasses.
- Listify (once we have sliced) rather than assuming batched slices will honour
the complete list protocol.
1.2.3 (2011-04-06)
==================
- Add IRangeFactory and the ability to use backend database hints for efficient
retrieval of pages.
- Remove terrible-scaling getBatchURLs method.
1.2.2 (2010-08-19)
==================
- Make len() cheap to call when the current batch is the last (or
only) batch.
- Avoid calling len() when generating navigator URLs.
1.2.1 (2010-08-12)
==================
- fix a bug in the len() of a batch when the batch had previously been
iterated over
1.2.0 (2010-08-05)
==================
- avoid calling len() on the underlying sequence when possible
- return None for endNumber when the batch is out of range
1.1.1 (2010-05-10)
==================
- Ignore negative batch sizes
1.1 (2009-08-31)
================
- Remove build dependencies on bzr and egg_info
- remove sys.path hack in setup.py for __version__
1.0 (2009-03-24)
================
- Initial release on PyPI
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/not-zip-safe 0000644 0001750 0000144 00000000001 11620231531 025024 0 ustar abel users
lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/top_level.txt 0000644 0001750 0000144 00000000005 11634122075 025332 0 ustar abel users lazr