Ignore:
Timestamp:
Aug 3, 2013, 9:25:34 PM (6 years ago)
Author:
Stefan Schwarzer <sschwarzer@…>
Branch:
default
Message:
First go at revised I/O. `test_file.py` succeeds for Python 2 and 3.

On the other hand, currently lots of tests fail, including for
Python 2. These tests still expect the old I/O semantics.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • ftputil/file.py

    r1228 r1318  
    88
    99from __future__ import unicode_literals
     10
     11import io
    1012
    1113import ftputil.compat
     
    4648    """
    4749    Represents a file-like object associated with an FTP host. File
    48     and socket are closed appropriately if the `close` operation is
     50    and socket are closed appropriately if the `close` method is
    4951    called.
    5052    """
     
    6769        self._fo = None
    6870
    69     def _open(self, path, mode):
    70         """Open the remote file with given path name and mode."""
     71    def _open(self, path, mode, buffering=None, encoding=None, errors=None,
     72              newline=None):
     73        """
     74        Open the remote file with given path name and mode.
     75
     76        Contrary to the `open` builtin, this method returns `None`,
     77        instead this file object is modified in-place.
     78        """
     79        # Python 3.x's `socket.makefile` supports the same interface
     80        # as the new `open` builtin, but Python 2.x supports a mode,
     81        # but neither buffering nor encoding/decoding. Therefore, to
     82        # make the code work on Python 2.x and 3.x, create an
     83        # unbuffered binary file and wrap it.
     84        #
    7185        # Check mode.
    72         if 'a' in mode:
     86        if "a" in mode:
    7387            raise ftputil.error.FTPIOError("append mode not supported")
    74         if mode not in ('r', 'rb', 'w', 'wb'):
     88        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
    7589            raise ftputil.error.FTPIOError("invalid mode '{0}'".format(mode))
     90        if "b" in mode and "t" in mode:
     91            # Raise a `ValueError` like Python would.
     92            raise ValueError("can't have text and binary mode at once")
    7693        # Remember convenience variables instead of the mode itself.
    7794        self._bin_mode = 'b' in mode
    7895        self._read_mode = 'r' in mode
    79         # Select ASCII or binary mode.
    80         transfer_type = ('A', 'I')[self._bin_mode]
     96        # Always use binary mode (see above).
     97        transfer_type = "I"
    8198        command = 'TYPE {0}'.format(transfer_type)
    8299        with ftputil.error.ftplib_error_to_ftp_io_error:
     
    85102        command_type = ('STOR', 'RETR')[self._read_mode]
    86103        command = '{0} {1}'.format(command_type, path)
    87         # Ensure we can process the raw line separators.
    88         # Force to binary regardless of transfer type.
    89         if not 'b' in mode:
    90             mode = mode + 'b'
     104        # Force to binary regardless of transfer type (see above).
     105        makefile_mode = mode
     106        if "t" in mode:
     107            makefile_mode = makefile_mode.replace("t", "")
     108        if not "b" in makefile_mode:
     109            makefile_mode += "b"
    91110        # Get connection and file object.
    92111        with ftputil.error.ftplib_error_to_ftp_io_error:
    93112            self._conn = self._session.transfercmd(command)
    94         self._fo = self._conn.makefile(mode)
     113        # The actual file object.
     114        self._fo = self._conn.makefile(makefile_mode)
     115        if self._read_mode:
     116            self._fo = io.BufferedReader(self._fo)
     117        else:
     118            self._fo = io.BufferedWriter(self._fo)
     119        if not self._bin_mode:
     120            self._fo = io.TextIOWrapper(self._fo, encoding=encoding,
     121                                        errors=errors, newline=newline)
    95122        # This comes last so that `close` won't try to close `_FTPFile`
    96123        # objects without `_conn` and `_fo` attributes in case of an error.
    97124        self.closed = False
    98 
    99     #
    100     # Read and write operations with support for line separator
    101     # conversion for text modes.
    102     #
    103     # Note that we must convert line endings because the FTP server
    104     # expects `\r\n` to be sent on text transfers.
    105     #
    106     def read(self, *args):
    107         """Return read bytes, normalized if in text transfer mode."""
    108         data = self._fo.read(*args)
    109         if self._bin_mode:
    110             return data
    111         data = _crlf_to_python_linesep(data)
    112         if args == ():
    113             return data
    114         # If the read data contains `\r` characters the number of read
    115         # characters will be too small! Thus we (would) have to
    116         # continue to read until we have fetched the requested number
    117         # of bytes (or run out of source data).
    118         #
    119         # The algorithm below avoids repetitive string concatanations
    120         # in the style of
    121         #     data = data + more_data
    122         # and so should also work relatively well if there are many
    123         # short lines in the file.
    124         wanted_size = args[0]
    125         chunks = [data]
    126         current_size = len(data)
    127         while current_size < wanted_size:
    128             # print 'not enough bytes (now %s, wanting %s)' % \
    129             #       (current_size, wanted_size)
    130             more_data = self._fo.read(wanted_size - current_size)
    131             if not more_data:
    132                 break
    133             more_data = _crlf_to_python_linesep(more_data)
    134             # print '-> new (normalized) data:', repr(more_data)
    135             chunks.append(more_data)
    136             current_size += len(more_data)
    137         return ''.join(chunks)
    138 
    139     def readline(self, *args):
    140         """Return one read line, normalized if in text transfer mode."""
    141         data = self._fo.readline(*args)
    142         if self._bin_mode:
    143             return data
    144         # If necessary, complete begun newline.
    145         if data.endswith('\r'):
    146             data = data + self.read(1)
    147         return _crlf_to_python_linesep(data)
    148 
    149     def readlines(self, *args):
    150         """Return read lines, normalized if in text transfer mode."""
    151         lines = self._fo.readlines(*args)
    152         if self._bin_mode:
    153             return lines
    154         # More memory-friendly than `return [... for line in lines]`
    155         for index, line in enumerate(lines):
    156             lines[index] = _crlf_to_python_linesep(line)
    157         return lines
    158125
    159126    def __iter__(self):
     
    177144    next = __next__
    178145
    179     def write(self, data):
    180         """Write data to file. Do linesep conversion for text mode."""
    181         if not self._bin_mode:
    182             data = _python_to_crlf_linesep(data)
    183             if ftputil.compat.python_version == 3:
    184                 # For Python 3, always require that the data for text
    185                 # mode writes is a unicode string.
    186                 data = data.encode(self._encoding)
    187             else:
    188                 # For Python 2, also accept byte strings for
    189                 # compatibility with `open` semantics. Only encode
    190                 # unicode strings.
    191                 data = ftputil.tool.encode_if_unicode(data, self._encoding)
    192         self._fo.write(data)
    193 
    194     def writelines(self, lines):
    195         """Write lines to file. Do linesep conversion for text mode."""
    196         if self._bin_mode:
    197             self._fo.writelines(lines)
    198             return
    199         # We can't modify the list of lines in-place, as in the
    200         # `readlines` method. That would modify the original list,
    201         # given as argument `lines`.
    202         for line in lines:
    203             self.write(line)
    204 
    205146    #
    206147    # Context manager methods
     
    226167        delegate the requests to the contained file object.
    227168        """
    228         if attr_name in ('flush isatty fileno seek tell '
    229                          'truncate name softspace'.split()):
     169        if attr_name in ("flush isatty fileno read readlines seek tell "
     170                         "truncate name softspace write writelines".split()):
    230171            return getattr(self._fo, attr_name)
    231172        raise AttributeError(
Note: See TracChangeset for help on using the changeset viewer.