lazr.batchnavigator-1.2.10/0000755000175000001440000000000011634122075014361 5ustar abeluserslazr.batchnavigator-1.2.10/README.txt0000644000175000001440000000135511620231465016062 0ustar abelusersA 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.py0000755000175000001440000000477111620231465016106 0ustar abelusers#!/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.py0000644000175000001440000002246611620231465016602 0ustar abelusers#!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.cfg0000644000175000001440000000007311634122075016202 0ustar abelusers[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 lazr.batchnavigator-1.2.10/PKG-INFO0000644000175000001440000006535411634122075015473 0ustar abelusersMetadata-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/0000755000175000001440000000000011634122075015150 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/0000755000175000001440000000000011634122075016120 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/__init__.py0000644000175000001440000000164111620231465020232 0ustar abelusers# 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/0000755000175000001440000000000011634122075021114 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/0000755000175000001440000000000011634122075023150 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/README.txt0000644000175000001440000000055311620231465024650 0ustar abelusersThis 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__.py0000644000175000001440000000000011620231465025246 0ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/batchnavigator/z3batching/interfaces.py0000644000175000001440000000507611631660274025662 0ustar abelusers############################################################################## # # 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.py0000644000175000001440000003440711631712667024624 0ustar abelusers############################################################################## # # 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.txt0000644000175000001440000000420211620231465024770 0ustar abelusersZope 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.txt0000644000175000001440000000074211620231465025263 0ustar abelusersCopyright (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.txt0000644000175000001440000004611311620231465022616 0ustar abelusers.. 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.txt0000644000175000001440000000427511634121714022440 0ustar abelusers============================ 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.py0000644000175000001440000003367011631712667024643 0ustar abelusers# 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__.py0000644000175000001440000000222411620231465023224 0ustar abelusers# 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.py0000644000175000001440000001401011631712667023616 0ustar abelusers# 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/0000755000175000001440000000000011634122075022256 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr/batchnavigator/tests/test_z3batching.py0000644000175000001440000007560611631712667025752 0ustar abelusers############################################################################## # # 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__.py0000644000175000001440000000232611620231465024371 0ustar abelusers# 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.py0000644000175000001440000000343511620231465024623 0ustar abelusers# 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.py0000644000175000001440000003451411631712667026703 0ustar abelusers# 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.txt0000644000175000001440000000000711634121747023344 0ustar abelusers1.2.10 lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/0000755000175000001440000000000011634122075022605 5ustar abeluserslazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/namespace_packages.txt0000644000175000001440000000000511634122075027133 0ustar abeluserslazr lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/dependency_links.txt0000644000175000001440000000000111634122075026653 0ustar abelusers lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/requires.txt0000644000175000001440000000016511634122075025207 0ustar abelusersfixtures setuptools testtools zope.cachedescriptors zope.interface zope.publisher [docs] Sphinx z3c.recipe.sphinxdoclazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/SOURCES.txt0000644000175000001440000000206611634122075024475 0ustar abelusersREADME.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.pylazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/PKG-INFO0000644000175000001440000006535411634122075023717 0ustar abelusersMetadata-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-safe0000644000175000001440000000000111620231531025024 0ustar abelusers lazr.batchnavigator-1.2.10/src/lazr.batchnavigator.egg-info/top_level.txt0000644000175000001440000000000511634122075025332 0ustar abeluserslazr