source: ftputil/file.py

Last change on this file was 1895:3c3ac50e0334, checked in by Stefan Schwarzer <sschwarzer@…>, 3 months ago
Make `rest` a keyword-only argument This applies to `FTPHost.open` and `FTPFile._open`. The latter is only used internally. Making `rest` a keyword-only argument has several reasons: - Python's built-in `open` has two additional arguments that aren't supported in `FTPHost.open`, i. e. they're not in `FTPHost.open`'s argument list. Using `rest` as a positional argument may create confusion about the role of this positional argument. (The position 7 has completely different semantics in Python's `open` and `FTPHost.open`.) - Adding other `open` arguments modeled after Python's `open` will be easier. These arguments can now get the corresponding positions as in Python's `open`. - The `rest` argument probably is rarely used, especially as a positional argument, so very few people would be affected.
File size: 6.6 KB
Line 
1# Copyright (C) 2003-2020, Stefan Schwarzer <sschwarzer@sschwarzer.net>
2# and ftputil contributors (see `doc/contributors.txt`)
3# See the file LICENSE for licensing terms.
4
5"""
6ftputil.file - support for file-like objects on FTP servers
7"""
8
9import io
10
11import ftputil.error
12
13
14# This module shouldn't be used by clients of the ftputil library.
15__all__ = []
16
17
18class FTPFile:
19    """
20    Represents a file-like object associated with an FTP host. File
21    and socket are closed appropriately if the `close` method is
22    called.
23    """
24
25    # Set timeout in seconds when closing file connections (see ticket #51).
26    _close_timeout = 5
27
28    def __init__(self, host):
29        """Construct the file(-like) object."""
30        self._host = host
31        # pylint: disable=protected-access
32        self._session = host._session
33        # The file is still closed.
34        self.closed = True
35        self._conn = None
36        self._fobj = None
37
38    def _open(
39        self,
40        path,
41        mode,
42        buffering=None,
43        encoding=None,
44        errors=None,
45        newline=None,
46        *,
47        rest=None,
48    ):
49        """
50        Open the remote file with given path name and mode.
51
52        Contrary to the `open` builtin, this method returns `None`,
53        instead this file object is modified in-place.
54        """
55        # We use the same arguments as in `open`.
56        # pylint: disable=unused-argument
57        # pylint: disable=too-many-arguments
58        #
59        # Check mode.
60        if mode is None:
61            # This is Python's behavior for local files.
62            raise TypeError("open() argument 2 must be str, not None")
63        if "a" in mode:
64            raise ftputil.error.FTPIOError("append mode not supported")
65        if mode not in ("r", "rb", "rt", "w", "wb", "wt"):
66            raise ftputil.error.FTPIOError("invalid mode '{}'".format(mode))
67        if "b" in mode and "t" in mode:
68            # Raise a `ValueError` like Python would.
69            raise ValueError("can't have text and binary mode at once")
70        # Convenience variables
71        is_binary_mode = "b" in mode
72        is_read_mode = "r" in mode
73        # `rest` is only allowed for binary mode.
74        if (not is_binary_mode) and (rest is not None):
75            raise ftputil.error.CommandNotImplementedError(
76                "`rest` argument can't be used for text files"
77            )
78        # Always use binary mode and leave any conversions to Python,
79        # controlled by the arguments to `makefile` below.
80        transfer_type = "I"
81        command = "TYPE {}".format(transfer_type)
82        with ftputil.error.ftplib_error_to_ftp_io_error:
83            self._session.voidcmd(command)
84        # Make transfer command.
85        command_type = "RETR" if is_read_mode else "STOR"
86        command = "{} {}".format(command_type, path)
87        # Get connection and file object.
88        with ftputil.error.ftplib_error_to_ftp_io_error:
89            self._conn = self._session.transfercmd(command, rest)
90        self._fobj = self._conn.makefile(
91            mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline
92        )
93        # This comes last so that `close` won't try to close `FTPFile`
94        # objects without `_conn` and `_fobj` attributes in case of an
95        # error.
96        self.closed = False
97
98    def __iter__(self):
99        """Return a file iterator."""
100        return self
101
102    def __next__(self):
103        """
104        Return the next line or raise `StopIteration`, if there are
105        no more.
106        """
107        # Apply implicit line ending conversion for text files.
108        line = self.readline()
109        if line:
110            return line
111        else:
112            raise StopIteration
113
114    #
115    # Context manager methods
116    #
117    def __enter__(self):
118        # Return `self`, so it can be accessed as the variable
119        # component of the `with` statement.
120        return self
121
122    def __exit__(self, exc_type, exc_val, exc_tb):
123        # We don't need the `exc_*` arguments here
124        # pylint: disable=unused-argument
125        self.close()
126        # Be explicit
127        return False
128
129    #
130    # Other attributes
131    #
132    def __getattr__(self, attr_name):
133        """
134        Handle requests for attributes unknown to `FTPFile` objects:
135        delegate the requests to the contained file object.
136        """
137        if attr_name in (
138            "encoding flush isatty fileno read readline "
139            "readlines seek tell truncate name softspace "
140            "write writelines".split()
141        ):
142            return getattr(self._fobj, attr_name)
143        raise AttributeError("'FTPFile' object has no attribute '{}'".format(attr_name))
144
145    # TODO: Implement `__dir__`? (See
146    # http://docs.python.org/whatsnew/2.6.html#other-language-changes )
147
148    def close(self):
149        """Close the `FTPFile`."""
150        if self.closed:
151            return
152        # Timeout value to restore, see below.
153        # Statement works only before the try/finally statement,
154        # otherwise Python raises an `UnboundLocalError`.
155        old_timeout = self._session.sock.gettimeout()
156        try:
157            self._fobj.close()
158            self._fobj = None
159            with ftputil.error.ftplib_error_to_ftp_io_error:
160                self._conn.close()
161            # Set a timeout to prevent waiting until server timeout
162            # if we have a server blocking here like in ticket #51.
163            self._session.sock.settimeout(self._close_timeout)
164            try:
165                with ftputil.error.ftplib_error_to_ftp_io_error:
166                    self._session.voidresp()
167            except ftputil.error.FTPIOError as exc:
168                # Ignore some errors, see tickets #51 and #17 at
169                # http://ftputil.sschwarzer.net/trac/ticket/51 and
170                # http://ftputil.sschwarzer.net/trac/ticket/17,
171                # respectively.
172                exc = str(exc)
173                error_code = exc[:3]
174                if exc.splitlines()[0] != "timed out" and error_code not in (
175                    "150",
176                    "426",
177                    "450",
178                    "451",
179                ):
180                    raise
181        finally:
182            # Restore timeout for socket of `FTPFile`'s `ftplib.FTP`
183            # object in case the connection is reused later.
184            self._session.sock.settimeout(old_timeout)
185            # If something went wrong before, the file is probably
186            # defunct and subsequent calls to `close` won't help
187            # either, so we consider the file closed for practical
188            # purposes.
189            self.closed = True
190
191    def __getstate__(self):
192        raise TypeError("cannot serialize FTPFile object")
Note: See TracBrowser for help on using the repository browser.