9d2fb6fdedde0b8921f64ae09ccf6c620fbe3a8f
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 # This module is based on OFS.Image originaly copyrighted as:
5 # Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved.
7 # This software is subject to the provisions of the Zope Public License,
8 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
9 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
10 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
11 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
12 # FOR A PARTICULAR PURPOSE
14 ##############################################################################
17 $Id: blobbases.py 949 2009-04-30 14:42:24Z pin $
18 $URL: http://svn.luxia.fr/svn/labo/projects/zope/Photo/trunk/blobbases.py $
22 from warnings
import warn
23 from zope
.contenttype
import guess_content_type
24 from Globals
import DTMLFile
25 from Globals
import InitializeClass
26 from OFS
.PropertyManager
import PropertyManager
27 from AccessControl
import ClassSecurityInfo
28 from AccessControl
.Role
import RoleManager
29 from AccessControl
.Permissions
import change_images_and_files
30 from AccessControl
.Permissions
import view_management_screens
31 from AccessControl
.Permissions
import view
as View
32 from AccessControl
.Permissions
import ftp_access
33 from AccessControl
.Permissions
import delete_objects
34 from webdav
.common
import rfc1123_date
35 from webdav
.Lockable
import ResourceLockedError
36 from webdav
.WriteLockInterface
import WriteLockInterface
37 from OFS
.SimpleItem
import Item_w__name__
38 from cStringIO
import StringIO
39 from Globals
import Persistent
40 from Acquisition
import Implicit
41 from DateTime
import DateTime
42 from OFS
.Cache
import Cacheable
43 from mimetools
import choose_boundary
44 from ZPublisher
import HTTPRangeSupport
45 from ZPublisher
.HTTPRequest
import FileUpload
46 from ZPublisher
.Iterators
import filestream_iterator
47 from zExceptions
import Redirect
48 from cgi
import escape
50 from ZODB
.blob
import Blob
54 manage_addFileForm
=DTMLFile('dtml/imageAdd', globals(),Kind
='File',kind
='file')
55 def manage_addFile(self
,id,file='',title
='',precondition
='', content_type
='',
57 """Add a new File object.
59 Creates a new File object 'id' with the contents of 'file'"""
63 content_type
=str(content_type
)
64 precondition
=str(precondition
)
66 id, title
= cookId(id, title
, file)
69 self
._setObject
(id, File(id,title
,file,content_type
, precondition
))
71 if REQUEST
is not None:
72 REQUEST
['RESPONSE'].redirect(self
.absolute_url()+'/manage_main')
75 class File(Persistent
, Implicit
, PropertyManager
,
76 RoleManager
, Item_w__name__
, Cacheable
):
77 """A File object is a content object for arbitrary files."""
79 __implements__
= (WriteLockInterface
, HTTPRangeSupport
.HTTPRangeInterface
)
82 security
= ClassSecurityInfo()
83 security
.declareObjectProtected(View
)
88 manage_editForm
=DTMLFile('dtml/fileEdit',globals(),
89 Kind
='File',kind
='file')
90 manage_editForm
._setName
('manage_editForm')
92 security
.declareProtected(view_management_screens
, 'manage')
93 security
.declareProtected(view_management_screens
, 'manage_main')
94 manage
=manage_main
=manage_editForm
95 manage_uploadForm
=manage_editForm
99 {'label':'Edit', 'action':'manage_main',
100 'help':('OFSP','File_Edit.stx')},
101 {'label':'View', 'action':'',
102 'help':('OFSP','File_View.stx')},
104 + PropertyManager
.manage_options
105 + RoleManager
.manage_options
106 + Item_w__name__
.manage_options
107 + Cacheable
.manage_options
110 _properties
=({'id':'title', 'type': 'string'},
111 {'id':'content_type', 'type':'string'},
114 def __init__(self
, id, title
, file, content_type
='', precondition
=''):
117 self
.precondition
=precondition
118 self
.uploaded_filename
= cookId('', '', file)[0]
121 content_type
=self
._get
_content
_type
(file, id, content_type
)
122 self
.update_data(file, content_type
)
124 security
.declarePrivate('save')
125 def save(self
, file):
126 bf
= self
.bdata
.open('w')
127 bf
.write(file.read())
128 self
.size
= bf
.tell()
131 security
.declarePrivate('open')
132 def open(self
, mode
='r'):
133 bf
= self
.bdata
.open(mode
)
136 security
.declarePrivate('updateSize')
137 def updateSize(self
, size
=None):
141 self
.size
= bf
.tell()
146 def _getLegacyData(self
) :
147 warn("Accessing 'data' attribute may be inefficient with "
148 "this blob based file. You should refactor your product "
149 "by accessing data like: "
150 "f = self.open('r') "
152 DeprecationWarning, stacklevel
=2)
158 def _setLegacyData(self
, data
) :
159 warn("Accessing 'data' attribute may be inefficient with "
160 "this blob based file. You should refactor your product "
161 "by accessing data like: "
162 "f = self.save(data)",
163 DeprecationWarning, stacklevel
=2)
164 if isinstance(data
, str) :
171 data
= property(_getLegacyData
, _setLegacyData
,
172 "Data Legacy attribute to ensure compatibility "
173 "with derived classes that access data by this way.")
178 def _if_modified_since_request_handler(self
, REQUEST
, RESPONSE
):
179 # HTTP If-Modified-Since header handling: return True if
180 # we can handle this request by returning a 304 response
181 header
=REQUEST
.get_header('If-Modified-Since', None)
182 if header
is not None:
183 header
=header
.split( ';')[0]
184 # Some proxies seem to send invalid date strings for this
185 # header. If the date string is not valid, we ignore it
186 # rather than raise an error to be generally consistent
187 # with common servers such as Apache (which can usually
188 # understand the screwy date string as a lucky side effect
189 # of the way they parse it).
190 # This happens to be what RFC2616 tells us to do in the face of an
192 try: mod_since
=long(DateTime(header
).timeTime())
193 except: mod_since
=None
194 if mod_since
is not None:
196 last_mod
= long(self
._p
_mtime
)
199 if last_mod
> 0 and last_mod
<= mod_since
:
200 RESPONSE
.setHeader('Last-Modified',
201 rfc1123_date(self
._p
_mtime
))
202 RESPONSE
.setHeader('Content-Type', self
.content_type
)
203 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
204 RESPONSE
.setStatus(304)
207 def _range_request_handler(self
, REQUEST
, RESPONSE
):
208 # HTTP Range header handling: return True if we've served a range
209 # chunk out of our data.
210 range = REQUEST
.get_header('Range', None)
211 request_range
= REQUEST
.get_header('Request-Range', None)
212 if request_range
is not None:
213 # Netscape 2 through 4 and MSIE 3 implement a draft version
214 # Later on, we need to serve a different mime-type as well.
215 range = request_range
216 if_range
= REQUEST
.get_header('If-Range', None)
217 if range is not None:
218 ranges
= HTTPRangeSupport
.parseRange(range)
220 if if_range
is not None:
221 # Only send ranges if the data isn't modified, otherwise send
222 # the whole object. Support both ETags and Last-Modified dates!
223 if len(if_range
) > 1 and if_range
[:2] == 'ts':
225 if if_range
!= self
.http__etag():
226 # Modified, so send a normal response. We delete
227 # the ranges, which causes us to skip to the 200
232 date
= if_range
.split( ';')[0]
233 try: mod_since
=long(DateTime(date
).timeTime())
234 except: mod_since
=None
235 if mod_since
is not None:
237 last_mod
= long(self
._p
_mtime
)
240 if last_mod
> mod_since
:
241 # Modified, so send a normal response. We delete
242 # the ranges, which causes us to skip to the 200
247 # Search for satisfiable ranges.
249 for start
, end
in ranges
:
250 if start
< self
.size
:
255 RESPONSE
.setHeader('Content-Range',
256 'bytes */%d' % self
.size
)
257 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
258 RESPONSE
.setHeader('Last-Modified',
259 rfc1123_date(self
._p
_mtime
))
260 RESPONSE
.setHeader('Content-Type', self
.content_type
)
261 RESPONSE
.setHeader('Content-Length', self
.size
)
262 RESPONSE
.setStatus(416)
265 ranges
= HTTPRangeSupport
.expandRanges(ranges
, self
.size
)
268 # Easy case, set extra header and return partial set.
269 start
, end
= ranges
[0]
272 RESPONSE
.setHeader('Last-Modified',
273 rfc1123_date(self
._p
_mtime
))
274 RESPONSE
.setHeader('Content-Type', self
.content_type
)
275 RESPONSE
.setHeader('Content-Length', size
)
276 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
277 RESPONSE
.setHeader('Content-Range',
278 'bytes %d-%d/%d' % (start
, end
- 1, self
.size
))
279 RESPONSE
.setStatus(206) # Partial content
283 RESPONSE
.write(bf
.read(size
))
288 boundary
= choose_boundary()
290 # Calculate the content length
291 size
= (8 + len(boundary
) + # End marker length
292 len(ranges
) * ( # Constant lenght per set
293 49 + len(boundary
) + len(self
.content_type
) +
294 len('%d' % self
.size
)))
295 for start
, end
in ranges
:
296 # Variable length per set
297 size
= (size
+ len('%d%d' % (start
, end
- 1)) +
301 # Some clients implement an earlier draft of the spec, they
302 # will only accept x-byteranges.
303 draftprefix
= (request_range
is not None) and 'x-' or ''
305 RESPONSE
.setHeader('Content-Length', size
)
306 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
307 RESPONSE
.setHeader('Last-Modified',
308 rfc1123_date(self
._p
_mtime
))
309 RESPONSE
.setHeader('Content-Type',
310 'multipart/%sbyteranges; boundary=%s' % (
311 draftprefix
, boundary
))
312 RESPONSE
.setStatus(206) # Partial content
316 for start
, end
in ranges
:
317 RESPONSE
.write('\r\n--%s\r\n' % boundary
)
318 RESPONSE
.write('Content-Type: %s\r\n' %
321 'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
322 start
, end
- 1, self
.size
))
327 RESPONSE
.write(bf
.read(size
))
331 RESPONSE
.write('\r\n--%s--\r\n' % boundary
)
334 security
.declareProtected(View
, 'index_html')
335 def index_html(self
, REQUEST
, RESPONSE
):
337 The default view of the contents of a File or Image.
339 Returns the contents of the file or image. Also, sets the
340 Content-Type HTTP header to the objects content type.
343 if self
._if
_modified
_since
_request
_handler
(REQUEST
, RESPONSE
):
344 # we were able to handle this by returning a 304
345 # unfortunately, because the HTTP cache manager uses the cache
346 # API, and because 304 responses are required to carry the Expires
347 # header for HTTP/1.1, we need to call ZCacheable_set here.
348 # This is nonsensical for caches other than the HTTP cache manager
350 self
.ZCacheable_set(None)
353 if self
.precondition
and hasattr(self
, str(self
.precondition
)):
354 # Grab whatever precondition was defined and then
355 # execute it. The precondition will raise an exception
356 # if something violates its terms.
357 c
=getattr(self
, str(self
.precondition
))
358 if hasattr(c
,'isDocTemp') and c
.isDocTemp
:
359 c(REQUEST
['PARENTS'][1],REQUEST
)
363 if self
._range
_request
_handler
(REQUEST
, RESPONSE
):
364 # we served a chunk of content in response to a range request.
367 RESPONSE
.setHeader('Last-Modified', rfc1123_date(self
._p
_mtime
))
368 RESPONSE
.setHeader('Content-Type', self
.content_type
)
369 RESPONSE
.setHeader('Content-Length', self
.size
)
370 RESPONSE
.setHeader('Accept-Ranges', 'bytes')
372 if self
.ZCacheable_isCachingEnabled():
373 result
= self
.ZCacheable_get(default
=None)
374 if result
is not None:
375 # We will always get None from RAMCacheManager and HTTP
376 # Accelerated Cache Manager but we will get
377 # something implementing the IStreamIterator interface
378 # from a "FileCacheManager"
381 self
.ZCacheable_set(None)
384 chunk
= bf
.read(CHUNK_SIZE
)
386 RESPONSE
.write(chunk
)
387 chunk
= bf
.read(CHUNK_SIZE
)
391 security
.declareProtected(View
, 'view_image_or_file')
392 def view_image_or_file(self
, URL1
):
394 The default view of the contents of the File or Image.
398 security
.declareProtected(View
, 'PrincipiaSearchSource')
399 def PrincipiaSearchSource(self
):
400 """ Allow file objects to be searched.
402 if self
.content_type
.startswith('text/'):
409 security
.declarePrivate('update_data')
410 def update_data(self
, file, content_type
=None):
411 if isinstance(file, unicode):
412 raise TypeError('Data can only be str or file-like. '
413 'Unicode objects are expressly forbidden.')
414 elif isinstance(file, str) :
420 if content_type
is not None: self
.content_type
=content_type
422 self
.ZCacheable_invalidate()
423 self
.ZCacheable_set(None)
424 self
.http__refreshEtag()
426 security
.declareProtected(change_images_and_files
, 'manage_edit')
427 def manage_edit(self
, title
, content_type
, precondition
='',
428 filedata
=None, REQUEST
=None):
430 Changes the title and content type attributes of the File or Image.
432 if self
.wl_isLocked():
433 raise ResourceLockedError
, "File is locked via WebDAV"
435 self
.title
=str(title
)
436 self
.content_type
=str(content_type
)
437 if precondition
: self
.precondition
=str(precondition
)
438 elif self
.precondition
: del self
.precondition
439 if filedata
is not None:
440 self
.update_data(filedata
, content_type
)
442 self
.ZCacheable_invalidate()
444 message
="Saved changes."
445 return self
.manage_main(self
,REQUEST
,manage_tabs_message
=message
)
447 security
.declareProtected(change_images_and_files
, 'manage_upload')
448 def manage_upload(self
,file='',REQUEST
=None):
450 Replaces the current contents of the File or Image object with file.
452 The file or images contents are replaced with the contents of 'file'.
454 if self
.wl_isLocked():
455 raise ResourceLockedError
, "File is locked via WebDAV"
457 content_type
=self
._get
_content
_type
(file, self
.__name
__,
458 'application/octet-stream')
459 self
.update_data(file, content_type
)
462 message
="Saved changes."
463 return self
.manage_main(self
,REQUEST
,manage_tabs_message
=message
)
465 def _get_content_type(self
, file, id, content_type
=None):
466 headers
=getattr(file, 'headers', None)
467 if headers
and headers
.has_key('content-type'):
468 content_type
=headers
['content-type']
470 name
= getattr(file, 'filename', self
.uploaded_filename
) or id
471 content_type
, enc
=guess_content_type(name
, '', content_type
)
474 security
.declareProtected(delete_objects
, 'DELETE')
476 security
.declareProtected(change_images_and_files
, 'PUT')
477 def PUT(self
, REQUEST
, RESPONSE
):
478 """Handle HTTP PUT requests"""
479 self
.dav__init(REQUEST
, RESPONSE
)
480 self
.dav__simpleifhandler(REQUEST
, RESPONSE
, refresh
=1)
481 type=REQUEST
.get_header('content-type', None)
483 file=REQUEST
['BODYFILE']
485 content_type
= self
._get
_content
_type
(file, self
.__name
__,
486 type or self
.content_type
)
487 self
.update_data(file, content_type
)
489 RESPONSE
.setStatus(204)
492 security
.declareProtected(View
, 'get_size')
494 """Get the size of a file or image.
496 Returns the size of the file or image.
502 self
.size
= size
= bf
.tell()
506 # deprecated; use get_size!
509 security
.declareProtected(View
, 'getContentType')
510 def getContentType(self
):
511 """Get the content type of a file or image.
513 Returns the content type (MIME type) of a file or image.
515 return self
.content_type
518 def __str__(self
): return str(self
.data
)
519 def __len__(self
): return 1
521 security
.declareProtected(ftp_access
, 'manage_FTPstat')
522 security
.declareProtected(ftp_access
, 'manage_FTPlist')
524 security
.declareProtected(ftp_access
, 'manage_FTPget')
525 def manage_FTPget(self
):
526 """Return body for ftp."""
527 RESPONSE
= self
.REQUEST
.RESPONSE
529 if self
.ZCacheable_isCachingEnabled():
530 result
= self
.ZCacheable_get(default
=None)
531 if result
is not None:
532 # We will always get None from RAMCacheManager but we will get
533 # something implementing the IStreamIterator interface
534 # from FileCacheManager.
535 # the content-length is required here by HTTPResponse, even
536 # though FTP doesn't use it.
537 RESPONSE
.setHeader('Content-Length', self
.size
)
543 RESPONSE
.setBase(None)
546 manage_addImageForm
=DTMLFile('dtml/imageAdd',globals(),
547 Kind
='Image',kind
='image')
548 def manage_addImage(self
, id, file, title
='', precondition
='', content_type
='',
551 Add a new Image object.
553 Creates a new Image object 'id' with the contents of 'file'.
558 content_type
=str(content_type
)
559 precondition
=str(precondition
)
561 id, title
= cookId(id, title
, file)
564 self
._setObject
(id, Image(id,title
,file,content_type
, precondition
))
566 if REQUEST
is not None:
567 try: url
=self
.DestinationURL()
568 except: url
=REQUEST
['URL1']
569 REQUEST
.RESPONSE
.redirect('%s/manage_main' % url
)
573 def getImageInfo(file):
581 if (size
>= 10) and data
[:6] in ('GIF87a', 'GIF89a'):
582 # Check to see if content_type is correct
583 content_type
= 'image/gif'
584 w
, h
= struct
.unpack("<HH", data
[6:10])
588 # See PNG v1.2 spec (http://www.cdrom.com/pub/png/spec/)
589 # Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
590 # and finally the 4-byte width, height
591 elif ((size
>= 24) and (data
[:8] == '\211PNG\r\n\032\n')
592 and (data
[12:16] == 'IHDR')):
593 content_type
= 'image/png'
594 w
, h
= struct
.unpack(">LL", data
[16:24])
598 # Maybe this is for an older PNG version.
599 elif (size
>= 16) and (data
[:8] == '\211PNG\r\n\032\n'):
600 # Check to see if we have the right content type
601 content_type
= 'image/png'
602 w
, h
= struct
.unpack(">LL", data
[8:16])
607 elif (size
>= 2) and (data
[:2] == '\377\330'):
608 content_type
= 'image/jpeg'
614 while (b
and ord(b
) != 0xDA):
615 while (ord(b
) != 0xFF): b
= jpeg
.read(1)
616 while (ord(b
) == 0xFF): b
= jpeg
.read(1)
617 if (ord(b
) >= 0xC0 and ord(b
) <= 0xC3):
619 h
, w
= struct
.unpack(">HH", jpeg
.read(4))
622 jpeg
.read(int(struct
.unpack(">H", jpeg
.read(2))[0])-2)
628 return content_type
, width
, height
632 """Image objects can be GIF, PNG or JPEG and have the same methods
633 as File objects. Images also have a string representation that
634 renders an HTML 'IMG' tag.
636 __implements__
= (WriteLockInterface
,)
637 meta_type
='Blob Image'
639 security
= ClassSecurityInfo()
640 security
.declareObjectProtected(View
)
646 # FIXME: Redundant, already in base class
647 security
.declareProtected(change_images_and_files
, 'manage_edit')
648 security
.declareProtected(change_images_and_files
, 'manage_upload')
649 security
.declareProtected(change_images_and_files
, 'PUT')
650 security
.declareProtected(View
, 'index_html')
651 security
.declareProtected(View
, 'get_size')
652 security
.declareProtected(View
, 'getContentType')
653 security
.declareProtected(ftp_access
, 'manage_FTPstat')
654 security
.declareProtected(ftp_access
, 'manage_FTPlist')
655 security
.declareProtected(ftp_access
, 'manage_FTPget')
656 security
.declareProtected(delete_objects
, 'DELETE')
658 _properties
=({'id':'title', 'type': 'string'},
659 {'id':'alt', 'type':'string'},
660 {'id':'content_type', 'type':'string','mode':'w'},
661 {'id':'height', 'type':'string'},
662 {'id':'width', 'type':'string'},
666 ({'label':'Edit', 'action':'manage_main',
667 'help':('OFSP','Image_Edit.stx')},
668 {'label':'View', 'action':'view_image_or_file',
669 'help':('OFSP','Image_View.stx')},)
670 + PropertyManager
.manage_options
671 + RoleManager
.manage_options
672 + Item_w__name__
.manage_options
673 + Cacheable
.manage_options
676 manage_editForm
=DTMLFile('dtml/imageEdit',globals(),
677 Kind
='Image',kind
='image')
678 manage_editForm
._setName
('manage_editForm')
680 security
.declareProtected(View
, 'view_image_or_file')
681 view_image_or_file
=DTMLFile('dtml/imageView',globals())
683 security
.declareProtected(view_management_screens
, 'manage')
684 security
.declareProtected(view_management_screens
, 'manage_main')
685 manage
=manage_main
=manage_editForm
686 manage_uploadForm
=manage_editForm
688 security
.declarePrivate('update_data')
689 def update_data(self
, file, content_type
=None):
690 super(Image
, self
).update_data(file, content_type
)
691 self
.updateFormat(size
=self
.size
, content_type
=content_type
)
693 security
.declarePrivate('updateFormat')
694 def updateFormat(self
, size
=None, dimensions
=None, content_type
=None):
695 self
.updateSize(size
=size
)
697 if dimensions
is None or content_type
is None :
699 ct
, width
, height
= getImageInfo(bf
)
703 if width
>= 0 and height
>= 0:
707 # Now we should have the correct content type, or still None
708 if content_type
is not None: self
.content_type
= content_type
710 self
.width
, self
.height
= dimensions
711 self
.content_type
= content_type
716 security
.declareProtected(View
, 'tag')
717 def tag(self
, height
=None, width
=None, alt
=None,
718 scale
=0, xscale
=0, yscale
=0, css_class
=None, title
=None, **args
):
720 Generate an HTML IMG tag for this image, with customization.
721 Arguments to self.tag() can be any valid attributes of an IMG tag.
722 'src' will always be an absolute pathname, to prevent redundant
723 downloading of images. Defaults are applied intelligently for
724 'height', 'width', and 'alt'. If specified, the 'scale', 'xscale',
725 and 'yscale' keyword arguments will be used to automatically adjust
726 the output height and width values of the image tag.
728 Since 'class' is a Python reserved word, it cannot be passed in
729 directly in keyword arguments which is a problem if you are
730 trying to use 'tag()' to include a CSS class. The tag() method
731 will accept a 'css_class' argument that will be converted to
732 'class' in the output tag to work around this.
734 if height
is None: height
=self
.height
735 if width
is None: width
=self
.width
737 # Auto-scaling support
738 xdelta
= xscale
or scale
739 ydelta
= yscale
or scale
742 width
= str(int(round(int(width
) * xdelta
)))
743 if ydelta
and height
:
744 height
= str(int(round(int(height
) * ydelta
)))
746 result
='<img src="%s"' % (self
.absolute_url())
749 alt
=getattr(self
, 'alt', '')
750 result
= '%s alt="%s"' % (result
, escape(alt
, 1))
753 title
=getattr(self
, 'title', '')
754 result
= '%s title="%s"' % (result
, escape(title
, 1))
757 result
= '%s height="%s"' % (result
, height
)
760 result
= '%s width="%s"' % (result
, width
)
762 # Omitting 'border' attribute (Collector #1557)
763 # if not 'border' in [ x.lower() for x in args.keys()]:
764 # result = '%s border="0"' % result
766 if css_class
is not None:
767 result
= '%s class="%s"' % (result
, css_class
)
769 for key
in args
.keys():
770 value
= args
.get(key
)
772 result
= '%s %s="%s"' % (result
, key
, value
)
774 return '%s />' % result
777 def cookId(id, title
, file):
778 if not id and hasattr(file,'filename'):
779 filename
=file.filename
780 title
=title
or filename
781 id=filename
[max(filename
.rfind('/'),
782 filename
.rfind('\\'),
787 #class Pdata(Persistent, Implicit):
788 # # Wrapper for possibly large data
792 # def __init__(self, data):
795 # def __getslice__(self, i, j):
796 # return self.data[i:j]
804 # if next is None: return self.data
807 # while next is not None:
809 # r.append(self.data)