Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/paginate/__init__.py : 16%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2007-2012 Christoph Haas <email@christoph-haas.de>
2# See the file LICENSE for copying permission.
4"""
5paginate: helps split up large collections into individual pages
6================================================================
8What is pagination?
9---------------------
11This module helps split large lists of items into pages. The user is shown one page at a time and
12can navigate to other pages. Imagine you are offering a company phonebook and let the user search
13the entries. The entire search result may contains 23 entries but you want to display no more than
1410 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23.
15Each "Page" instance represents the items of one of these three pages.
17See the documentation of the "Page" class for more information.
19How do I use it?
20------------------
22A page of items is represented by the *Page* object. A *Page* gets initialized with these arguments:
24- The collection of items to pick a range from. Usually just a list.
25- The page number you want to display. Default is 1: the first page.
27Now we can make up a collection and create a Page instance of it::
29 # Create a sample collection of 1000 items
30 >> my_collection = range(1000)
32 # Create a Page object for the 3rd page (20 items per page is the default)
33 >> my_page = Page(my_collection, page=3)
35 # The page object can be printed as a string to get its details
36 >> str(my_page)
37 Page:
38 Collection type: <type 'range'>
39 Current page: 3
40 First item: 41
41 Last item: 60
42 First page: 1
43 Last page: 50
44 Previous page: 2
45 Next page: 4
46 Items per page: 20
47 Number of items: 1000
48 Number of pages: 50
50 # Print a list of items on the current page
51 >> my_page.items
52 [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
54 # The *Page* object can be used as an iterator:
55 >> for my_item in my_page: print(my_item)
56 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
58 # The .pager() method returns an HTML fragment with links to surrounding pages.
59 >> my_page.pager(url="http://example.org/foo/page=$page")
61 <a href="http://example.org/foo/page=1">1</a>
62 <a href="http://example.org/foo/page=2">2</a>
63 3
64 <a href="http://example.org/foo/page=4">4</a>
65 <a href="http://example.org/foo/page=5">5</a>
66 ..
67 <a href="http://example.org/foo/page=50">50</a>'
69 # Without the HTML it would just look like:
70 # 1 2 [3] 4 5 .. 50
72 # The pager can be customized:
73 >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)',
74 url="http://example.org/foo/page=$page")
76 <a href="http://example.org/foo/page=2"><</a>
77 <a href="http://example.org/foo/page=1">1</a>
78 <a href="http://example.org/foo/page=2">2</a>
79 3
80 <a href="http://example.org/foo/page=4">4</a>
81 <a href="http://example.org/foo/page=5">5</a>
82 <a href="http://example.org/foo/page=6">6</a>
83 ..
84 <a href="http://example.org/foo/page=50">50</a>
85 <a href="http://example.org/foo/page=4">></a>
86 (Page 3 of 50)
88 # Without the HTML it would just look like:
89 # 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50)
91 # The url argument to the pager method can be omitted when an url_maker is
92 # given during instantiation:
93 >> my_page = Page(my_collection, page=3,
94 url_maker=lambda p: "http://example.org/%s" % p)
95 >> page.pager()
97There are some interesting parameters that customize the Page's behavior. See the documentation on
98``Page`` and ``Page.pager()``.
101Notes
102-------
104Page numbers and item numbers start at 1. This concept has been used because users expect that the
105first page has number 1 and the first item on a page also has number 1. So if you want to use the
106page's items by their index number please note that you have to subtract 1.
107"""
109__author__ = "Christoph Haas"
110__copyright__ = "Copyright 2007-2016 Christoph Haas and contributors"
111__credits__ = ["Mike Orr"]
112__license__ = "MIT"
113__version__ = "0.5.4"
114__maintainer__ = "Marcin Lulek, Luke Crooks"
115__email__ = "info@webreactor.eu, luke@pumalo.org"
116__status__ = "Beta"
119import re
120from string import Template
121import sys
123# are we running at least python 3.x ?
124PY3 = sys.version_info[0] >= 3
126if PY3:
127 unicode = str
130# Since the items on a page are mainly a list we subclass the "list" type
131class Page(list):
132 """A list/iterator representing the items on one page of a larger collection.
134 An instance of the "Page" class is created from a _collection_ which is any
135 list-like object that allows random access to its elements.
137 The instance works as an iterator running from the first item to the last item on the given
138 page. The Page.pager() method creates a link list allowing the user to go to other pages.
140 A "Page" does not only carry the items on a certain page. It gives you additional information
141 about the page in these "Page" object attributes:
143 item_count
144 Number of items in the collection
146 **WARNING:** Unless you pass in an item_count, a count will be
147 performed on the collection every time a Page instance is created.
149 page
150 Number of the current page
152 items_per_page
153 Maximal number of items displayed on a page
155 first_page
156 Number of the first page - usually 1 :)
158 last_page
159 Number of the last page
161 previous_page
162 Number of the previous page. If this is the first page it returns None.
164 next_page
165 Number of the next page. If this is the first page it returns None.
167 page_count
168 Number of pages
170 items
171 Sequence/iterator of items on the current page
173 first_item
174 Index of first item on the current page - starts with 1
176 last_item
177 Index of last item on the current page
178 """
179 def __init__(self, collection, page=1, items_per_page=20, item_count=None,
180 wrapper_class=None, url_maker=None, **kwargs):
181 """Create a "Page" instance.
183 Parameters:
185 collection
186 Sequence representing the collection of items to page through.
188 page
189 The requested page number - starts with 1. Default: 1.
191 items_per_page
192 The maximal number of items to be displayed per page.
193 Default: 20.
195 item_count (optional)
196 The total number of items in the collection - if known.
197 If this parameter is not given then the paginator will count
198 the number of elements in the collection every time a "Page"
199 is created. Giving this parameter will speed up things. In a busy
200 real-life application you may want to cache the number of items.
202 url_maker (optional)
203 Callback to generate the URL of other pages, given its numbers.
204 Must accept one int parameter and return a URI string.
205 """
206 if collection is not None:
207 if wrapper_class is None:
208 # Default case. The collection is already a list-type object.
209 self.collection = collection
210 else:
211 # Special case. A custom wrapper class is used to access elements of the collection.
212 self.collection = wrapper_class(collection)
213 else:
214 self.collection = []
216 self.collection_type = type(collection)
218 if url_maker is not None:
219 self.url_maker = url_maker
220 else:
221 self.url_maker = self._default_url_maker
223 # Assign kwargs to self
224 self.kwargs = kwargs
226 # The self.page is the number of the current page.
227 # The first page has the number 1!
228 try:
229 self.page = int(page) # make it int() if we get it as a string
230 except (ValueError, TypeError):
231 self.page = 1
232 # normally page should be always at least 1 but the original maintainer
233 # decided that for empty collection and empty page it can be...0? (based on tests)
234 # preserving behavior for BW compat
235 if self.page < 1:
236 self.page = 1
238 self.items_per_page = items_per_page
240 # We subclassed "list" so we need to call its init() method
241 # and fill the new list with the items to be displayed on the page.
242 # We use list() so that the items on the current page are retrieved
243 # only once. In an SQL context that could otherwise lead to running the
244 # same SQL query every time items would be accessed.
245 # We do this here, prior to calling len() on the collection so that a
246 # wrapper class can execute a query with the knowledge of what the
247 # slice will be (for efficiency) and, in the same query, ask for the
248 # total number of items and only execute one query.
249 try:
250 first = (self.page - 1) * items_per_page
251 last = first + items_per_page
252 self.items = list(self.collection[first:last])
253 except TypeError:
254 raise TypeError("Your collection of type {} cannot be handled "
255 "by paginate.".format(type(self.collection)))
257 # Unless the user tells us how many items the collections has
258 # we calculate that ourselves.
259 if item_count is not None:
260 self.item_count = item_count
261 else:
262 self.item_count = len(self.collection)
264 # Compute the number of the first and last available page
265 if self.item_count > 0:
266 self.first_page = 1
267 self.page_count = ((self.item_count - 1) // self.items_per_page) + 1
268 self.last_page = self.first_page + self.page_count - 1
270 # Make sure that the requested page number is the range of valid pages
271 if self.page > self.last_page:
272 self.page = self.last_page
273 elif self.page < self.first_page:
274 self.page = self.first_page
276 # Note: the number of items on this page can be less than
277 # items_per_page if the last page is not full
278 self.first_item = (self.page - 1) * items_per_page + 1
279 self.last_item = min(self.first_item + items_per_page - 1, self.item_count)
281 # Links to previous and next page
282 if self.page > self.first_page:
283 self.previous_page = self.page-1
284 else:
285 self.previous_page = None
287 if self.page < self.last_page:
288 self.next_page = self.page+1
289 else:
290 self.next_page = None
292 # No items available
293 else:
294 self.first_page = None
295 self.page_count = 0
296 self.last_page = None
297 self.first_item = None
298 self.last_item = None
299 self.previous_page = None
300 self.next_page = None
301 self.items = []
303 # This is a subclass of the 'list' type. Initialise the list now.
304 list.__init__(self, self.items)
306 def __str__(self):
307 return ("Page:\n"
308 "Collection type: {0.collection_type}\n"
309 "Current page: {0.page}\n"
310 "First item: {0.first_item}\n"
311 "Last item: {0.last_item}\n"
312 "First page: {0.first_page}\n"
313 "Last page: {0.last_page}\n"
314 "Previous page: {0.previous_page}\n"
315 "Next page: {0.next_page}\n"
316 "Items per page: {0.items_per_page}\n"
317 "Total number of items: {0.item_count}\n"
318 "Number of pages: {0.page_count}\n"
319 ).format(self)
321 def __repr__(self):
322 return("<paginate.Page: Page {0}/{1}>".format(self.page, self.page_count))
324 def pager(self, format='~2~', url=None, show_if_single_page=False, separator=' ',
325 symbol_first='<<', symbol_last='>>', symbol_previous='<', symbol_next='>',
326 link_attr=dict(), curpage_attr=dict(), dotdot_attr=dict(), link_tag=None):
327 """
328 Return string with links to other pages (e.g. '1 .. 5 6 7 [8] 9 10 11 .. 50').
330 format:
331 Format string that defines how the pager is rendered. The string
332 can contain the following $-tokens that are substituted by the
333 string.Template module:
335 - $first_page: number of first reachable page
336 - $last_page: number of last reachable page
337 - $page: number of currently selected page
338 - $page_count: number of reachable pages
339 - $items_per_page: maximal number of items per page
340 - $first_item: index of first item on the current page
341 - $last_item: index of last item on the current page
342 - $item_count: total number of items
343 - $link_first: link to first page (unless this is first page)
344 - $link_last: link to last page (unless this is last page)
345 - $link_previous: link to previous page (unless this is first page)
346 - $link_next: link to next page (unless this is last page)
348 To render a range of pages the token '~3~' can be used. The
349 number sets the radius of pages around the current page.
350 Example for a range with radius 3:
352 '1 .. 5 6 7 [8] 9 10 11 .. 50'
354 Default: '~2~'
356 url
357 The URL that page links will point to. Make sure it contains the string
358 $page which will be replaced by the actual page number.
359 Must be given unless a url_maker is specified to __init__, in which
360 case this parameter is ignored.
362 symbol_first
363 String to be displayed as the text for the $link_first link above.
365 Default: '<<' (<<)
367 symbol_last
368 String to be displayed as the text for the $link_last link above.
370 Default: '>>' (>>)
372 symbol_previous
373 String to be displayed as the text for the $link_previous link above.
375 Default: '<' (<)
377 symbol_next
378 String to be displayed as the text for the $link_next link above.
380 Default: '>' (>)
382 separator:
383 String that is used to separate page links/numbers in the above range of pages.
385 Default: ' '
387 show_if_single_page:
388 if True the navigator will be shown even if there is only one page.
390 Default: False
392 link_attr (optional)
393 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
394 be used to define a CSS style or class to customize the look of links.
396 Example: { 'style':'border: 1px solid green' }
397 Example: { 'class':'pager_link' }
399 curpage_attr (optional)
400 A dictionary of attributes that get added to the current page number in the pager (which
401 is obviously not a link). If this dictionary is not empty then the elements will be
402 wrapped in a SPAN tag with the given attributes.
404 Example: { 'style':'border: 3px solid blue' }
405 Example: { 'class':'pager_curpage' }
407 dotdot_attr (optional)
408 A dictionary of attributes that get added to the '..' string in the pager (which is
409 obviously not a link). If this dictionary is not empty then the elements will be wrapped
410 in a SPAN tag with the given attributes.
412 Example: { 'style':'color: #808080' }
413 Example: { 'class':'pager_dotdot' }
415 link_tag (optional)
416 A callable that accepts single argument `page` (page link information)
417 and generates string with html that represents the link for specific page.
418 Page objects are supplied from `link_map()` so the keys are the same.
421 """
422 self.curpage_attr = curpage_attr
423 self.separator = separator
424 self.link_attr = link_attr
425 self.dotdot_attr = dotdot_attr
426 self.url = url
427 self.link_tag = link_tag or self.default_link_tag
429 # Don't show navigator if there is no more than one page
430 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
431 return ''
433 regex_res = re.search(r'~(\d+)~', format)
434 if regex_res:
435 radius = regex_res.group(1)
436 else:
437 radius = 2
438 radius = int(radius)
439 self.radius = radius
440 link_map = self.link_map(
441 format=format, url=url, show_if_single_page=show_if_single_page, separator=separator,
442 symbol_first=symbol_first, symbol_last=symbol_last, symbol_previous=symbol_previous,
443 symbol_next=symbol_next, link_attr=link_attr, curpage_attr=curpage_attr, dotdot_attr=dotdot_attr
444 )
445 links_markup = self._range(link_map, radius)
447 # Replace ~...~ in token format by range of pages
448 result = re.sub(r'~(\d+)~', links_markup, format)
450 # Interpolate '$' variables
451 result = Template(result).safe_substitute({
452 'first_page': self.first_page,
453 'last_page': self.last_page,
454 'page': self.page,
455 'page_count': self.page_count,
456 'items_per_page': self.items_per_page,
457 'first_item': self.first_item,
458 'last_item': self.last_item,
459 'item_count': self.item_count,
460 'link_first': self.page>self.first_page and self.link_tag(link_map['first_page']) or '',
461 'link_last': self.page<self.last_page and self.link_tag(link_map['last_page']) or '',
462 'link_previous': self.previous_page and self.link_tag(link_map['previous_page']) or '',
463 'link_next': self.next_page and self.link_tag(link_map['next_page']) or ''
464 })
466 return result
468 def link_map(self, format='~2~', url=None, show_if_single_page=False, separator=' ',
469 symbol_first='<<', symbol_last='>>', symbol_previous='<', symbol_next='>',
470 link_attr=dict(), curpage_attr=dict(), dotdot_attr=dict()):
471 """ Return map with links to other pages if default pager() function is not suitable solution.
472 format:
473 Format string that defines how the pager would be normally rendered rendered. Uses same arguments as pager()
474 method, but returns a simple dictionary in form of:
475 {'current_page': {'attrs': {},
476 'href': 'http://example.org/foo/page=1',
477 'value': 1},
478 'first_page': {'attrs': {},
479 'href': 'http://example.org/foo/page=1',
480 'type': 'first_page',
481 'value': 1},
482 'last_page': {'attrs': {},
483 'href': 'http://example.org/foo/page=8',
484 'type': 'last_page',
485 'value': 8},
486 'next_page': {'attrs': {}, 'href': 'HREF', 'type': 'next_page', 'value': 2},
487 'previous_page': None,
488 'range_pages': [{'attrs': {},
489 'href': 'http://example.org/foo/page=1',
490 'type': 'current_page',
491 'value': 1},
492 ....
493 {'attrs': {}, 'href': '', 'type': 'span', 'value': '..'}]}
496 The string can contain the following $-tokens that are substituted by the
497 string.Template module:
499 - $first_page: number of first reachable page
500 - $last_page: number of last reachable page
501 - $page: number of currently selected page
502 - $page_count: number of reachable pages
503 - $items_per_page: maximal number of items per page
504 - $first_item: index of first item on the current page
505 - $last_item: index of last item on the current page
506 - $item_count: total number of items
507 - $link_first: link to first page (unless this is first page)
508 - $link_last: link to last page (unless this is last page)
509 - $link_previous: link to previous page (unless this is first page)
510 - $link_next: link to next page (unless this is last page)
512 To render a range of pages the token '~3~' can be used. The
513 number sets the radius of pages around the current page.
514 Example for a range with radius 3:
516 '1 .. 5 6 7 [8] 9 10 11 .. 50'
518 Default: '~2~'
520 url
521 The URL that page links will point to. Make sure it contains the string
522 $page which will be replaced by the actual page number.
523 Must be given unless a url_maker is specified to __init__, in which
524 case this parameter is ignored.
526 symbol_first
527 String to be displayed as the text for the $link_first link above.
529 Default: '<<' (<<)
531 symbol_last
532 String to be displayed as the text for the $link_last link above.
534 Default: '>>' (>>)
536 symbol_previous
537 String to be displayed as the text for the $link_previous link above.
539 Default: '<' (<)
541 symbol_next
542 String to be displayed as the text for the $link_next link above.
544 Default: '>' (>)
546 separator:
547 String that is used to separate page links/numbers in the above range of pages.
549 Default: ' '
551 show_if_single_page:
552 if True the navigator will be shown even if there is only one page.
554 Default: False
556 link_attr (optional)
557 A dictionary of attributes that get added to A-HREF links pointing to other pages. Can
558 be used to define a CSS style or class to customize the look of links.
560 Example: { 'style':'border: 1px solid green' }
561 Example: { 'class':'pager_link' }
563 curpage_attr (optional)
564 A dictionary of attributes that get added to the current page number in the pager (which
565 is obviously not a link). If this dictionary is not empty then the elements will be
566 wrapped in a SPAN tag with the given attributes.
568 Example: { 'style':'border: 3px solid blue' }
569 Example: { 'class':'pager_curpage' }
571 dotdot_attr (optional)
572 A dictionary of attributes that get added to the '..' string in the pager (which is
573 obviously not a link). If this dictionary is not empty then the elements will be wrapped
574 in a SPAN tag with the given attributes.
576 Example: { 'style':'color: #808080' }
577 Example: { 'class':'pager_dotdot' }
578 """
579 self.curpage_attr = curpage_attr
580 self.separator = separator
581 self.link_attr = link_attr
582 self.dotdot_attr = dotdot_attr
583 self.url = url
585 regex_res = re.search(r'~(\d+)~', format)
586 if regex_res:
587 radius = regex_res.group(1)
588 else:
589 radius = 2
590 radius = int(radius)
591 self.radius = radius
593 # Compute the first and last page number within the radius
594 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
595 # -> leftmost_page = 5
596 # -> rightmost_page = 9
597 leftmost_page = max(self.first_page, (self.page-radius)) if \
598 self.first_page else None
599 rightmost_page = min(self.last_page, (self.page+radius)) if \
600 self.last_page else None
602 nav_items = {
603 "first_page": None,
604 "last_page": None,
605 "previous_page": None,
606 "next_page": None,
607 "current_page": None,
608 "radius": self.radius,
609 "range_pages": []
610 }
612 if leftmost_page is None or rightmost_page is None:
613 return nav_items
615 nav_items["first_page"] = {"type": "first_page", "value": unicode(symbol_first), "attrs": self.link_attr,
616 "number": self.first_page, "href": self.url_maker(self.first_page)}
618 # Insert dots if there are pages between the first page
619 # and the currently displayed page range
620 if leftmost_page - self.first_page > 1:
621 # Wrap in a SPAN tag if dotdot_attr is set
622 nav_items["range_pages"].append({"type": "span", "value": '..', "attrs": self.dotdot_attr, "href": "",
623 "number": None})
625 for thispage in range(leftmost_page, rightmost_page+1):
626 # Highlight the current page number and do not use a link
627 if thispage == self.page:
628 # Wrap in a SPAN tag if curpage_attr is set
629 nav_items["range_pages"].append({"type": "current_page", "value": unicode(thispage), "number": thispage,
630 "attrs": self.curpage_attr, "href": self.url_maker(thispage)})
631 nav_items["current_page"] = {"value": thispage, "attrs": self.curpage_attr,
632 "type": "current_page", "href": self.url_maker(thispage)}
633 # Otherwise create just a link to that page
634 else:
635 nav_items["range_pages"].append({"type": "page", "value": unicode(thispage), "number": thispage,
636 "attrs": self.link_attr, "href": self.url_maker(thispage)})
638 # Insert dots if there are pages between the displayed
639 # page numbers and the end of the page range
640 if self.last_page - rightmost_page > 1:
641 # Wrap in a SPAN tag if dotdot_attr is set
642 nav_items["range_pages"].append({"type": "span", "value": '..', "attrs": self.dotdot_attr, "href": "",
643 "number":None})
645 # Create a link to the very last page (unless we are on the last
646 # page or there would be no need to insert '..' spacers)
647 nav_items["last_page"] = {"type": "last_page", "value": unicode(symbol_last), "attrs": self.link_attr,
648 "href": self.url_maker(self.last_page), "number":self.last_page}
650 nav_items["previous_page"] = {"type": "previous_page", "value": unicode(symbol_previous),
651 "attrs": self.link_attr, "number": self.previous_page or self.first_page,
652 "href": self.url_maker(self.previous_page or self.first_page)}
654 nav_items["next_page"] = {"type": "next_page", "value": unicode(symbol_next),
655 "attrs": self.link_attr, "number": self.next_page or self.last_page,
656 "href": self.url_maker(self.next_page or self.last_page)}
658 return nav_items
661 def _range(self, link_map, radius):
662 """
663 Return range of linked pages to substiture placeholder in pattern
664 """
667 leftmost_page = max(self.first_page, (self.page-radius))
668 rightmost_page = min(self.last_page, (self.page+radius))
670 nav_items = []
671 # Create a link to the first page (unless we are on the first page
672 # or there would be no need to insert '..' spacers)
673 if self.page != self.first_page and self.first_page < leftmost_page:
674 page = link_map['first_page'].copy()
675 page['value'] = unicode(page['number'])
676 nav_items.append(self.link_tag(page))
678 for item in link_map['range_pages']:
679 nav_items.append(self.link_tag(item))
681 # Create a link to the very last page (unless we are on the last
682 # page or there would be no need to insert '..' spacers)
683 if self.page != self.last_page and rightmost_page < self.last_page:
684 page = link_map['last_page'].copy()
685 page['value'] = unicode(page['number'])
686 nav_items.append(self.link_tag(page))
688 return self.separator.join(nav_items)
690 def _default_url_maker(self, page_number):
691 if self.url is None:
692 raise Exception(
693 "You need to specify a 'url' parameter containing a '$page' placeholder.")
695 if "$page" not in self.url:
696 raise Exception("The 'url' parameter must contain a '$page' placeholder.")
698 return self.url.replace('$page', unicode(page_number))
700 @staticmethod
701 def default_link_tag(item):
702 """
703 Create an A-HREF tag that points to another page.
704 """
705 text = item['value']
706 target_url = item['href']
708 if not item['href'] or item['type'] in ('span', 'current_page'):
709 if item['attrs']:
710 text = make_html_tag('span', **item['attrs']) + text + '</span>'
711 return text
713 return make_html_tag('a', text=text, href=target_url, **item['attrs'])
716def make_html_tag(tag, text=None, **params):
717 """Create an HTML tag string.
719 tag
720 The HTML tag to use (e.g. 'a', 'span' or 'div')
722 text
723 The text to enclose between opening and closing tag. If no text is specified then only
724 the opening tag is returned.
726 Example::
727 make_html_tag('a', text="Hello", href="/another/page")
728 -> <a href="/another/page">Hello</a>
730 To use reserved Python keywords like "class" as a parameter prepend it with
731 an underscore. Instead of "class='green'" use "_class='green'".
733 Warning: Quotes and apostrophes are not escaped."""
734 params_string = ''
736 # Parameters are passed. Turn the dict into a string like "a=1 b=2 c=3" string.
737 for key, value in sorted(params.items()):
738 # Strip off a leading underscore from the attribute's key to allow attributes like '_class'
739 # to be used as a CSS class specification instead of the reserved Python keyword 'class'.
740 key = key.lstrip('_')
742 params_string += u' {0}="{1}"'.format(key, value)
744 # Create the tag string
745 tag_string = u'<{0}{1}>'.format(tag, params_string)
747 # Add text and closing tag if required.
748 if text:
749 tag_string += u'{0}</{1}>'.format(text, tag)
751 return tag_string