1# filelike/__init__.py
2#
3# Copyright (C) 2006-2009, Ryan Kelly
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the
17# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18# Boston, MA 02111-1307, USA.
19#
20"""
21
22    filelike: a python module for creating and handling file-like objects.
23
24This module takes care of the groundwork for implementing and manipulating
25objects that provide a rich file-like interface, including reading, writing,
26seeking and iteration.  It also provides a number of useful classes built on
27top of this functionality.
28
29The main class is FileLikeBase, which implements the entire file-like interface
30on top of primitive _read(), _write(), _seek(), _tell() and _truncate() methods.
31Subclasses may implement any or all of these methods to obtain the related
32higher-level file behaviors.
33
34It also provides some nifty file-handling functions:
35
36    :open:    mirrors the standard open() function but is much cleverer;
37              URLs are automatically fetched, .bz2 files are transparently
38              decompressed, and so-on.
39
40    :join:    concatenate multiple file-like objects together so that they
41              act like a single file.
42
43    :slice:   access a section of a file-like object as if it were an
44              independent file.
45
46
47The "wrappers" subpackage contains a collection of useful classes built on
48top of this framework.  These include:
49
50    :Translate:  pass file contents through an arbitrary translation
51                 function (e.g. compression, encryption, ...)
52
53    :Decrypt:    on-the-fly reading and writing to an encrypted file
54                 (using PEP272 cipher API)
55
56    :UnBZip2:    on-the-fly decompression of bzip'd files
57                 (like the standard library's bz2 module, but accepts
58                 any file-like object)
59
60As an example of the type of thing this module is designed to achieve, here's
61how the Decrypt wrapper can be used to transparently access an encrypted
62file::
63
64    # Create the decryption key
65    from Crypto.Cipher import DES
66    cipher = DES.new('abcdefgh',DES.MODE_ECB)
67    # Open the encrypted file
68    from filelike.wrappers import Decrypt
69    f = Decrypt(file("some_encrypted_file.bin","r"),cipher)
70
71The object in 'f' now behaves as a file-like object, transparently decrypting
72the file on-the-fly as it is read.
73
74
75The "pipeline" subpackage contains facilities for composing these wrappers
76in the form of a unix pipeline.  In the following example, 'f' will read the
77first five lines of an encrypted file::
78
79    from filelike.pipeline import Decrypt, Head
80    f = file("some_encrypted_file.bin") > Decrypt(cipher) | Head(lines=5)
81
82
83Finally, two utility functions are provided for when code expects to deal with
84file-like objects:
85
86    :is_filelike(obj):   checks that an object is file-like
87    :to_filelike(obj):   wraps a variety of objects in a file-like interface
88
89"""
90
91__ver_major__ = 0
92__ver_minor__ = 4
93__ver_patch__ = 1
94__ver_sub__ = ""
95__ver_tuple__ = (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__)
96__version__ = "%d.%d.%d%s" % __ver_tuple__
97
98
99from StringIO import StringIO
100import urllib2
101import urlparse
102import tempfile
103
104
105class NotReadableError(IOError):
106    pass
107class NotWritableError(IOError):
108    pass
109class NotSeekableError(IOError):
110    pass
111class NotTruncatableError(IOError):
112    pass
113
114
115class FileLikeBase(object):
116    """Base class for implementing file-like objects.
117
118    This class takes a lot of the legwork out of writing file-like objects
119    with a rich interface.  It implements the higher-level file-like
120    methods on top of five primitive methods: _read, _write, _seek, _tell and
121    _truncate. See their docstrings for precise details on how these methods
122    behave.
123
124    Subclasses then need only implement some subset of these methods for
125    rich file-like interface compatability.  They may of course override
126    other methods as desired.
127
128    The class is missing the following attributes and methods, which dont
129    really make sense for anything but real files:
130
131        * fileno()
132        * isatty()
133        * encoding
134        * mode
135        * name
136        * newlines
137
138    Unlike standard file objects, all read methods share the same buffer
139    and so can be freely mixed (e.g. read(), readline(), next(), ...).
140
141    This class understands and will accept the following mode strings,
142    with any additional characters being ignored:
143
144        * r    - open the file for reading only.
145        * r+   - open the file for reading and writing.
146        * r-   - open the file for streamed reading; do not allow seek/tell.
147        * w    - open the file for writing only; create the file if
148                 it doesn't exist; truncate it to zero length.
149        * w+   - open the file for reading and writing; create the file
150                 if it doesn't exist; truncate it to zero length.
151        * w-   - open the file for streamed writing; do not allow seek/tell.
152        * a    - open the file for writing only; create the file if it
153                 doesn't exist; place pointer at end of file.
154        * a+   - open the file for reading and writing; create the file
155                 if it doesn't exist; place pointer at end of file.
156
157    These are mostly standard except for the "-" indicator, which has
158    been added for efficiency purposes in cases where seeking can be
159    expensive to simulate (e.g. compressed files).  Note that any file
160    opened for both reading and writing must also support seeking.
161
162    """
163
164    def __init__(self,bufsize=1024*64):
165        """FileLikeBase Constructor.
166
167        The optional argument 'bufsize' specifies the number of bytes to
168        read at a time when looking for a newline character.  Setting this to
169        a larger number when lines are long should improve efficiency.
170        """
171        # File-like attributes
172        self.closed = False
173        self.softspace = 0
174        # Our own attributes
175        self._bufsize = bufsize  # buffer size for chunked reading
176        self._rbuffer = None     # data that's been read but not returned
177        self._wbuffer = None     # data that's been given but not written
178        self._sbuffer = None     # data between real & apparent file pos
179        self._soffset = 0        # internal offset of file pointer
180
181    def _check_mode(self,mode,mstr=None):
182        """Check whether the file may be accessed in the given mode.
183
184        'mode' must be one of "r" or "w", and this function returns False
185        if the file-like object has a 'mode' attribute, and it does not
186        permit access in that mode.  If there is no 'mode' attribute,
187        it defaults to "r+".
188
189        If seek support is not required, use "r-" or "w-" as the mode string.
190
191        To check a mode string other than self.mode, pass it in as the
192        second argument.
193        """
194        if mstr is None:
195            try:
196                mstr = self.mode
197            except AttributeError:
198                mstr = "r+"
199        if "+" in mstr:
200            return True
201        if "-" in mstr and "-" not in mode:
202            return False
203        if "r" in mode:
204            if "r" not in mstr:
205                return False
206        if "w" in mode:
207            if "w" not in mstr and "a" not in mstr:
208                return False
209        return True
210
211    def _assert_mode(self,mode,mstr=None):
212        """Check whether the file may be accessed in the given mode.
213
214        This method is equivalent to _check_assert(), but raises IOError
215        instead of returning False.
216        """
217        if mstr is None:
218            try:
219                mstr = self.mode
220            except AttributeError:
221                mstr = "r+"
222        if "+" in mstr:
223            return True
224        if "-" in mstr and "-" not in mode:
225            raise NotSeekableError("File does not support seeking.")
226        if "r" in mode:
227            if "r" not in mstr:
228                raise NotReadableError("File not opened for reading")
229        if "w" in mode:
230            if "w" not in mstr and "a" not in mstr:
231                raise NotWritableError("File not opened for writing")
232        return True
233
234    def flush(self):
235        """Flush internal write buffer, if necessary."""
236        if self.closed:
237            raise IOError("File has been closed")
238        if self._check_mode("w-") and self._wbuffer is not None:
239            buffered = ""
240            if self._sbuffer:
241                buffered = buffered + self._sbuffer
242                self._sbuffer = None
243            buffered = buffered + self._wbuffer
244            self._wbuffer = None
245            leftover = self._write(buffered,flushing=True)
246            if leftover:
247                raise IOError("Could not flush write buffer.")
248
249    def close(self):
250        """Flush write buffers and close the file.
251
252        The file may not be accessed further once it is closed.
253        """
254        #  Errors in subclass constructors can cause this to be called without
255        #  having called FileLikeBase.__init__().  Since we need the attrs it
256        #  initialises in cleanup, ensure we call it here.
257        if not hasattr(self,"closed"):
258            FileLikeBase.__init__(self)
259        if not self.closed:
260            self.flush()
261            self.closed = True
262
263    def __del__(self):
264        self.close()
265
266    def __enter__(self):
267        return self
268
269    def __exit__(self,exc_type,exc_val,exc_tb):
270        self.close()
271        return False
272
273    def next(self):
274        """next() method complying with the iterator protocol.
275
276        File-like objects are their own iterators, with each call to
277        next() returning subsequent lines from the file.
278        """
279        ln = self.readline()
280        if ln == "":
281            raise StopIteration()
282        return ln
283
284    def __iter__(self):
285        return self
286
287    def truncate(self,size=None):
288        """Truncate the file to the given size.
289
290        If <size> is not specified or is None, the current file position is
291        used.  Note that this method may fail at runtime if the underlying
292        filelike object is not truncatable.
293        """
294        if "-" in getattr(self,"mode",""):
295            raise NotTruncatableError("File is not seekable, can't truncate.")
296        if self._wbuffer:
297            self.flush()
298        if size is None:
299            size = self.tell()
300        self._truncate(size)
301
302    def seek(self,offset,whence=0):
303        """Move the internal file pointer to the given location."""
304        if whence > 2 or whence < 0:
305            raise ValueError("Invalid value for 'whence': " + str(whence))
306        if "-" in getattr(self,"mode",""):
307            raise NotSeekableError("File is not seekable.")
308        # Ensure that there's nothing left in the write buffer
309        if self._wbuffer:
310            self.flush()
311        # Adjust for any data left in the read buffer
312        if whence == 1 and self._rbuffer:
313            offset = offset - len(self._rbuffer)
314        self._rbuffer = None
315        # Adjust for any discrepancy in actual vs apparent seek position
316        if whence == 1:
317            if self._sbuffer:
318                offset = offset + len(self._sbuffer)
319            if self._soffset:
320                offset = offset + self._soffset
321        self._sbuffer = None
322        self._soffset = 0
323        # Shortcut the special case of staying put
324        if offset == 0 and whence == 1:
325            return
326        # Catch any failed attempts to read while simulating seek
327        try:
328            # Try to do a whence-wise seek if it is implemented.
329            sbuf = None
330            try:
331                sbuf = self._seek(offset,whence)
332            except NotImplementedError:
333                # Try to simulate using an absolute seek.
334                try:
335                    if whence == 1:
336                        offset = self._tell() + offset
337                    elif whence == 2:
338                        if hasattr(self,"size"):
339                            offset = self.size + offset
340                        else:
341                            self._do_read_rest()
342                            offset = self.tell() + offset
343                    else:
344                        # absolute seek already failed, don't try again
345                        raise NotImplementedError
346                    sbuf = self._seek(offset,0)
347                except NotImplementedError:
348                    # Simulate by reseting to start
349                    self._seek(0,0)
350                    self._soffset = offset
351            finally:
352                self._sbuffer = sbuf
353        except NotReadableError:
354            raise NotSeekableError("File not readable, can't simulate seek")
355
356    def tell(self):
357        """Determine current position of internal file pointer."""
358        # Need to adjust for unread/unwritten data in buffers
359        pos = self._tell()
360        if self._rbuffer:
361            pos = pos - len(self._rbuffer)
362        if self._wbuffer:
363            pos = pos + len(self._wbuffer)
364        if self._sbuffer:
365            pos = pos + len(self._sbuffer)
366        if self._soffset:
367            pos = pos + self._soffset
368        return pos
369
370    def read(self,size=-1):
371        """Read at most 'size' bytes from the file.
372
373        Bytes are returned as a string.  If 'size' is negative, zero or
374        missing, the remainder of the file is read.  If EOF is encountered
375        immediately, the empty string is returned.
376        """
377        if self.closed:
378            raise IOError("File has been closed")
379        self._assert_mode("r-")
380        return self._do_read(size)
381
382    def _do_read(self,size):
383        """Private method to read from the file.
384
385        This method behaves the same as self.read(), but skips some
386        permission and sanity checks.  It is intended for use in simulating
387        seek(), where we may want to read (and discard) information from
388        a file not opened in read mode.
389
390        Note that this may still fail if the file object actually can't
391        be read from - it just won't check whether the mode string gives
392        permission.
393        """
394        # If we were previously writing, ensure position is correct
395        if self._wbuffer is not None:
396            self.seek(0,1)
397        # Discard any data that should have been seeked over
398        if self._sbuffer:
399            s = len(self._sbuffer)
400            self._sbuffer = None
401            self.read(s)
402        elif self._soffset:
403            s = self._soffset
404            self._soffset = 0
405            while s > self._bufsize:
406                self._do_read(self._bufsize)
407                s -= self._bufsize
408            self._do_read(s)
409        # Should the entire file be read?
410        if size <= 0:
411            if self._rbuffer:
412                data = [self._rbuffer]
413            else:
414                data = []
415            self._rbuffer = ""
416            newData = self._read()
417            while newData is not None:
418                data.append(newData)
419                newData = self._read()
420            output = "".join(data)
421        # Otherwise, we need to return a specific amount of data
422        else:
423            if self._rbuffer:
424                newData = self._rbuffer
425                data = [newData]
426            else:
427                newData = ""
428                data = []
429            sizeSoFar = len(newData)
430            while sizeSoFar < size:
431                newData = self._read(size-sizeSoFar)
432                if newData is None:
433                    break
434                data.append(newData)
435                sizeSoFar += len(newData)
436            data = "".join(data)
437            if sizeSoFar > size:
438                # read too many bytes, store in the buffer
439                self._rbuffer = data[size:]
440                data = data[:size]
441            else:
442                self._rbuffer = ""
443            output = data
444        return output
445
446    def _do_read_rest(self):
447        """Private method to read the file through to EOF."""
448        data = self._do_read(self._bufsize)
449        while data != "":
450            data = self._do_read(self._bufsize)
451
452    def readline(self,size=-1):
453        """Read a line from the file, or at most <size> bytes."""
454        bits = []
455        indx = -1
456        sizeSoFar = 0
457        while indx == -1:
458            nextBit = self.read(self._bufsize)
459            bits.append(nextBit)
460            sizeSoFar += len(nextBit)
461            if nextBit == "":
462                break
463            if size > 0 and sizeSoFar >= size:
464                break
465            indx = nextBit.find("\n")
466        # If not found, return whole string up to <size> length
467        # Any leftovers are pushed onto front of buffer
468        if indx == -1:
469            data = "".join(bits)
470            if size > 0 and sizeSoFar > size:
471                extra = data[size:]
472                data = data[:size]
473                self._rbuffer = extra + self._rbuffer
474            return data
475        # If found, push leftovers onto front of buffer
476        # Add one to preserve the newline in the return value
477        indx += 1
478        extra = bits[-1][indx:]
479        bits[-1] = bits[-1][:indx]
480        self._rbuffer = extra + self._rbuffer
481        return "".join(bits)
482
483    def readlines(self,sizehint=-1):
484        """Return a list of all lines in the file."""
485        return [ln for ln in self]
486
487    def xreadlines(self):
488        """Iterator over lines in the file - equivalent to iter(self)."""
489        return iter(self)
490
491    def write(self,string):
492        """Write the given string to the file."""
493        if self.closed:
494            raise IOError("File has been closed")
495        self._assert_mode("w-")
496        # If we were previously reading, ensure position is correct
497        if self._rbuffer is not None:
498            self.seek(0,1)
499        # If we're actually behind the apparent position, we must also
500        # write the data in the gap.
501        if self._sbuffer:
502            string = self._sbuffer + string
503            self._sbuffer = None
504        elif self._soffset:
505            s = self._soffset
506            self._soffset = 0
507            try:
508                string = self._do_read(s) + string
509            except NotReadableError:
510                raise NotSeekableError("File not readable, could not complete simulation of seek")
511            self.seek(0,0)
512        if self._wbuffer:
513            string = self._wbuffer + string
514        leftover = self._write(string)
515        if leftover is None:
516            self._wbuffer = ""
517        else:
518            self._wbuffer = leftover
519
520    def writelines(self,seq):
521        """Write a sequence of lines to the file."""
522        for ln in seq:
523            self.write(ln)
524
525    def _read(self,sizehint=-1):
526        """Read approximately <sizehint> bytes from the file-like object.
527
528        This method is to be implemented by subclasses that wish to be
529        readable.  It should read approximately <sizehint> bytes from the
530        file and return them as a string.  If <sizehint> is missing or
531        less than or equal to zero, try to read all the remaining contents.
532
533        The method need not guarantee any particular number of bytes -
534        it may return more bytes than requested, or fewer.  If needed, the
535        size hint may be completely ignored.  It may even return an empty
536        string if no data is yet available.
537
538        Because of this, the method must return None to signify that EOF
539        has been reached.  The higher-level methods will never indicate EOF
540        until None has been read from _read().  Once EOF is reached, it
541        should be safe to call _read() again, immediately returning None.
542        """
543        raise NotReadableError("Object not readable")
544
545    def _write(self,string,flushing=False):
546        """Write the given string to the file-like object.
547
548        This method must be implemented by subclasses wishing to be writable.
549        It must attempt to write as much of the given data as possible to the
550        file, but need not guarantee that it is all written.  It may return
551        None to indicate that all data was written, or return as a string any
552        data that could not be written.
553
554        If the keyword argument 'flushing' is true, it indicates that the
555        internal write buffers are being flushed, and *all* the given data
556        is expected to be written to the file. If unwritten data is returned
557        when 'flushing' is true, an IOError will be raised.
558        """
559        raise NotWritableError("Object not writable")
560
561    def _seek(self,offset,whence):
562        """Set the file's internal position pointer, approximately.
563
564        This method should set the file's position to approximately 'offset'
565        bytes relative to the position specified by 'whence'.  If it is
566        not possible to position the pointer exactly at the given offset,
567        it should be positioned at a convenient *smaller* offset and the
568        file data between the real and apparent position should be returned.
569
570        At minimum, this method must implement the ability to seek to
571        the start of the file, i.e. offset=0 and whence=0.  If more
572        complex seeks are difficult to implement then it may raise
573        NotImplementedError to have them simulated (inefficiently) by
574        the higher-level machinery of this class.
575        """
576        raise NotSeekableError("Object not seekable")
577
578    def _tell(self):
579        """Get the location of the file's internal position pointer.
580
581        This method must be implemented by subclasses that wish to be
582        seekable, and must return the position of the file's internal
583        pointer.
584
585        Due to buffering, the position seen by users of this class
586        (the "apparent position") may be different to the position
587        returned by this method (the "actual position").
588        """
589        raise NotSeekableError("Object not seekable")
590
591    def _truncate(self,size):
592        """Truncate the file's size to <size>.
593
594        This method must be implemented by subclasses that wish to be
595        truncatable.  It must truncate the file to exactly the given size
596        or fail with an IOError.
597
598        Note that <size> will never be None; if it was not specified by the
599        user then it is calculated as the file's apparent position (which may
600        be different to its actual position due to buffering).
601        """
602        raise NotTruncatableError("Object not truncatable")
603
604
605class Opener(object):
606    """Class allowing clever opening of files.
607
608    Instances of this class are callable using inst(filename,mode),
609    and are intended as a 'smart' replacement for the standard file
610    constructor and open command.  Given a filename and a mode, it returns
611    a file-like object representing that file, according to rules such
612    as:
613
614        * URLs are opened using urllib2
615        * files with names ending in ".gz" are gunzipped on the fly
616        * etc...
617
618    The precise rules that are implemented are determined by two lists
619    of functions - openers and decoders.  First, each successive opener
620    function is called with the filename and mode until one returns non-None.
621    Theese functions must attempt to open the given filename and return it as
622    a filelike object.
623
624    Once the file has been opened, it is passed to each successive decoder
625    function.  These should return non-None if they perform some decoding
626    step on the file.  In this case, they must wrap and return the file-like
627    object, modifying its name if appropriate.
628    """
629
630    def __init__(self,openers=(),decoders=()):
631        self.openers = [o for o in openers]
632        self.decoders = [d for d in decoders]
633
634    def __call__(self,filename,mode="r"):
635        # Open the file
636        for o in self.openers:
637            try:
638                f = o(filename,mode)
639            except IOError,e:
640                f = None
641            if f is not None:
642                break
643        else:
644            raise IOError("Could not open file %s in mode '%s'" \
645                                                        %(filename,mode))
646        # Decode the file as many times as required
647        goAgain = True
648        while goAgain:
649            for d in self.decoders:
650                res = d(f)
651                if res is not None:
652                    f = res
653                    break
654            else:
655                goAgain = False
656        # Return the final file object
657        return f
658
659##  Create default Opener that uses urllib2.urlopen() and file() as openers
660def _urllib_opener(filename,mode):
661    if mode not in ("r","r-"):
662        return None
663    comps = urlparse.urlparse(filename)
664    # ensure it's a URL
665    if comps[0] == "":
666        return None
667    f = urllib2.urlopen(filename)
668    f.name = f.geturl()
669    f.mode = mode
670    return f
671def _file_opener(filename,mode):
672    # Dont open URLS as local files
673    comps = urlparse.urlparse(filename)
674    if comps[0] and comps[1]:
675        return None
676    return file(filename,mode)
677
678open = Opener(openers=(_urllib_opener,_file_opener))
679
680
681def is_filelike(obj,mode="rw"):
682    """Test whether an object implements the file-like interface.
683
684    'obj' must be the object to be tested, and 'mode' a file access
685    mode such as "r", "w" or "rw".  This function returns True if
686    the given object implements the full reading/writing interface
687    as required by the given mode, and False otherwise.
688
689    If 'mode' is not specified, it deaults to "rw" - that is,
690    checking that the full file interface is supported.
691
692    This method is not intended for checking basic functionality such as
693    existance of read(), but for ensuring the richer interface is
694    available.  If only read() or write() is needed, it's probably
695    simpler to (a) catch the AttributeError, or (b) use to_filelike(obj)
696    to ensure a suitable object.
697    """
698    # Check reading interface
699    if "r" in mode:
700        # Special-case for FileLikeBase subclasses
701        if isinstance(obj,FileLikeBase):
702            if not hasattr(obj,"_read"):
703                return False
704            if obj._read.im_class is FileLikeBase:
705                return False
706        else:
707            attrs = ("read","readline","readlines","__iter__",)
708            for a in attrs:
709                if not hasattr(obj,a):
710                    return False
711    # Check writing interface
712    if "w" in mode or "a" in mode:
713        # Special-case for FileLikeBase subclasses
714        if isinstance(obj,FileLikeBase):
715            if not hasattr(obj,"_write"):
716                return False
717            if obj._write.im_class is FileLikeBase:
718                return False
719        else:
720            attrs = ("write","writelines","close")
721            for a in attrs:
722                if not hasattr(obj,a):
723                    return False
724    # Check for seekability
725    if "-" not in mode:
726        if isinstance(obj,FileLikeBase):
727            if not hasattr(obj,"_seek"):
728                return False
729            if obj._seek.im_class is FileLikeBase:
730                return False
731        else:
732            attrs = ("seek","tell",)
733            for a in attrs:
734                if not hasattr(obj,a):
735                    return False
736    return True
737
738
739class join(FileLikeBase):
740    """Class concatenating several file-like objects into a single file.
741
742    This class is similar in spirit to the unix `cat` command, except that
743    it produces a file-like object that is readable, writable and seekable
744    (so long as the underlying files permit those operations, of course).
745
746    When reading, data is read from each file in turn until it has been
747    exhausted.  Seeks and tells are calculated using the individual positions
748    of each file.
749
750    When writing, data is spread across each file according to its size,
751    and only the last file in the sequence will grow as data is appended.
752    This requires that the size of each file can be determined, either by
753    checking for a 'size' attribute or using seek/tell.
754    """
755
756    def __init__(self,files,mode=None):
757        """Filelike join constructor.
758
759        This first argument must be a sequence of file-like objects
760        that are to be joined together.  The optional second argument
761        specifies the access mode and can be used e.g. to prevent
762        writing even when the underlying files are writable.
763        """
764        super(join,self).__init__()
765        if mode:
766            self.mode = mode
767        self._files = list(files)
768        self._curFile = 0
769        if mode and "a" in mode:
770            self.seek(0,2)
771
772    def close(self):
773        super(join,self).close()
774        for f in self._files:
775            if hasattr(f,"close"):
776                f.close()
777
778    def flush(self):
779        super(join,self).flush()
780        for f in self._files:
781            if hasattr(f,"flush"):
782                f.flush()
783
784    def _read(self,sizehint=-1):
785        data = self._files[self._curFile].read(sizehint)
786        if data == "":
787            if self._curFile == len(self._files) - 1:
788                return None
789            else:
790                self._curFile += 1
791                return self._read(sizehint)
792        else:
793            return data
794
795    def _write(self,data,flushing=False):
796        cf = self._files[self._curFile]
797        # If we're at the last file, just write it all out
798        if self._curFile == len(self._files) - 1:
799            cf.write(data)
800            return None
801        # Otherwise, we may need to write into multiple files
802        pos = cf.tell()
803        try:
804            size = cf.size
805        except AttributeError:
806            cf.seek(0,2)
807            size = cf.tell()
808            cf.seek(pos,0)
809        # If the data will all fit in the current file, just write it
810        gap = size - pos
811        if gap >= len(data):
812            cf.write(data)
813            return None
814        # Otherwise, split up the data and recurse
815        cf.write(data[:gap])
816        self._curFile += 1
817        return self._write(data[gap:],flushing=flushing)
818
819    def _seek(self,offset,whence):
820        # Seek-from-end simulated using seek-to-end, then relative seek.
821        if whence == 2:
822            for f in self._files[self._curFile:]:
823                f.seek(0,2)
824            self._curFile = len(self._files)-1
825            self._seek(offset,1)
826        # Absolute seek simulated using tell() and relative seek.
827        elif whence == 0:
828            offset = offset - self._tell()
829            self._seek(offset,1)
830        # Relative seek
831        elif whence == 1:
832            # Working backwards, we simply rewind each file until
833            # the offset is small enough to be within the current file
834            if offset < 0:
835                off1 = self._files[self._curFile].tell()
836                while off1 < -1*offset:
837                    offset += off1
838                    self._files[self._curFile].seek(0,0)
839                    # If seeking back past start of first file, stop at zero
840                    if self._curFile == 0:
841                        return None
842                    self._curFile -= 1
843                    off1 = self._files[self._curFile].tell()
844                self._files[self._curFile].seek(offset,1)
845            # Working forwards, we wind each file forward to its end,
846            # then seek backwards once we've gone too far.
847            elif offset > 0:
848                offset += self._files[self._curFile].tell()
849                self._files[self._curFile].seek(0,2)
850                offset -= self._files[self._curFile].tell()
851                while offset > 0:
852                    self._curFile += 1
853                    self._files[self._curFile].seek(0,2)
854                    offset -= self._files[self._curFile].tell()
855                self.seek(offset,1)
856
857    def _tell(self):
858        return sum([f.tell() for f in self._files[:self._curFile+1]])
859
860
861def slice(f,start=0,stop=None,mode=None,resizable=False):
862    """Manipulate a portion of a file-like object.
863
864    This function simply exposes the class filelike.wrappers.Slice
865    at the top-level of the module, since it has a nice symmetry
866    with the 'join' operation.
867    """
868    return filelike.wrappers.Slice(f,start,stop,mode,resizable)
869
870
871def to_filelike(obj,mode="r+"):
872    """Convert 'obj' to a file-like object if possible.
873
874    This method takes an arbitrary object 'obj', and attempts to
875    wrap it in a file-like interface.  This will results in the
876    object itself if it is already file-like, or some sort of
877    wrapper class otherwise.
878
879    'mode', if provided, should specify how the resulting object
880    will be accessed.
881
882    If the object cannot be converted, ValueError is raised.
883    """
884    # File-like objects are sutiable on their own
885    if is_filelike(obj,mode):
886        return obj
887    # Strings can be wrapped using StringIO
888    if isinstance(obj,basestring):
889        return StringIO(obj)
890    # Anything with read() and/or write() can be trivially wrapped
891    hasRead = hasattr(obj,"read")
892    hasWrite = hasattr(obj,"write")
893    hasSeek = hasattr(obj,"seek")
894    if "r" in mode:
895        if "w" in mode or "a" in mode or "+" in mode:
896            if hasRead and hasWrite and hasSeek:
897                return filelike.wrappers.FileWrapper(obj)
898        elif "-" not in mode:
899            if hasRead and hasSeek:
900                return filelike.wrappers.FileWrapper(obj)
901        else:
902            if hasRead:
903                return filelike.wrappers.FileWrapper(obj)
904    if "w" in mode or "a" in mode:
905        if "-" not in mode:
906            if hasWrite and hasSeek:
907                return filelike.wrappers.FileWrapper(obj)
908        elif hasWrite:
909            return filelike.wrappers.FileWrapper(obj)
910    # TODO: lots more could be done here...
911    raise ValueError("Could not make object file-like: %s", (obj,))
912
913# Imported here to aoid circular imports
914import filelike.wrappers
915