from collections import deque
class BrowserHistory(object):
'''Class for keeping track of the history while moving to different locations,
as implemented in Web browsers.
Both back and forward history are supported and each of them can be bounded
or unbounded.
>>> h = BrowserHistory(max_back=3, max_forward=3)
>>> h.location = 'http://www.google.com/'
>>> h.location = 'http://www.python.org/'
>>> h.location = 'http://xkcd.com/'
>>> h.location = 'http://www.slashdot.org/'
>>> h.location = 'http://www.digg.com/'
>>> h.back()
'http://www.slashdot.org/'
>>> h.forward()
'http://www.digg.com/'
>>> h.go(-2)
'http://xkcd.com/'
>>> # max_back=3 so any result more than 3 pages back is deleted
>>> for i_loc in h.indexed_locations():
... print '%+d: %s' % i_loc
+2: http://www.digg.com/
+1: http://www.slashdot.org/
+0: http://xkcd.com/
-1: http://www.python.org/
>>> # visiting a new location erases the forward history
>>> h.location = 'http://en.wikipedia.org/'
>>> for i_loc in h.indexed_locations():
... print '%+d: %s' % i_loc
+0: http://en.wikipedia.org/
-1: http://xkcd.com/
-2: http://www.python.org/
'''
def __init__(self, location=None, max_back=None, max_forward=None):
'''Initialize a new BrowserHistory instance.
:param location: If not None, the initial location.
:param max_back: The maximum number of back locations to remember
(default: no limit)
:param max_forward: The maximum number of forward locations to remember
(default: no limit)
'''
if max_back is not None:
max_back += 1 # +1 for storing the current location
self._back = deque(maxlen=max_back)
self._forward = deque(maxlen=max_forward)
if location is not None:
self.location = location
@property
def location(self):
'''The current location, i.e. the last location set or went to by
:meth:`back`, :meth:`forward` or :meth:`go`.
'''
if self._back:
return self._back[-1]
raise AttributeError('location has not been set')
@location.setter
def location(self, value):
'''Go to a new location. Any existing forward history is erased.'''
self._back.append(value)
self._forward.clear()
def back(self, i=1):
'''Jump to a location in history ``i`` steps back.
:param i: The number of steps to jump back.
:type i: positive int
:returns: The current :attr:`location`.
'''
if i > 0:
push_forward = self._forward.appendleft
pop_back = self._back.pop
for _ in xrange(min(i, len(self._back)-1)):
push_forward(pop_back())
return self.location
def forward(self, i=1):
'''Jump to a location in history ``i`` steps forward.
:param i: The number of steps to jump forward.
:type i: positive int
:returns: The current :attr:`location`.
'''
if i > 0:
push_back = self._back.append
pop_forward = self._forward.popleft
for _ in xrange(min(i, len(self._forward))):
push_back(pop_forward())
return self.location
def go(self, i):
'''Jump to a location in history ``i`` steps back or forward.
:param i: The number of steps to jump forward (if positive) or back (if negative).
:type i: int
:returns: The current :attr:`location`.
'''
if i < 0:
return self.back(-i)
if i > 0:
return self.forward(i)
return self.location
def indexed_locations(self):
'''Return a list of ``(index,location)`` tuples for each location in history.
The index ``i`` of a location ``l`` is defined so that ``go(i) == l``.
Therefore indexes are positive for forward locations, negative for back
locations and zero for the current :attr:`location`.
'''
result = []
# back and current locations
n = len(self._back)-1
result.extend((i-n, location) for i,location in enumerate(self._back))
# forward locations
result.extend((i+1, location) for i,location in enumerate(self._forward))
result.reverse()
return result
if __name__ == '__main__':
import doctest; doctest.testmod()