Changeset 1935:38acf28f3905


Ignore:
Timestamp:
May 24, 2020, 9:10:32 PM (15 months ago)
Author:
Stefan Schwarzer <sschwarzer@…>
Branch:
default
histedit_source:
77121e02df6ea8f8f7a668ed9db705457274eb18
Message:
Format docstrings and comments

Reformat docstrings and comments to 80 characters. Traditionally, the
margin for the code was at 79 columns, so I chose a bit less for the
comments.

With the switch to Black for formatting the right margin is now at
nearly 90 columns, so it makes sense to also allow a greater right
margin for docstrings and comments. Lines still don't get too long
because the formatted text often starts only in column 5 or 9.
Files:
25 edited

Legend:

Unmodified
Added
Removed
  • ftputil/error.py

    r1901 r1935  
    1515
    1616
    17 # You _can_ import these with `from ftputil.error import *`, - but
    18 # it's _not_ recommended.
     17# You _can_ import these with `from ftputil.error import *`, - but it's _not_
     18# recommended.
    1919__all__ = [
    2020    "InternalError",
     
    3434
    3535class FTPError(Exception):
    36     """General ftputil error class."""
    37 
    38     # In Python 2, we can't use a keyword argument after `*args`, so
    39     # `pop` from `**kwargs`.
     36    """
     37    General ftputil error class.
     38    """
     39
     40    # In Python 2, we can't use a keyword argument after `*args`, so `pop` from
     41    # `**kwargs`.
    4042    def __init__(self, *args, **kwargs):
    4143        super().__init__(*args)
     
    4345            self.strerror = str(kwargs.pop("original_exception"))
    4446        elif args:
    45             # If there was no `original_exception` argument, assume
    46             # the first argument is a string. It may be a byte string
    47             # though.
     47            # If there was no `original_exception` argument, assume the first
     48            # argument is a string. It may be a byte string though.
    4849            self.strerror = ftputil.tool.as_str(args[0])
    4950        else:
     
    6263
    6364
    64 # Internal errors are those that have more to do with the inner
    65 # workings of ftputil than with errors on the server side.
     65# Internal errors are those that have more to do with the inner workings of
     66# ftputil than with errors on the server side.
    6667class InternalError(FTPError):
    6768    """Internal error."""
     
    146147class FtplibErrorToFTPOSError:
    147148    """
    148     Context manager to convert `ftplib` exceptions to exceptions
    149     derived from `FTPOSError`.
     149    Context manager to convert `ftplib` exceptions to exceptions derived from
     150    `FTPOSError`.
    150151    """
    151152
     
    160161            raise TemporaryError(*exc_value.args, original_exception=exc_value)
    161162        elif isinstance(exc_value, ftplib.error_perm):
    162             # If `exc_value.args[0]` is present, assume it's a byte or
    163             # unicode string.
     163            # If `exc_value.args[0]` is present, assume it's a byte or unicode
     164            # string.
    164165            if exc_value.args and ftputil.tool.as_str(exc_value.args[0]).startswith(
    165166                "502"
     
    185186class FtplibErrorToFTPIOError:
    186187    """
    187     Context manager to convert `ftplib` exceptions to `FTPIOError`
    188     exceptions.
     188    Context manager to convert `ftplib` exceptions to `FTPIOError` exceptions.
    189189    """
    190190
  • ftputil/file.py

    r1895 r1935  
    1818class FTPFile:
    1919    """
    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.
     20    Represents a file-like object associated with an FTP host. File and socket
     21    are closed appropriately if the `close` method is called.
    2322    """
    2423
     
    5049        Open the remote file with given path name and mode.
    5150
    52         Contrary to the `open` builtin, this method returns `None`,
    53         instead this file object is modified in-place.
     51        Contrary to the `open` builtin, this method returns `None`, instead
     52        this file object is modified in-place.
    5453        """
    5554        # We use the same arguments as in `open`.
     
    9190            mode, buffering=buffering, encoding=encoding, errors=errors, newline=newline
    9291        )
    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.
     92        # This comes last so that `close` won't try to close `FTPFile` objects
     93        # without `_conn` and `_fobj` attributes in case of an error.
    9694        self.closed = False
    9795
    9896    def __iter__(self):
    99         """Return a file iterator."""
     97        """
     98        Return a file iterator.
     99        """
    100100        return self
    101101
    102102    def __next__(self):
    103103        """
    104         Return the next line or raise `StopIteration`, if there are
    105         no more.
     104        Return the next line or raise `StopIteration`, if there are no more.
    106105        """
    107106        # Apply implicit line ending conversion for text files.
     
    116115    #
    117116    def __enter__(self):
    118         # Return `self`, so it can be accessed as the variable
    119         # component of the `with` statement.
     117        # Return `self`, so it can be accessed as the variable component of the
     118        # `with` statement.
    120119        return self
    121120
     
    132131    def __getattr__(self, attr_name):
    133132        """
    134         Handle requests for attributes unknown to `FTPFile` objects:
    135         delegate the requests to the contained file object.
     133        Handle requests for attributes unknown to `FTPFile` objects: delegate
     134        the requests to the contained file object.
    136135        """
    137136        if attr_name in (
    138             "encoding flush isatty fileno read readline "
    139             "readlines seek tell truncate name softspace "
    140             "write writelines".split()
     137            "encoding flush isatty fileno read readline readlines seek tell "
     138            "truncate name softspace write writelines".split()
    141139        ):
    142140            return getattr(self._fobj, attr_name)
     
    147145
    148146    def close(self):
    149         """Close the `FTPFile`."""
     147        """
     148        Close the `FTPFile`.
     149        """
    150150        if self.closed:
    151151            return
    152152        # Timeout value to restore, see below.
    153         # Statement works only before the try/finally statement,
    154         # otherwise Python raises an `UnboundLocalError`.
     153        # Statement works only before the try/finally statement, otherwise
     154        # Python raises an `UnboundLocalError`.
    155155        old_timeout = self._session.sock.gettimeout()
    156156        try:
     
    159159            with ftputil.error.ftplib_error_to_ftp_io_error:
    160160                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.
     161            # Set a timeout to prevent waiting until server timeout if we have
     162            # a server blocking here like in ticket #51.
    163163            self._session.sock.settimeout(self._close_timeout)
    164164            try:
     
    168168                # Ignore some errors, see tickets #51 and #17 at
    169169                # http://ftputil.sschwarzer.net/trac/ticket/51 and
    170                 # http://ftputil.sschwarzer.net/trac/ticket/17,
    171                 # respectively.
     170                # http://ftputil.sschwarzer.net/trac/ticket/17, respectively.
    172171                exc = str(exc)
    173172                error_code = exc[:3]
     
    180179                    raise
    181180        finally:
    182             # Restore timeout for socket of `FTPFile`'s `ftplib.FTP`
    183             # object in case the connection is reused later.
     181            # Restore timeout for socket of `FTPFile`'s `ftplib.FTP` object in
     182            # case the connection is reused later.
    184183            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.
     184            # If something went wrong before, the file is probably defunct and
     185            # subsequent calls to `close` won't help either, so we consider the
     186            # file closed for practical purposes.
    189187            self.closed = True
    190188
  • ftputil/file_transfer.py

    r1923 r1935  
    1313
    1414# TODO: Think a bit more about the API before making it public.
    15 # # Only `chunks` should be used by clients of the ftputil library. Any
    16 # #  other functionality is supposed to be used via `FTPHost` objects.
     15# Only `chunks` should be used by clients of the ftputil library. Any other
     16# functionality is supposed to be used via `FTPHost` objects.
    1717# __all__ = ["chunks"]
    1818__all__ = []
     
    2424class LocalFile:
    2525    """
    26     Represent a file on the local side which is to be transferred or
    27     is already transferred.
     26    Represent a file on the local side which is to be transferred or is already
     27    transferred.
    2828    """
    2929
     
    3434    def exists(self):
    3535        """
    36         Return `True` if the path representing this file exists.
    37         Otherwise return `False`.
     36        Return `True` if the path representing this file exists. Otherwise
     37        return `False`.
    3838        """
    3939        return os.path.exists(self.name)
    4040
    4141    def mtime(self):
    42         """Return the timestamp for the last modification in seconds."""
     42        """
     43        Return the timestamp for the last modification in seconds.
     44        """
    4345        return os.path.getmtime(self.name)
    4446
    4547    def mtime_precision(self):
    46         """Return the precision of the last modification time in seconds."""
     48        """
     49        Return the precision of the last modification time in seconds.
     50        """
    4751        # Derived classes might want to use `self`.
    4852        # pylint: disable=no-self-use
    4953        #
    50         # Assume modification timestamps for local file systems are
    51         # at least precise up to a second.
     54        # Assume modification timestamps for local file systems are at least
     55        # precise up to a second.
    5256        return 1.0
    5357
    5458    def fobj(self):
    55         """Return a file object for the name/path in the constructor."""
     59        """
     60        Return a file object for the name/path in the constructor.
     61        """
    5662        return open(self.name, self.mode)
    5763
     
    5965class RemoteFile:
    6066    """
    61     Represent a file on the remote side which is to be transferred or
    62     is already transferred.
     67    Represent a file on the remote side which is to be transferred or is
     68    already transferred.
    6369    """
    6470
     
    7177    def exists(self):
    7278        """
    73         Return `True` if the path representing this file exists.
    74         Otherwise return `False`.
     79        Return `True` if the path representing this file exists. Otherwise
     80        return `False`.
    7581        """
    7682        return self._path.exists(self.name)
    7783
    7884    def mtime(self):
    79         """Return the timestamp for the last modification in seconds."""
    80         # Convert to client time zone (see definition of time
    81         # shift in docstring of `FTPHost.set_time_shift`).
     85        """
     86        Return the timestamp for the last modification in seconds.
     87        """
     88        # Convert to client time zone (see definition of time shift in
     89        # docstring of `FTPHost.set_time_shift`).
    8290        return self._path.getmtime(self.name)
    8391
    8492    def mtime_precision(self):
    85         """Return the precision of the last modification time in seconds."""
     93        """
     94        Return the precision of the last modification time in seconds.
     95        """
    8696        # I think using `stat` instead of `lstat` makes more sense here.
    8797        return self._host.stat(self.name)._st_mtime_precision
    8898
    8999    def fobj(self):
    90         """Return a file object for the name/path in the constructor."""
     100        """
     101        Return a file object for the name/path in the constructor.
     102        """
    91103        return self._host.open(self.name, self.mode)
    92104
     
    105117    (truncated).
    106118
    107     For the purpose of this test the source is newer than the target
    108     if any of the possible actual source modification times is greater
    109     than the reported target modification time. In other words: If in
    110     doubt, the file should be transferred.
    111 
    112     This is the only situation where the source is _not_ considered
    113     newer than the target:
     119    For the purpose of this test the source is newer than the target if any of
     120    the possible actual source modification times is greater than the reported
     121    target modification time. In other words: If in doubt, the file should be
     122    transferred.
     123
     124    This is the only situation where the source is _not_ considered newer than
     125    the target:
    114126
    115127    |/////////////////////|              possible source mtime
    116128                            |////////|   possible target mtime
    117129
    118     That is, the latest possible actual source modification time is
    119     before the first possible actual target modification time.
     130    That is, the latest possible actual source modification time is before the
     131    first possible actual target modification time.
    120132    """
    121133    if source_file.mtime_precision() is ftputil.stat.UNKNOWN_PRECISION:
     
    131143    Return an iterator which yields the contents of the file object.
    132144
    133     For each iteration, at most `max_chunk_size` bytes are read from
    134     `fobj` and yielded as a byte string. If the file object is
    135     exhausted, then don't yield any more data but stop the iteration,
    136     so the client does _not_ get an empty byte string.
    137 
    138     Any exceptions resulting from reading the file object are passed
    139     through to the client.
     145    For each iteration, at most `max_chunk_size` bytes are read from `fobj` and
     146    yielded as a byte string. If the file object is exhausted, then don't yield
     147    any more data but stop the iteration, so the client does _not_ get an empty
     148    byte string.
     149
     150    Any exceptions resulting from reading the file object are passed through to
     151    the client.
    140152    """
    141153    while True:
     
    149161    source_fobj, target_fobj, max_chunk_size=MAX_COPY_CHUNK_SIZE, callback=None
    150162):
    151     """Copy data from file-like object source to file-like object target."""
    152     # Inspired by `shutil.copyfileobj` (I don't use the `shutil`
    153     # code directly because it might change)
     163    """
     164    Copy data from file-like object source to file-like object target.
     165    """
     166    # Inspired by `shutil.copyfileobj` (I don't use the `shutil` code directly
     167    # because it might change)
    154168    for chunk in chunks(source_fobj, max_chunk_size):
    155169        target_fobj.write(chunk)
     
    162176    Copy a file from `source_file` to `target_file`.
    163177
    164     These are `LocalFile` or `RemoteFile` objects. Which of them
    165     is a local or a remote file, respectively, is determined by
    166     the arguments. If `conditional` is true, the file is only
    167     copied if the target doesn't exist or is older than the
    168     source. If `conditional` is false, the file is copied
    169     unconditionally. Return `True` if the file was copied, else
    170     `False`.
     178    These are `LocalFile` or `RemoteFile` objects. Which of them is a local or
     179    a remote file, respectively, is determined by the arguments. If
     180    `conditional` is true, the file is only copied if the target doesn't exist
     181    or is older than the source. If `conditional` is false, the file is copied
     182    unconditionally. Return `True` if the file was copied, else `False`.
    171183    """
    172184    if conditional:
    173         # Evaluate condition: The target file either doesn't exist or is
    174         # older than the source file. If in doubt (due to imprecise
    175         # timestamps), perform the transfer.
     185        # Evaluate condition: The target file either doesn't exist or is older
     186        # than the source file. If in doubt (due to imprecise timestamps),
     187        # perform the transfer.
    176188        transfer_condition = not target_file.exists() or source_is_newer_than_target(
    177189            source_file, target_file
  • ftputil/host.py

    r1933 r1935  
    2626
    2727
    28 # The "protected" attributes PyLint talks about aren't intended for
    29 # clients of the library. `FTPHost` objects need to use some of these
    30 # library-internal attributes though.
     28# The "protected" attributes PyLint talks about aren't intended for clients of
     29# the library. `FTPHost` objects need to use some of these library-internal
     30# attributes though.
    3131# pylint: disable=protected-access
    3232
     
    3737
    3838class FTPHost:
    39     """FTP host class."""
     39    """
     40    FTP host class.
     41    """
    4042
    4143    # Implementation notes:
    4244    #
    43     # Upon every request of a file (`FTPFile` object) a new FTP
    44     # session is created (or reused from a cache), leading to a child
    45     # session of the `FTPHost` object from which the file is requested.
    46     #
    47     # This is needed because opening an `FTPFile` will make the
    48     # local session object wait for the completion of the transfer.
    49     # In fact, code like this would block indefinitely, if the `RETR`
    50     # request would be made on the `_session` of the object host:
     45    # Upon every request of a file (`FTPFile` object) a new FTP session is
     46    # created (or reused from a cache), leading to a child session of the
     47    # `FTPHost` object from which the file is requested.
     48    #
     49    # This is needed because opening an `FTPFile` will make the local session
     50    # object wait for the completion of the transfer. In fact, code like this
     51    # would block indefinitely, if the `RETR` request would be made on the
     52    # `_session` of the object host:
    5153    #
    5254    #   host = FTPHost(ftp_server, user, password)
     
    5456    #   host.getcwd()   # would block!
    5557    #
    56     # On the other hand, the initially constructed host object will
    57     # store references to already established `FTPFile` objects and
    58     # reuse an associated connection if its associated `FTPFile`
    59     # has been closed.
     58    # On the other hand, the initially constructed host object will store
     59    # references to already established `FTPFile` objects and reuse an
     60    # associated connection if its associated `FTPFile` has been closed.
    6061
    6162    def __init__(self, *args, **kwargs):
    62         """Abstract initialization of `FTPHost` object."""
     63        """
     64        Abstract initialization of `FTPHost` object.
     65        """
    6366        # Store arguments for later operations.
    6467        self._args = args
    6568        self._kwargs = kwargs
    6669        # XXX: Maybe put the following in a `reset` method.
    67         # The time shift setting shouldn't be reset though.
    68         # Make a session according to these arguments.
     70        # The time shift setting shouldn't be reset though. Make a session
     71        # according to these arguments.
    6972        self._session = self._make_session()
    7073        # Simulate `os.path`.
     
    8083        # Associated `FTPHost` objects for data transfer.
    8184        self._children = []
    82         # This is only set to something else than `None` if this
    83         # instance represents an `FTPFile`.
     85        # This is only set to something else than `None` if this instance
     86        # represents an `FTPFile`.
    8487        self._file = None
    8588        # Now opened.
    8689        self.closed = False
    87         # Set curdir, pardir etc. for the remote host. RFC 959 states
    88         # that this is, strictly speaking, dependent on the server OS
    89         # but it seems to work at least with Unix and Windows servers.
     90        # Set curdir, pardir etc. for the remote host. RFC 959 states that this
     91        # is, strictly speaking, dependent on the server OS but it seems to
     92        # work at least with Unix and Windows servers.
    9093        self.curdir, self.pardir, self.sep = ".", "..", "/"
    9194        # Set default time shift (used in `upload_if_newer` and
     
    9396        self._time_shift = 0.0
    9497        # Don't use `LIST -a` option by default. If the server doesn't
    95         # understand the `-a` option and interprets it as a path, the
    96         # results can be surprising. See ticket #110.
     98        # understand the `-a` option and interprets it as a path, the results
     99        # can be surprising. See ticket #110.
    97100        self.use_list_a_option = False
    98101
     
    101104        Try to keep the connection alive in order to avoid server timeouts.
    102105
    103         Note that this won't help if the connection has already timed
    104         out! In this case, `keep_alive` will raise an `TemporaryError`.
    105         (Actually, if you get a server timeout, the error - for a specific
    106         connection - will be permanent.)
     106        Note that this won't help if the connection has already timed out! In
     107        this case, `keep_alive` will raise an `TemporaryError`. (Actually, if
     108        you get a server timeout, the error - for a specific connection - will
     109        be permanent.)
    107110        """
    108111        # Warning: Don't call this method on `FTPHost` instances which
     
    113116
    114117    #
    115     # Dealing with child sessions and file-like objects
    116     # (rather low-level)
     118    # Dealing with child sessions and file-like objects (rather low-level)
    117119    #
    118120    def _make_session(self):
    119121        """
    120         Return a new session object according to the current state of
    121         this `FTPHost` instance.
     122        Return a new session object according to the current state of this
     123        `FTPHost` instance.
    122124        """
    123125        # Don't modify original attributes below.
    124126        args = self._args[:]
    125127        kwargs = self._kwargs.copy()
    126         # If a session factory has been given on the instantiation of
    127         # this `FTPHost` object, use the same factory for this
    128         # `FTPHost` object's child sessions.
     128        # If a session factory has been given on the instantiation of this
     129        # `FTPHost` object, use the same factory for this `FTPHost` object's
     130        # child sessions.
    129131        factory = kwargs.pop("session_factory", ftplib.FTP)
    130132        with ftputil.error.ftplib_error_to_ftp_os_error:
     
    133135
    134136    def _copy(self):
    135         """Return a copy of this `FTPHost` object."""
    136         # The copy includes a new session factory return value (aka
    137         # session) but doesn't copy the state of `self.getcwd()`.
     137        """
     138        Return a copy of this `FTPHost` object.
     139        """
     140        # The copy includes a new session factory return value (aka session)
     141        # but doesn't copy the state of `self.getcwd()`.
    138142        return self.__class__(*self._args, **self._kwargs)
    139143
    140144    def _available_child(self):
    141145        """
    142         Return an available (i. e. one whose `_file` object is closed
    143         and doesn't have a timed-out server connection) child
    144         (`FTPHost` object) from the pool of children or `None` if
    145         there aren't any.
    146         """
    147         # TODO: Currently timed-out child sessions aren't removed and
    148         # may collect over time. In very busy or long running
    149         # processes, this might slow down an application because the
    150         # same stale child sessions have to be processed again and
    151         # again.
     146        Return an available (i. e. one whose `_file` object is closed and
     147        doesn't have a timed-out server connection) child (`FTPHost` object)
     148        from the pool of children or `None` if there aren't any.
     149        """
     150        # TODO: Currently timed-out child sessions aren't removed and may
     151        # collect over time. In very busy or long running processes, this might
     152        # slow down an application because the same stale child sessions have
     153        # to be processed again and again.
    152154        for host in self._children:
    153155            # Test for timeouts only after testing for a closed file:
    154             # - If a file isn't closed, save time; don't bother to access
    155             #   the remote server.
    156             # - If a file transfer on the child is in progress, requesting
    157             #   the directory is an invalid operation because of the way
    158             #   the FTP state machine works (see RFC 959).
     156            # - If a file isn't closed, save time; don't bother to access the
     157            #   remote server.
     158            # - If a file transfer on the child is in progress, requesting the
     159            #   directory is an invalid operation because of the way the FTP
     160            #   state machine works (see RFC 959).
    159161            if host._file.closed:
    160162                try:
    161163                    host._session.pwd()
    162                 # Under high load, a 226 status response from a
    163                 # previous download may arrive too late, so that it's
    164                 # "seen" in the `pwd` call. For now, skip the
    165                 # potential child session; it will be considered again
    166                 # when `_available_child` is called the next time.
     164                # Under high load, a 226 status response from a previous
     165                # download may arrive too late, so that it's "seen" in the
     166                # `pwd` call. For now, skip the potential child session; it
     167                # will be considered again when `_available_child` is called
     168                # the next time.
    167169                except ftplib.error_reply:
    168170                    continue
     
    170172                except ftplib.error_temp:
    171173                    continue
    172                 # The server may have closed the connection which may
    173                 # cause `host._session.getline` to raise an `EOFError`
    174                 # (see ticket #114).
     174                # The server may have closed the connection which may cause
     175                # `host._session.getline` to raise an `EOFError` (see ticket
     176                # #114).
    175177                except EOFError:
    176178                    continue
    177                 # Under high load, there may be a socket read timeout
    178                 # during the last FTP file `close` (see ticket #112).
    179                 # Note that a socket timeout is quite different from
    180                 # an FTP session timeout.
     179                # Under high load, there may be a socket read timeout during
     180                # the last FTP file `close` (see ticket #112). Note that a
     181                # socket timeout is quite different from an FTP session
     182                # timeout.
    181183                except OSError:
    182184                    continue
     
    199201    ):
    200202        """
    201         Return an open file(-like) object which is associated with
    202         this `FTPHost` object.
    203 
    204         The arguments `path`, `mode`, `buffering`, `encoding`,
    205         `errors` and `newlines` have the same meaning as for `open`.
    206         If `rest` is given as an integer,
     203        Return an open file(-like) object which is associated with this
     204        `FTPHost` object.
     205
     206        The arguments `path`, `mode`, `buffering`, `encoding`, `errors` and
     207        `newlines` have the same meaning as for `open`. If `rest` is given as
     208        an integer,
    207209
    208210        - reading will start at the byte (zero-based) `rest`
    209211        - writing will overwrite the remote file from byte `rest`
    210212
    211         This method tries to reuse a child but will generate a new one
    212         if none is available.
     213        This method tries to reuse a child but will generate a new one if none
     214        is available.
    213215        """
    214216        # Support the same arguments as `open`.
     
    221223            host._file = ftputil.file.FTPFile(host)
    222224        basedir = self.getcwd()
    223         # Prepare for changing the directory (see whitespace workaround
    224         # in method `_dir`).
     225        # Prepare for changing the directory (see whitespace workaround in
     226        # method `_dir`).
    225227        if host.path.isabs(path):
    226228            effective_path = path
     
    232234            host.chdir(effective_dir)
    233235        except ftputil.error.PermanentError:
    234             # Similarly to a failed `file` in a local file system,
    235             # raise an `IOError`, not an `OSError`.
     236            # Similarly to a failed `file` in a local file system, raise an
     237            # `IOError`, not an `OSError`.
    236238            raise ftputil.error.FTPIOError(
    237239                "remote directory '{}' doesn't "
     
    253255
    254256    def close(self):
    255         """Close host connection."""
     257        """
     258        Close host connection.
     259        """
    256260        if self.closed:
    257261            return
     
    266270                self._session.close()
    267271        finally:
    268             # If something went wrong before, the host/session is
    269             # probably defunct and subsequent calls to `close` won't
    270             # help either, so consider the host/session closed for
    271             # practical purposes.
     272            # If something went wrong before, the host/session is probably
     273            # defunct and subsequent calls to `close` won't help either, so
     274            # consider the host/session closed for practical purposes.
    272275            self.stat_cache.clear()
    273276            self._children = []
     
    279282    def set_parser(self, parser):
    280283        """
    281         Set the parser for extracting stat results from directory
    282         listings.
    283 
    284         The parser interface is described in the documentation, but
    285         here are the most important things:
     284        Set the parser for extracting stat results from directory listings.
     285
     286        The parser interface is described in the documentation, but here are
     287        the most important things:
    286288
    287289        - A parser should derive from `ftputil.stat.Parser`.
    288290
    289291        - The parser has to implement two methods, `parse_line` and
    290           `ignores_line`. For the latter, there's a probably useful
    291           default in the class `ftputil.stat.Parser`.
    292 
    293         - `parse_line` should try to parse a line of a directory
    294           listing and return a `ftputil.stat.StatResult` instance. If
    295           parsing isn't possible, raise `ftputil.error.ParserError`
    296           with a useful error message.
    297 
    298         - `ignores_line` should return a true value if the line isn't
    299           assumed to contain stat information.
     292          `ignores_line`. For the latter, there's a probably useful default
     293          in the class `ftputil.stat.Parser`.
     294
     295        - `parse_line` should try to parse a line of a directory listing and
     296          return a `ftputil.stat.StatResult` instance. If parsing isn't
     297          possible, raise `ftputil.error.ParserError` with a useful error
     298          message.
     299
     300        - `ignores_line` should return a true value if the line isn't assumed
     301          to contain stat information.
    300302        """
    301303        # The cache contents, if any, probably aren't useful.
     
    311313    def __rounded_time_shift(time_shift):
    312314        """
    313         Return the given time shift in seconds, but rounded to
    314         15-minute units. The argument is also assumed to be given in
    315         seconds.
     315        Return the given time shift in seconds, but rounded to 15-minute units.
     316        The argument is also assumed to be given in seconds.
    316317        """
    317318        minute = 60.0
     
    331332    def __assert_valid_time_shift(self, time_shift):
    332333        """
    333         Perform sanity checks on the time shift value (given in
    334         seconds). If the value is invalid, raise a `TimeShiftError`,
    335         else simply return `None`.
     334        Perform sanity checks on the time shift value (given in seconds). If
     335        the value is invalid, raise a `TimeShiftError`, else simply return
     336        `None`.
    336337        """
    337338        minute = 60.0  # seconds
    338339        hour = 60.0 * minute
    339340        absolute_rounded_time_shift = abs(self.__rounded_time_shift(time_shift))
    340         # Test 1: Fail if the absolute time shift is greater than
    341         #         a full day (24 hours).
     341        # Test 1: Fail if the absolute time shift is greater than a full day
     342        #         (24 hours).
    342343        if absolute_rounded_time_shift > 24 * hour:
    343344            raise ftputil.error.TimeShiftError(
    344345                "time shift abs({0:.2f} s) > 1 day".format(time_shift)
    345346            )
    346         # Test 2: Fail if the deviation between given time shift and
    347         #         15-minute units is greater than a certain limit.
     347        # Test 2: Fail if the deviation between given time shift and 15-minute
     348        #         units is greater than a certain limit.
    348349        maximum_deviation = 5 * minute
    349350        if abs(time_shift - self.__rounded_time_shift(time_shift)) > maximum_deviation:
     
    355356    def set_time_shift(self, time_shift):
    356357        """
    357         Set the time shift value (i. e. the time difference between
    358         client and server, where the client time is in UTC) for this
    359         `FTPHost` object. By (my) definition, the time shift value is
    360         positive if the local time of the server is greater than the
    361         local time of the client (for the same physical time), i. e.
     358        Set the time shift value (i. e. the time difference between client and
     359        server, where the client time is in UTC) for this `FTPHost` object. By
     360        (my) definition, the time shift value is positive if the local time of
     361        the server is greater than the local time of the client (for the same
     362        physical time), i. e.
    362363
    363364            time_shift =def= t_server - t_client_utc
     
    385386    def synchronize_times(self):
    386387        """
    387         Synchronize the local times of FTP client and server. This
    388         is necessary to let `upload_if_newer` and `download_if_newer`
    389         work correctly. If `synchronize_times` isn't applicable
    390         (see below), the time shift can still be set explicitly with
    391         `set_time_shift`.
    392 
    393         This implementation of `synchronize_times` requires _all_ of
    394         the following:
     388        Synchronize the local times of FTP client and server. This is necessary
     389        to let `upload_if_newer` and `download_if_newer` work correctly. If
     390        `synchronize_times` isn't applicable (see below), the time shift can
     391        still be set explicitly with `set_time_shift`.
     392
     393        This implementation of `synchronize_times` requires _all_ of the
     394        following:
    395395
    396396        - The connection between server and client is established.
    397         - The client has write access to the directory that is
    398           current when `synchronize_times` is called.
    399 
    400         The common usage pattern of `synchronize_times` is to call it
    401         directly after the connection is established. (As can be
    402         concluded from the points above, this requires write access
    403         to the login directory.)
     397        - The client has write access to the directory that is current when
     398          `synchronize_times` is called.
     399
     400        The common usage pattern of `synchronize_times` is to call it directly
     401        after the connection is established. (As can be concluded from the
     402        points above, this requires write access to the login directory.)
    404403
    405404        If `synchronize_times` fails, it raises a `TimeShiftError`.
    406405        """
    407406        helper_file_name = "_ftputil_sync_"
    408         # Open a dummy file for writing in the current directory
    409         # on the FTP host, then close it.
     407        # Open a dummy file for writing in the current directory on the FTP
     408        # host, then close it.
    410409        try:
    411410            # May raise `FTPIOError` if directory isn't writable.
     
    416415                """couldn't write helper file in directory '{}'""".format(self.getcwd())
    417416            )
    418         # If everything worked up to here it should be possible to stat
    419         # and then remove the just-written file.
     417        # If everything worked up to here it should be possible to stat and
     418        # then remove the just-written file.
    420419        try:
    421420            server_time = self.path.getmtime(helper_file_name)
    422421            self.unlink(helper_file_name)
    423422        except ftputil.error.FTPOSError:
    424             # If we got a `TimeShiftError` exception above, we
    425             # should't come here: if we didn't get a `TimeShiftError`
    426             # above, deletion should be possible. The only reason for
    427             # an exception I can think of here is a race condition by
    428             # removing the helper file or write permission from the
    429             # directory or helper file after it has been written to.
     423            # If we got a `TimeShiftError` exception above, we should't come
     424            # here: if we didn't get a `TimeShiftError` above, deletion should
     425            # be possible. The only reason for an exception I can think of here
     426            # is a race condition by removing the helper file or write
     427            # permission from the directory or helper file after it has been
     428            # written to.
    430429            raise ftputil.error.TimeShiftError(
    431430                "could write helper file but not unlink it"
     
    434433        now = time.time()
    435434        time_shift = server_time - now
    436         # As the time shift for this host instance isn't set yet, the
    437         # directory parser will calculate times one year in the past if
    438         # the time zone of the server is east from ours. Thus the time
    439         # shift will be off by a year as well (see ticket #55).
     435        # As the time shift for this host instance isn't set yet, the directory
     436        # parser will calculate times one year in the past if the time zone of
     437        # the server is east from ours. Thus the time shift will be off by a
     438        # year as well (see ticket #55).
    440439        if time_shift < -360 * 24 * 60 * 60:
    441             # Readd one year and recalculate the time shift. We don't
    442             # know how many days made up that year (it might have been
    443             # a leap year), so go the route via `datetime.replace`.
     440            # Read one year and recalculate the time shift. We don't know how
     441            # many days made up that year (it might have been a leap year), so
     442            # go the route via `datetime.replace`.
    444443            server_datetime = datetime.datetime.fromtimestamp(
    445444                server_time, tz=datetime.timezone.utc
     
    450449
    451450    #
    452     # Operations based on file-like objects (rather high-level),
    453     # like upload and download
     451    # Operations based on file-like objects (rather high-level), like upload
     452    # and download
    454453    #
    455454    # XXX: This has a different API from `shutil.copyfileobj`, on which this
     
    465464    ):
    466465        """
    467         Copy data from file-like object `source` to file-like object
    468         `target`.
     466        Copy data from file-like object `source` to file-like object `target`.
    469467        """
    470468        ftputil.file_transfer.copyfileobj(source, target, max_chunk_size, callback)
     
    475473        respectively.
    476474
    477         The strings `source_path` and `target_path` are the (absolute
    478         or relative) paths of the local and the remote file, respectively.
     475        The strings `source_path` and `target_path` are the (absolute or
     476        relative) paths of the local and the remote file, respectively.
    479477        """
    480478        source_file = ftputil.file_transfer.LocalFile(source_path, "rb")
     
    485483    def upload(self, source, target, callback=None):
    486484        """
    487         Upload a file from the local source (name) to the remote
    488         target (name).
    489 
    490         If a callable `callback` is given, it's called after every
    491         chunk of transferred data. The chunk size is a constant
    492         defined in `file_transfer`. The callback will be called with a
    493         single argument, the data chunk that was transferred before
    494         the callback was called.
     485        Upload a file from the local source (name) to the remote target (name).
     486
     487        If a callable `callback` is given, it's called after every chunk of
     488        transferred data. The chunk size is a constant defined in
     489        `file_transfer`. The callback will be called with a single argument,
     490        the data chunk that was transferred before the callback was called.
    495491        """
    496492        target = ftputil.tool.as_str_path(target)
     
    502498    def upload_if_newer(self, source, target, callback=None):
    503499        """
    504         Upload a file only if it's newer than the target on the
    505         remote host or if the target file does not exist. See the
    506         method `upload` for the meaning of the parameters.
     500        Upload a file only if it's newer than the target on the remote host or
     501        if the target file does not exist. See the method `upload` for the
     502        meaning of the parameters.
    507503
    508504        If an upload was necessary, return `True`, else return `False`.
    509505
    510         If a callable `callback` is given, it's called after every
    511         chunk of transferred data. The chunk size is a constant
    512         defined in `file_transfer`. The callback will be called with a
    513         single argument, the data chunk that was transferred before
    514         the callback was called.
     506        If a callable `callback` is given, it's called after every chunk of
     507        transferred data. The chunk size is a constant defined in
     508        `file_transfer`. The callback will be called with a single argument,
     509        the data chunk that was transferred before the callback was called.
    515510        """
    516511        target = ftputil.tool.as_str_path(target)
     
    525520        respectively.
    526521
    527         The strings `source_path` and `target_path` are the (absolute
    528         or relative) paths of the remote and the local file, respectively.
     522        The strings `source_path` and `target_path` are the (absolute or
     523        relative) paths of the remote and the local file, respectively.
    529524        """
    530525        source_file = ftputil.file_transfer.RemoteFile(self, source_path, "rb")
     
    534529    def download(self, source, target, callback=None):
    535530        """
    536         Download a file from the remote source (name) to the local
    537         target (name).
    538 
    539         If a callable `callback` is given, it's called after every
    540         chunk of transferred data. The chunk size is a constant
    541         defined in `file_transfer`. The callback will be called with a
    542         single argument, the data chunk that was transferred before
    543         the callback was called.
     531        Download a file from the remote source (name) to the local target
     532        (name).
     533
     534        If a callable `callback` is given, it's called after every chunk of
     535        transferred data. The chunk size is a constant defined in
     536        `file_transfer`. The callback will be called with a single argument,
     537        the data chunk that was transferred before the callback was called.
    544538        """
    545539        source = ftputil.tool.as_str_path(source)
     
    551545    def download_if_newer(self, source, target, callback=None):
    552546        """
    553         Download a file only if it's newer than the target on the
    554         local host or if the target file does not exist. See the
    555         method `download` for the meaning of the parameters.
    556 
    557         If a download was necessary, return `True`, else return
    558         `False`.
    559 
    560         If a callable `callback` is given, it's called after every
    561         chunk of transferred data. The chunk size is a constant
    562         defined in `file_transfer`. The callback will be called with a
    563         single argument, the data chunk that was transferred before
    564         the callback was called.
     547        Download a file only if it's newer than the target on the local host or
     548        if the target file does not exist. See the method `download` for the
     549        meaning of the parameters.
     550
     551        If a download was necessary, return `True`, else return `False`.
     552
     553        If a callable `callback` is given, it's called after every chunk of
     554        transferred data. The chunk size is a constant defined in
     555        `file_transfer`. The callback will be called with a single argument,
     556        the data chunk that was transferred before the callback was called.
    565557        """
    566558        source = ftputil.tool.as_str_path(source)
     
    575567    def _check_inaccessible_login_directory(self):
    576568        """
    577         Raise an `InaccessibleLoginDirError` exception if we can't
    578         change to the login directory. This test is only reliable if
    579         the current directory is the login directory.
     569        Raise an `InaccessibleLoginDirError` exception if we can't change to
     570        the login directory. This test is only reliable if the current
     571        directory is the login directory.
    580572        """
    581573        presumable_login_dir = self.getcwd()
    582         # Bail out with an internal error rather than modify the
    583         # current directory without hope of restoration.
     574        # Bail out with an internal error rather than modify the current
     575        # directory without hope of restoration.
    584576        try:
    585577            self.chdir(presumable_login_dir)
     
    591583    def _robust_ftp_command(self, command, path, descend_deeply=False):
    592584        """
    593         Run an FTP command on a path. The return value of the method
    594         is the return value of the command.
    595 
    596         If `descend_deeply` is true (the default is false), descend
    597         deeply, i. e. change the directory to the end of the path.
    598         """
    599         # If we can't change to the yet-current directory, the code
    600         # below won't work (see below), so in this case rather raise
    601         # an exception than giving wrong results.
     585        Run an FTP command on a path. The return value of the method is the
     586        return value of the command.
     587
     588        If `descend_deeply` is true (the default is false), descend deeply,
     589        i. e. change the directory to the end of the path.
     590        """
     591        # If we can't change to the yet-current directory, the code below won't
     592        # work (see below), so in this case rather raise an exception than
     593        # giving wrong results.
    602594        self._check_inaccessible_login_directory()
    603         # Some FTP servers don't behave as expected if the directory
    604         # portion of the path contains whitespace; some even yield
    605         # strange results if the command isn't executed in the
    606         # current directory. Therefore, change to the directory
    607         # which contains the item to run the command on and invoke
    608         # the command just there.
     595        # Some FTP servers don't behave as expected if the directory portion of
     596        # the path contains whitespace; some even yield strange results if the
     597        # command isn't executed in the current directory. Therefore, change to
     598        # the directory which contains the item to run the command on and
     599        # invoke the command just there.
    609600        #
    610601        # Remember old working directory.
     
    614605                # Invoke the command in (not: on) the deepest directory.
    615606                self.chdir(path)
    616                 # Workaround for some servers that give recursive
    617                 # listings when called with a dot as path; see issue #33,
     607                # Workaround for some servers that give recursive listings when
     608                # called with a dot as path; see issue #33,
    618609                # http://ftputil.sschwarzer.net/trac/ticket/33
    619610                return command(self, "")
     
    630621    #
    631622    def getcwd(self):
    632         """Return the current path name."""
     623        """
     624        Return the current path name.
     625        """
    633626        return self._cached_current_dir
    634627
    635628    def chdir(self, path):
    636         """Change the directory on the host."""
     629        """
     630        Change the directory on the host.
     631        """
    637632        path = ftputil.tool.as_str_path(path)
    638633        with ftputil.error.ftplib_error_to_ftp_os_error:
     
    648643    def mkdir(self, path, mode=None):
    649644        """
    650         Make the directory path on the remote host. The argument
    651         `mode` is ignored and only "supported" for similarity with
    652         `os.mkdir`.
     645        Make the directory path on the remote host. The argument `mode` is
     646        ignored and only "supported" for similarity with `os.mkdir`.
    653647        """
    654648        path = ftputil.tool.as_str_path(path)
     
    661655        self._robust_ftp_command(command, path)
    662656
    663     # TODO: The virtual directory support doesn't have unit tests yet
    664     # because the mocking most likely would be quite complicated. The
    665     # tests should be added when mainly the `mock` library is used
    666     # instead of the mock code in `test.mock_ftplib`.
     657    # TODO: The virtual directory support doesn't have unit tests yet because
     658    # the mocking most likely would be quite complicated. The tests should be
     659    # added when mainly the `mock` library is used instead of the mock code in
     660    # `test.mock_ftplib`.
    667661    #
    668662    # Ignore unused argument `mode`
    669663    # pylint: disable=unused-argument
    670     def makedirs(self, path, mode=None):
    671         """
    672         Make the directory `path`, but also make not yet existing
    673         intermediate directories, like `os.makedirs`. The value of
    674         `mode` is only accepted for compatibility with `os.makedirs`
    675         but otherwise ignored.
     664    def makedirs(self, path, mode=None, exist_ok=False):
     665        """
     666        Make the directory `path`, but also make not yet existing intermediate
     667        directories, like `os.makedirs`. The value of `mode` is only accepted
     668        for compatibility with `os.makedirs` but otherwise ignored.
    676669        """
    677670        path = ftputil.tool.as_str_path(path)
     
    680673        old_dir = self.getcwd()
    681674        try:
    682             # Try to build the directory chain from the "uppermost" to
    683             # the "lowermost" directory.
     675            # Try to build the directory chain from the "uppermost" to the
     676            # "lowermost" directory.
    684677            for index in range(1, len(directories)):
    685                 # Re-insert the separator which got lost by using
    686                 # `path.split`.
     678                # Re-insert the separator which got lost by using `path.split`.
    687679                next_directory = self.sep + self.path.join(*directories[: index + 1])
    688                 # If we have "virtual directories" (see #86), just
    689                 # listing the parent directory won't tell us if a
    690                 # directory actually exists. So try to change into the
    691                 # directory.
     680                # If we have "virtual directories" (see #86), just listing the
     681                # parent directory won't tell us if a directory actually
     682                # exists. So try to change into the directory.
    692683                try:
    693684                    self.chdir(next_directory)
     
    696687                        self.mkdir(next_directory)
    697688                    except ftputil.error.PermanentError:
    698                         # Find out the cause of the error. Re-raise
    699                         # the exception only if the directory didn't
    700                         # exist already, else something went _really_
    701                         # wrong, e. g. there's a regular file with the
    702                         # name of the directory.
     689                        # Find out the cause of the error. Re-raise the
     690                        # exception only if the directory didn't exist already,
     691                        # else something went _really_ wrong, e. g. there's a
     692                        # regular file with the name of the directory.
    703693                        if not self.path.isdir(next_directory):
    704694                            raise
     
    712702        Compatibility note:
    713703
    714         Previous versions of ftputil could possibly delete non-
    715         empty directories as well, - if the server allowed it. This
    716         is no longer supported.
     704        Previous versions of ftputil could possibly delete non-empty
     705        directories as well, - if the server allowed it. This is no longer
     706        supported.
    717707        """
    718708        path = ftputil.tool.as_str_path(path)
     
    733723        Remove the file or link given by `path`.
    734724
    735         Raise a `PermanentError` if the path doesn't exist, but maybe
    736         raise other exceptions depending on the state of the server
    737         (e. g. timeout).
     725        Raise a `PermanentError` if the path doesn't exist, but maybe raise
     726        other exceptions depending on the state of the server (e. g. timeout).
    738727        """
    739728        path = ftputil.tool.as_str_path(path)
    740729        path = self.path.abspath(path)
    741         # Though `isfile` includes also links to files, `islink`
    742         # is needed to include links to directories.
     730        # Though `isfile` includes also links to files, `islink` is needed to
     731        # include links to directories.
    743732        if (
    744733            self.path.isfile(path)
     
    746735            or not self.path.exists(path)
    747736        ):
    748             # If the path doesn't exist, let the removal command trigger
    749             # an exception with a more appropriate error message.
     737            # If the path doesn't exist, let the removal command trigger an
     738            # exception with a more appropriate error message.
    750739            def command(self, path):
    751740                """Callback function."""
     
    764753    def rmtree(self, path, ignore_errors=False, onerror=None):
    765754        """
    766         Remove the given remote, possibly non-empty, directory tree.
    767         The interface of this method is rather complex, in favor of
    768         compatibility with `shutil.rmtree`.
    769 
    770         If `ignore_errors` is set to a true value, errors are ignored.
    771         If `ignore_errors` is a false value _and_ `onerror` isn't set,
    772         all exceptions occurring during the tree iteration and
    773         processing are raised. These exceptions are all of type
    774         `PermanentError`.
    775 
    776         To distinguish between error situations, pass in a callable
    777         for `onerror`. This callable must accept three arguments:
    778         `func`, `path` and `exc_info`. `func` is a bound method
    779         object, _for example_ `your_host_object.listdir`. `path` is
    780         the path that was the recent argument of the respective method
    781         (`listdir`, `remove`, `rmdir`). `exc_info` is the exception
    782         info as it's got from `sys.exc_info`.
    783 
    784         Implementation note: The code is copied from `shutil.rmtree`
    785         in Python 2.4 and adapted to ftputil.
     755        Remove the given remote, possibly non-empty, directory tree. The
     756        interface of this method is rather complex, in favor of compatibility
     757        with `shutil.rmtree`.
     758
     759        If `ignore_errors` is set to a true value, errors are ignored. If
     760        `ignore_errors` is a false value _and_ `onerror` isn't set, all
     761        exceptions occurring during the tree iteration and processing are
     762        raised. These exceptions are all of type `PermanentError`.
     763
     764        To distinguish between error situations, pass in a callable for
     765        `onerror`. This callable must accept three arguments: `func`, `path`
     766        and `exc_info`. `func` is a bound method object, _for example_
     767        `your_host_object.listdir`. `path` is the path that was the recent
     768        argument of the respective method (`listdir`, `remove`, `rmdir`).
     769        `exc_info` is the exception info as it's got from `sys.exc_info`.
     770
     771        Implementation note: The code is copied from `shutil.rmtree` in
     772        Python 2.4 and adapted to ftputil.
    786773        """
    787774        path = ftputil.tool.as_str_path(path)
     
    828815
    829816    def rename(self, source, target):
    830         """Rename the source on the FTP host to target."""
     817        """
     818        Rename the source on the FTP host to target.
     819        """
    831820        source = ftputil.tool.as_str_path(source)
    832821        target = ftputil.tool.as_str_path(target)
    833         # The following code is in spirit similar to the code in the
    834         # method `_robust_ftp_command`, though we do _not_ do
    835         # _everything_ imaginable.
     822        # The following code is in spirit similar to the code in the method
     823        # `_robust_ftp_command`, though we do _not_ do _everything_ imaginable.
    836824        self._check_inaccessible_login_directory()
    837825        source_head, source_tail = self.path.split(source)
     
    852840                self._session.rename(source, target)
    853841
    854     # XXX: One could argue to put this method into the `_Stat` class,
    855     # but I refrained from that because then `_Stat` would have to
    856     # know about `FTPHost`'s `_session` attribute and in turn about
    857     # `_session`'s `dir` method.
     842    # XXX: One could argue to put this method into the `_Stat` class, but I
     843    # refrained from that because then `_Stat` would have to know about
     844    # `FTPHost`'s `_session` attribute and in turn about `_session`'s `dir`
     845    # method.
    858846    def _dir(self, path):
    859847        """
    860         Return a directory listing as made by FTP's `LIST` command as
    861         a list of strings.
    862         """
    863         # Don't use `self.path.isdir` in this method because that
    864         # would cause a call of `(l)stat` and thus a call to `_dir`,
    865         # so we would end up with an infinite recursion.
     848        Return a directory listing as made by FTP's `LIST` command as a list of
     849        strings.
     850        """
     851        # Don't use `self.path.isdir` in this method because that would cause a
     852        # call of `(l)stat` and thus a call to `_dir`, so we would end up with
     853        # an infinite recursion.
    866854        def _FTPHost_dir_command(self, path):
    867855            """Callback function."""
     
    884872        return lines
    885873
    886     # The `listdir`, `lstat` and `stat` methods don't use
    887     # `_robust_ftp_command` because they implicitly already use
    888     # `_dir` which actually uses `_robust_ftp_command`.
     874    # The `listdir`, `lstat` and `stat` methods don't use `_robust_ftp_command`
     875    # because they implicitly already use `_dir` which actually uses
     876    # `_robust_ftp_command`.
    889877
    890878    def listdir(self, path):
    891879        """
    892         Return a list of directories, files etc. in the directory
    893         named `path`.
    894 
    895         If the directory listing from the server can't be parsed with
    896         any of the available parsers raise a `ParserError`.
     880        Return a list of directories, files etc. in the directory named `path`.
     881
     882        If the directory listing from the server can't be parsed with any of
     883        the available parsers raise a `ParserError`.
    897884        """
    898885        original_path = path
     
    905892        Return an object similar to that returned by `os.lstat`.
    906893
    907         If the directory listing from the server can't be parsed with
    908         any of the available parsers, raise a `ParserError`. If the
    909         directory _can_ be parsed and the `path` is _not_ found, raise
    910         a `PermanentError`.
    911 
    912         (`_exception_for_missing_path` is an implementation aid and
    913         _not_ intended for use by ftputil clients.)
     894        If the directory listing from the server can't be parsed with any of
     895        the available parsers, raise a `ParserError`. If the directory _can_ be
     896        parsed and the `path` is _not_ found, raise a `PermanentError`.
     897
     898        (`_exception_for_missing_path` is an implementation aid and _not_
     899        intended for use by ftputil clients.)
    914900        """
    915901        path = ftputil.tool.as_str_path(path)
     
    921907
    922908        If the directory containing `path` can't be parsed, raise a
    923         `ParserError`. If the directory containing `path` can be
    924         parsed but the `path` can't be found, raise a
    925         `PermanentError`. Also raise a `PermanentError` if there's an
    926         endless (cyclic) chain of symbolic links "behind" the `path`.
    927 
    928         (`_exception_for_missing_path` is an implementation aid and
    929         _not_ intended for use by ftputil clients.)
     909        `ParserError`. If the directory containing `path` can be parsed but the
     910        `path` can't be found, raise a `PermanentError`. Also raise a
     911        `PermanentError` if there's an endless (cyclic) chain of symbolic links
     912        "behind" the `path`.
     913
     914        (`_exception_for_missing_path` is an implementation aid and _not_
     915        intended for use by ftputil clients.)
    930916        """
    931917        path = ftputil.tool.as_str_path(path)
     
    934920    def walk(self, top, topdown=True, onerror=None, followlinks=False):
    935921        """
    936         Iterate over directory tree and return a tuple (dirpath,
    937         dirnames, filenames) on each iteration, like the `os.walk`
    938         function (see https://docs.python.org/library/os.html#os.walk ).
     922        Iterate over directory tree and return a tuple (dirpath, dirnames,
     923        filenames) on each iteration, like the `os.walk` function (see
     924        https://docs.python.org/library/os.html#os.walk ).
    939925        """
    940926        top = ftputil.tool.as_str_path(top)
    941         # The following code is copied from `os.walk` in Python 2.4
    942         # and adapted to ftputil.
     927        # The following code is copied from `os.walk` in Python 2.4 and adapted
     928        # to ftputil.
    943929        try:
    944930            names = self.listdir(top)
     
    964950    def chmod(self, path, mode):
    965951        """
    966         Change the mode of a remote `path` (a string) to the integer
    967         `mode`. This integer uses the same bits as the mode value
    968         returned by the `stat` and `lstat` commands.
     952        Change the mode of a remote `path` (a string) to the integer `mode`.
     953        This integer uses the same bits as the mode value returned by the
     954        `stat` and `lstat` commands.
    969955
    970956        If something goes wrong, raise a `TemporaryError` or a
    971         `PermanentError`, according to the status code returned by
    972         the server. In particular, a non-existent path usually
    973         causes a `PermanentError`.
     957        `PermanentError`, according to the status code returned by the server.
     958        In particular, a non-existent path usually causes a `PermanentError`.
    974959        """
    975960        path = ftputil.tool.as_str_path(path)
     
    991976    #
    992977    def __enter__(self):
    993         # Return `self`, so it can be accessed as the variable
    994         # component of the `with` statement.
     978        # Return `self`, so it can be accessed as the variable component of the
     979        # `with` statement.
    995980        return self
    996981
  • ftputil/path.py

    r1900 r1935  
    1515
    1616
    17 # The `_Path` class shouldn't be used directly by clients of the
    18 # ftputil library.
     17# The `_Path` class shouldn't be used directly by clients of the ftputil
     18# library.
    1919__all__ = []
    2020
     
    2222class _Path:
    2323    """
    24     Support class resembling `os.path`, accessible from the `FTPHost`
    25     object, e. g. as `FTPHost().path.abspath(path)`.
     24    Support class resembling `os.path`, accessible from the `FTPHost` object,
     25    e. g. as `FTPHost().path.abspath(path)`.
    2626
    2727    Hint: substitute `os` with the `FTPHost` object.
     
    5050
    5151    def abspath(self, path):
    52         """Return an absolute path."""
     52        """
     53        Return an absolute path.
     54        """
    5355        original_path = path
    5456        path = ftputil.tool.as_str_path(path)
     
    6062
    6163    def exists(self, path):
    62         """Return true if the path exists."""
     64        """
     65        Return true if the path exists.
     66        """
    6367        try:
    6468            lstat_result = self._host.lstat(path, _exception_for_missing_path=False)
     
    6973    def getmtime(self, path):
    7074        """
    71         Return the timestamp for the last modification for `path`
    72         as a float.
    73 
    74         This will raise `PermanentError` if the path doesn't exist,
    75         but maybe other exceptions depending on the state of the
    76         server (e. g. timeout).
     75        Return the timestamp for the last modification for `path` as a float.
     76
     77        This will raise `PermanentError` if the path doesn't exist, but maybe
     78        other exceptions depending on the state of the server (e. g. timeout).
    7779        """
    7880        return self._host.stat(path).st_mtime
     
    8284        Return the size of the `path` item as an integer.
    8385
    84         This will raise `PermanentError` if the path doesn't exist,
    85         but maybe raise other exceptions depending on the state of the
    86         server (e. g. timeout).
     86        This will raise `PermanentError` if the path doesn't exist, but maybe
     87        raise other exceptions depending on the state of the server (e. g.
     88        timeout).
    8789        """
    8890        return self._host.stat(path).st_size
    8991
    90     # Check whether a path is a regular file/dir/link. For the first
    91     # two cases follow links (like in `os.path`).
     92    # Check whether a path is a regular file/dir/link. For the first two cases
     93    # follow links (like in `os.path`).
    9294    #
    93     # Implementation note: The previous implementations simply called
    94     # `stat` or `lstat` and returned `False` if they ended with
    95     # raising a `PermanentError`. That exception usually used to
    96     # signal a missing path. This approach has the problem, however,
    97     # that exceptions caused by code earlier in `lstat` are obscured
    98     # by the exception handling in `isfile`, `isdir` and `islink`.
     95    # Implementation note: The previous implementations simply called `stat` or
     96    # `lstat` and returned `False` if they ended with raising a
     97    # `PermanentError`. That exception usually used to signal a missing path.
     98    # This approach has the problem, however, that exceptions caused by code
     99    # earlier in `lstat` are obscured by the exception handling in `isfile`,
     100    # `isdir` and `islink`.
    99101
    100102    def _is_file_system_entity(self, path, dir_or_file):
    101103        """
    102         Return `True` if `path` represents the file system entity
    103         described by `dir_or_file` ("dir" or "file").
    104 
    105         Return `False` if `path` isn't a directory or file,
    106         respectively or if `path` leads to an infinite chain of links.
     104        Return `True` if `path` represents the file system entity described by
     105        `dir_or_file` ("dir" or "file").
     106
     107        Return `False` if `path` isn't a directory or file, respectively or if
     108        `path` leads to an infinite chain of links.
    107109        """
    108110        assert dir_or_file in ["dir", "file"]
     
    116118        #
    117119        path = ftputil.tool.as_str_path(path)
    118         #  Workaround if we can't go up from the current directory.
    119         #  The result from `getcwd` should already be normalized.
     120        #  Workaround if we can't go up from the current directory. The result
     121        #  from `getcwd` should already be normalized.
    120122        if self.normpath(path) == self._host.getcwd():
    121123            return should_look_for_dir
     
    135137    def isdir(self, path):
    136138        """
    137         Return true if the `path` exists and corresponds to a
    138         directory (no link).
    139 
    140         A non-existing path does _not_ cause a `PermanentError`,
    141         instead return `False`.
     139        Return true if the `path` exists and corresponds to a directory (no
     140        link).
     141
     142        A non-existing path does _not_ cause a `PermanentError`, instead return
     143        `False`.
    142144        """
    143145        return self._is_file_system_entity(path, "dir")
     
    145147    def isfile(self, path):
    146148        """
    147         Return true if the `path` exists and corresponds to a regular
    148         file (no link).
    149 
    150         A non-existing path does _not_ cause a `PermanentError`,
    151         instead return `False`.
     149        Return true if the `path` exists and corresponds to a regular file (no
     150        link).
     151
     152        A non-existing path does _not_ cause a `PermanentError`, instead return
     153        `False`.
    152154        """
    153155        return self._is_file_system_entity(path, "file")
     
    157159        Return true if the `path` exists and is a link.
    158160
    159         A non-existing path does _not_ cause a `PermanentError`,
    160         instead return `False`.
     161        A non-existing path does _not_ cause a `PermanentError`, instead return
     162        `False`.
    161163        """
    162164        path = ftputil.tool.as_str_path(path)
     
    191193        """
    192194        top = ftputil.tool.as_str_path(top)
    193         # This code (and the above documentation) is taken from
    194         # `posixpath.py`, with slight modifications.
     195        # This code (and the above documentation) is taken from `posixpath.py`,
     196        # with slight modifications.
    195197        try:
    196198            names = self._host.listdir(top)
  • ftputil/session.py

    r1894 r1935  
    44
    55"""
    6 Session factory factory (the two "factory" are intentional :-) )
    7 for ftputil.
     6Session factory factory (the two "factory" are intentional :-) ) for ftputil.
    87"""
    98
     
    1514
    1615# In a way, it would be appropriate to call this function
    17 # `session_factory_factory`, but that's cumbersome to use. Think of
    18 # the function returning a session factory and the shorter name should
    19 # be fine.
     16# `session_factory_factory`, but that's cumbersome to use. Think of the
     17# function returning a session factory and the shorter name should be fine.
    2018def session_factory(
    2119    base_class=ftplib.FTP,
     
    2725):
    2826    """
    29     Create and return a session factory according to the keyword
    30     arguments.
     27    Create and return a session factory according to the keyword arguments.
    3128
    32     base_class: Base class to use for the session class (e. g.
    33     `ftplib.FTP_TLS` or `M2Crypto.ftpslib.FTP_TLS`, default is
    34     `ftplib.FTP`).
     29    base_class: Base class to use for the session class (e. g. `ftplib.FTP_TLS`
     30    or `M2Crypto.ftpslib.FTP_TLS`, default is `ftplib.FTP`).
    3531
    36     port: Port number (integer) for the command channel (default 21).
    37     If you don't know what "command channel" means, use the default or
    38     use what the provider gave you as "the FTP port".
     32    port: Port number (integer) for the command channel (default 21). If you
     33    don't know what "command channel" means, use the default or use what the
     34    provider gave you as "the FTP port".
    3935
    40     use_passive_mode: If `True`, explicitly use passive mode. If
    41     `False`, explicitly don't use passive mode. If `None` (default),
    42     let the `base_class` decide whether it wants to use active or
    43     passive mode.
     36    use_passive_mode: If `True`, explicitly use passive mode. If `False`,
     37    explicitly don't use passive mode. If `None` (default), let the
     38    `base_class` decide whether it wants to use active or passive mode.
    4439
    45     encrypt_data_channel: If `True` (the default), call the `prot_p`
    46     method of the base class if it has the method. If `False` or
    47     `None` (`None` is the default), don't call the method.
     40    encrypt_data_channel: If `True` (the default), call the `prot_p` method of
     41    the base class if it has the method. If `False` or `None` (`None` is the
     42    default), don't call the method.
    4843
    49     debug_level: Debug level (integer) to be set on a session
    50     instance. The default is `None`, meaning no debugging output.
     44    debug_level: Debug level (integer) to be set on a session instance. The
     45    default is `None`, meaning no debugging output.
    5146
    5247    This function should work for the base classes `ftplib.FTP`,
    53     `ftplib.FTP_TLS`. Other base classes should work if they use the
    54     same API as `ftplib.FTP`.
     48    `ftplib.FTP_TLS`. Other base classes should work if they use the same API
     49    as `ftplib.FTP`.
    5550
    5651    Usage example:
     
    6661
    6762    class Session(base_class):
    68         """Session factory class created by `session_factory`."""
     63        """
     64        Session factory class created by `session_factory`.
     65        """
    6966
    7067        def __init__(self, host, user, password):
     
    7471                self.set_debuglevel(debug_level)
    7572            self.login(user, password)
    76             # `set_pasv` can be called with `True` (causing passive
    77             # mode) or `False` (causing active mode).
     73            # `set_pasv` can be called with `True` (causing passive mode) or
     74            # `False` (causing active mode).
    7875            if use_passive_mode is not None:
    7976                self.set_pasv(use_passive_mode)
  • ftputil/stat.py

    r1917 r1935  
    2828class StatResult(tuple):
    2929    """
    30     Support class resembling a tuple like that returned from
    31     `os.(l)stat`.
     30    Support class resembling a tuple like that returned from `os.(l)stat`.
    3231    """
    3332
     
    6968
    7069    def __repr__(self):
    71         # "Invert" `_index_mapping` so that we can look up the names
    72         # for the tuple indices.
     70        # "Invert" `_index_mapping` so that we can look up the names for the
     71        # tuple indices.
    7372        index_to_name = dict((v, k) for k, v in self._index_mapping.items())
    7473        argument_strings = []
     
    236235        backward compatibility with custom parsers.
    237236
    238         The precision value takes into account that, for example, a
    239         time string like "May 26  2005" has only a precision of one
    240         day. This information is important for the `upload_if_newer`
    241         and `download_if_newer` methods in the `FTPHost` class.
     237        The precision value takes into account that, for example, a time string
     238        like "May 26  2005" has only a precision of one day. This information
     239        is important for the `upload_if_newer` and `download_if_newer` methods
     240        in the `FTPHost` class.
    242241
    243242        Times in Unix-style directory listings typically have one of these
     
    399398
    400399class UnixParser(Parser):
    401     """`Parser` class for Unix-specific directory format."""
     400    """
     401    `Parser` class for Unix-specific directory format.
     402    """
    402403
    403404    @staticmethod
     
    588589#
    589590class _Stat:
    590     """Methods for stat'ing directories, links and regular files."""
     591    """
     592    Methods for stat'ing directories, links and regular files.
     593    """
    591594
    592595    # pylint: disable=protected-access
  • ftputil/stat_cache.py

    r1848 r1935  
    2121    Implement an LRU (least-recently-used) cache.
    2222
    23     `StatCache` objects have an attribute `max_age`. After this
    24     duration after _setting_ it a cache entry will expire. For
    25     example, if you code
     23    `StatCache` objects have an attribute `max_age`. After this duration after
     24    _setting_ it a cache entry will expire. For example, if you code
    2625
    2726    my_cache = StatCache()
     
    2928    my_cache["/home"] = ...
    3029
    31     the value my_cache["/home"] can be retrieved for 10 seconds. After
    32     that, the entry will be treated as if it had never been in the
    33     cache and should be fetched again from the remote host.
     30    the value my_cache["/home"] can be retrieved for 10 seconds. After that,
     31    the entry will be treated as if it had never been in the cache and should
     32    be fetched again from the remote host.
    3433
    35     Note that the `__len__` method does no age tests and thus may
    36     include some or many already expired entries.
     34    Note that the `__len__` method does no age tests and thus may include some
     35    or many already expired entries.
    3736    """
    3837
     
    4847
    4948    def enable(self):
    50         """Enable storage of stat results."""
     49        """
     50        Enable storage of stat results.
     51        """
    5152        self._enabled = True
    5253
    5354    def disable(self):
    5455        """
    55         Disable the cache. Further storage attempts with `__setitem__`
    56         won't have any visible effect.
     56        Disable the cache. Further storage attempts with `__setitem__` won't
     57        have any visible effect.
    5758
    58         Disabling the cache only effects new storage attempts. Values
    59         stored before calling `disable` can still be retrieved unless
    60         disturbed by a `resize` command or normal cache expiration.
     59        Disabling the cache only effects new storage attempts. Values stored
     60        before calling `disable` can still be retrieved unless disturbed by a
     61        `resize` command or normal cache expiration.
    6162        """
    6263        # `_enabled` is set via calling `enable` in the constructor.
     
    6667    def resize(self, new_size):
    6768        """
    68         Set number of cache entries to the integer `new_size`.
    69         If the new size is smaller than the current cache size,
    70         relatively long-unused elements will be removed.
     69        Set number of cache entries to the integer `new_size`. If the new size
     70        is smaller than the current cache size, relatively long-unused elements
     71        will be removed.
    7172        """
    7273        self._cache.size = new_size
     
    7475    def _age(self, path):
    7576        """
    76         Return the age of a cache entry for `path` in seconds. If
    77         the path isn't in the cache, raise a `CacheMissError`.
     77        Return the age of a cache entry for `path` in seconds. If the path
     78        isn't in the cache, raise a `CacheMissError`.
    7879        """
    7980        try:
     
    8586
    8687    def clear(self):
    87         """Clear (invalidate) all cache entries."""
     88        """
     89        Clear (invalidate) all cache entries.
     90        """
    8891        self._cache.clear()
    8992
    9093    def invalidate(self, path):
    9194        """
    92         Invalidate the cache entry for the absolute `path` if present.
    93         After that, the stat result data for `path` can no longer be
    94         retrieved, as if it had never been stored.
     95        Invalidate the cache entry for the absolute `path` if present. After
     96        that, the stat result data for `path` can no longer be retrieved, as if
     97        it had never been stored.
    9598
    96         If no stat result for `path` is in the cache, do _not_
    97         raise an exception.
     99        If no stat result for `path` is in the cache, do _not_ raise an
     100        exception.
    98101        """
    99         # XXX: To be 100 % sure, this should be `host.sep`, but I
    100         # don't want to introduce a reference to the `FTPHost` object
    101         # for only that purpose.
     102        # XXX: To be 100 % sure, this should be `host.sep`, but I don't want to
     103        # introduce a reference to the `FTPHost` object for only that purpose.
    102104        assert path.startswith("/"), "{} must be an absolute path".format(path)
    103105        try:
     
    109111    def __getitem__(self, path):
    110112        """
    111         Return the stat entry for the `path`. If there's no stored
    112         stat entry or the cache is disabled, raise `CacheMissError`.
     113        Return the stat entry for the `path`. If there's no stored stat entry
     114        or the cache is disabled, raise `CacheMissError`.
    113115        """
    114116        if not self._enabled:
     
    121123            )
    122124        else:
    123             # XXX: I don't know if this may raise a `CacheMissError`
    124             # in case of race conditions. I prefer robust code.
     125            # XXX: I don't know if this may raise a `CacheMissError` in case of
     126            # race conditions. I prefer robust code.
    125127            try:
    126128                return self._cache[path]
     
    132134    def __setitem__(self, path, stat_result):
    133135        """
    134         Put the stat data for the absolute `path` into the cache,
    135         unless it's disabled.
     136        Put the stat data for the absolute `path` into the cache, unless it's
     137        disabled.
    136138        """
    137139        assert path.startswith("/")
     
    142144    def __contains__(self, path):
    143145        """
    144         Support for the `in` operator. Return a true value, if data
    145         for `path` is in the cache, else return a false value.
     146        Support for the `in` operator. Return a true value, if data for `path`
     147        is in the cache, else return a false value.
    146148        """
    147149        try:
     
    158160    def __len__(self):
    159161        """
    160         Return the number of entries in the cache. Note that this
    161         may include some (or many) expired entries.
     162        Return the number of entries in the cache. Note that this may include
     163        some (or many) expired entries.
    162164        """
    163165        return len(self._cache)
    164166
    165167    def __str__(self):
    166         """Return a string representation of the cache contents."""
     168        """
     169        Return a string representation of the cache contents.
     170        """
    167171        lines = []
    168172        for key in sorted(self._cache):
  • ftputil/sync.py

    r1848 r1935  
    3030class LocalHost:
    3131    """
    32     Provide an API for local directories and files so we can use the
    33     same code as for `FTPHost` instances.
     32    Provide an API for local directories and files so we can use the same code
     33    as for `FTPHost` instances.
    3434    """
    3535
    3636    def open(self, path, mode):
    3737        """
    38         Return a Python file object for file name `path`, opened in
    39         mode `mode`.
     38        Return a Python file object for file name `path`, opened in mode
     39        `mode`.
    4040        """
    4141        # This is the built-in `open` function, not `os.open`!
     
    4444    def time_shift(self):
    4545        """
    46         Return the time shift value (see methods `set_time_shift`
    47         and `time_shift` in class `FTPHost` for a definition). By
    48         definition, the value is zero for local file systems.
     46        Return the time shift value (see methods `set_time_shift` and
     47        `time_shift` in class `FTPHost` for a definition). By definition, the
     48        value is zero for local file systems.
    4949        """
    5050        return 0.0
     
    6464        Init the `FTPSyncer` instance.
    6565
    66         Each of `source` and `target` is either an `FTPHost` or a
    67         `LocalHost` object. The source and target directories, resp.
    68         have to be set with the `chdir` command before passing them
    69         in. The semantics is so that the items under the source
    70         directory will show up under the target directory after the
    71         synchronization (unless there's an error).
     66        Each of `source` and `target` is either an `FTPHost` or a `LocalHost`
     67        object. The source and target directories, resp. have to be set with
     68        the `chdir` command before passing them in. The semantics is so that
     69        the items under the source directory will show up under the target
     70        directory after the synchronization (unless there's an error).
    7271        """
    7372        self._source = source
     
    7675    def _mkdir(self, target_dir):
    7776        """
    78         Try to create the target directory `target_dir`. If it already
    79         exists, don't do anything. If the directory is present but
    80         it's actually a file, raise a `SyncError`.
     77        Try to create the target directory `target_dir`. If it already exists,
     78        don't do anything. If the directory is present but it's actually a
     79        file, raise a `SyncError`.
    8180        """
    8281        # TODO: Handle setting of target mtime according to source mtime
    8382        # (beware of rootdir anomalies; try to handle them as well).
    84         # print("Making", target_dir)
    8583        if self._target.path.isfile(target_dir):
    8684            raise ftputil.error.SyncError(
    8785                "target dir '{}' is actually a file".format(target_dir)
    8886            )
    89         # Deliberately use an `isdir` test instead of `try/except`. The
    90         #  latter approach might mask other errors we want to see, e. g.
    91         #  insufficient permissions.
     87        # Deliberately use an `isdir` test instead of `try/except`. The latter
     88        # approach might mask other errors we want to see, e. g. insufficient
     89        # permissions.
    9290        if not self._target.path.isdir(target_dir):
    9391            self._target.mkdir(target_dir)
     
    9593    def _sync_file(self, source_file, target_file):
    9694        # XXX: This duplicates code from `FTPHost._copyfileobj`. Maybe
    97         # implement the upload and download methods in terms of
    98         # `_sync_file`, or maybe not?
     95        # implement the upload and download methods in terms of `_sync_file`,
     96        # or maybe not?
    9997        # TODO: Handle `IOError`s
    10098        # TODO: Handle conditional copy
    10199        # TODO: Handle setting of target mtime according to source mtime
    102100        # (beware of rootdir anomalies; try to handle them as well).
    103         # print("Syncing", source_file, "->", target_file)
    104101        source = self._source.open(source_file, "rb")
    105102        try:
     
    114111    def _fix_sep_for_target(self, path):
    115112        """
    116         Return the string `path` with appropriate path separators for
    117         the target file system.
     113        Return the string `path` with appropriate path separators for the
     114        target file system.
    118115        """
    119116        return path.replace(self._source.sep, self._target.sep)
     
    121118    def _sync_tree(self, source_dir, target_dir):
    122119        """
    123         Synchronize the source and the target directory tree by
    124         updating the target to match the source as far as possible.
     120        Synchronize the source and the target directory tree by updating the
     121        target to match the source as far as possible.
    125122
    126123        Current limitations:
    127124        - _don't_ delete items which are on the target path but not on the
    128125          source path
    129         - files are always copied, the modification timestamps are not
    130           compared
     126        - files are always copied, the modification timestamps are not compared
    131127        - all files are copied in binary mode, never in ASCII/text mode
    132128        - incomplete error handling
     
    147143    def sync(self, source_path, target_path):
    148144        """
    149         Synchronize `source_path` and `target_path` (both are strings,
    150         each denoting a directory or file path), i. e. update the
    151         target path so that it's a copy of the source path.
     145        Synchronize `source_path` and `target_path` (both are strings, each
     146        denoting a directory or file path), i. e. update the target path so
     147        that it's a copy of the source path.
    152148
    153149        This method handles both directory trees and single files.
  • ftputil/tool.py

    r1900 r1935  
    1313
    1414
    15 # Encoding to convert between byte string and unicode string. This is
    16 # a "lossless" encoding: Strings can be encoded/decoded back and forth
    17 # without information loss or causing encoding-related errors. The
    18 # `ftplib` module under Python 3 also uses the "latin1" encoding
    19 # internally. It's important to use the same encoding here, so that users who
    20 # used `ftplib` to create FTP items with non-ASCII characters can access them
    21 # in the same way with ftputil.
     15# Encoding to convert between byte string and unicode string. This is a
     16# "lossless" encoding: Strings can be encoded/decoded back and forth without
     17# information loss or causing encoding-related errors. The `ftplib` module
     18# under Python 3 also uses the "latin1" encoding internally. It's important to
     19# use the same encoding here, so that users who used `ftplib` to create FTP
     20# items with non-ASCII characters can access them in the same way with ftputil.
    2221LOSSLESS_ENCODING = "latin1"
    2322
     
    2928
    3029    If the `type_source` and `string` don't have the same type, use
    31     `LOSSLESS_ENCODING` above to encode or decode, whatever operation is needed.
     30    `LOSSLESS_ENCODING` above to encode or decode, whatever operation is
     31    needed.
    3232    """
    3333    if isinstance(type_source, bytes) and isinstance(string, str):
     
    4141def as_str(string):
    4242    """
    43     Return the argument `string` converted to a unicode string if it's
    44     a `bytes` object. Otherwise just return the string.
     43    Return the argument `string` converted to a unicode string if it's a
     44    `bytes` object. Otherwise just return the string.
    4545
    4646    If `string` is neither `str` nor `bytes`, raise a `TypeError`.
     
    5656def as_str_path(path):
    5757    """
    58     Return the argument `path` converted to a unicode string if it's
    59     a `bytes` object. Otherwise just return the string.
     58    Return the argument `path` converted to a unicode string if it's a `bytes`
     59    object. Otherwise just return the string.
    6060
    61     Instead of passing a `bytes` or `str` object for `path`, you can
    62     pass a `PathLike` object that can be converted to a `bytes` or
    63     `str` object.
     61    Instead of passing a `bytes` or `str` object for `path`, you can pass a
     62    `PathLike` object that can be converted to a `bytes` or `str` object.
    6463
    65     If the `path` can't be converted to a `bytes` or `str`, a `TypeError`
    66     is raised.
     64    If the `path` can't be converted to a `bytes` or `str`, a `TypeError` is
     65    raised.
    6766    """
    6867    path = os.fspath(path)
  • test/scripted_session.py

    r1848 r1935  
    2929        # TODO: Describe how the comparison is made.
    3030        """
    31         Check the method name, args and kwargs from this `Call` object
    32         against the method name, args and kwargs from the system under test.
     31        Check the method name, args and kwargs from this `Call` object against
     32        the method name, args and kwargs from the system under test.
    3333
    3434        Raise an `AssertionError` if there's a mismatch.
     
    8686    "Scripted" `ftplib.FTP`-like class for testing.
    8787
    88     To avoid actual input/output over sockets or files, specify the
    89     values that should be returned by the class's methods.
    90 
    91     The class is instantiated with a `script` argument. This is a list
    92     of `Call` objects where each object specifies the name of the
    93     `ftplib.FTP` method that is expected to be called and what the
    94     method should return. If the value is an exception, it will be
    95     raised, not returned.
    96 
    97     In case the method returns a socket (like `transfercmd`), the
    98     return value to be specified in the `Call` instance is the content
    99     of the underlying socket file.
     88    To avoid actual input/output over sockets or files, specify the values that
     89    should be returned by the class's methods.
     90
     91    The class is instantiated with a `script` argument. This is a list of
     92    `Call` objects where each object specifies the name of the `ftplib.FTP`
     93    method that is expected to be called and what the method should return. If
     94    the value is an exception, it will be raised, not returned.
     95
     96    In case the method returns a socket (like `transfercmd`), the return value
     97    to be specified in the `Call` instance is the content of the underlying
     98    socket file.
    10099
    101100    The advantage of the approach of this class over the use of
    102     `unittest.mock.Mock` objects is that the sequence of calls is
    103     clearly visible. With `Mock` objects, the developer must keep in
    104     mind all the calls when specifying return values or side effects
    105     for the mock methods.
     101    `unittest.mock.Mock` objects is that the sequence of calls is clearly
     102    visible. With `Mock` objects, the developer must keep in mind all the calls
     103    when specifying return values or side effects for the mock methods.
    106104    """
    107105
     
    127125        # Always expect an entry for the constructor.
    128126        init_call = self._next_script_call("__init__")
    129         # The constructor isn't supposed to return anything. The only
    130         # reason to call it here is to raise an exception if that was
    131         # specified in the `script`.
     127        # The constructor isn't supposed to return anything. The only reason to
     128        # call it here is to raise an exception if that was specified in the
     129        # `script`.
    132130        init_call()
    133131
     
    219217class MultisessionFactory:
    220218    """
    221     Return a session factory using the scripted data from the given
    222     "scripts" for each consecutive call ("creation") of a factory.
     219    Return a session factory using the scripted data from the given "scripts"
     220    for each consecutive call ("creation") of a factory.
    223221
    224222    Example:
     
    228226
    229227    When the `session_factory` is "instantiated" for the first time by
    230     `FTPHost._make_session`, the factory object will use the behavior
    231     described by the script `script1`. When the `session_factory` is
    232     "instantiated" a second time, the factory object will use the
    233     behavior described by the script `script2`.
     228    `FTPHost._make_session`, the factory object will use the behavior described
     229    by the script `script1`. When the `session_factory` is "instantiated" a
     230    second time, the factory object will use the behavior described by the
     231    script `script2`.
    234232    """
    235233
  • test/test_base.py

    r1848 r1935  
    88
    99
    10 # Since `io.BytesIO` and `io.StringIO` are built-in, they can't be
    11 # patched with `unittest.mock.patch`. However, derived classes can be
    12 # mocked. Mocking is useful to test the arguments of `write` calls,
    13 # i. e. whether the expected data was written.
     10# Since `io.BytesIO` and `io.StringIO` are built-in, they can't be patched with
     11# `unittest.mock.patch`. However, derived classes can be mocked. Mocking is
     12# useful to test the arguments of `write` calls, i. e. whether the expected
     13# data was written.
    1414class MockableBytesIO(io.BytesIO):
    1515    pass
     
    2020
    2121
    22 # Factory to produce `FTPHost`-like classes from a given `FTPHost`
    23 # class and (usually) a given `MockSession` class.
     22# Factory to produce `FTPHost`-like classes from a given `FTPHost` class and
     23# (usually) a given `MockSession` class.
    2424def ftp_host_factory(session_factory, ftp_host_class=ftputil.FTPHost):
    2525    return ftp_host_class(
  • test/test_error.py

    r1901 r1935  
    1212class TestFTPErrorArguments:
    1313    """
    14     The `*Error` constructors should accept either a byte string or a
    15     unicode string.
     14    The `*Error` constructors should accept either a byte string or a unicode
     15    string.
    1616    """
    1717
     
    3333    def test_ftplib_error_to_ftp_os_error(self):
    3434        """
    35         Ensure the `ftplib` exception isn't used as `FTPOSError`
    36         argument.
     35        Ensure the `ftplib` exception isn't used as `FTPOSError` argument.
    3736        """
    3837        with pytest.raises(ftputil.error.FTPOSError) as exc_info:
     
    4544    def test_ftplib_error_to_ftp_io_error(self):
    4645        """
    47         Ensure the `ftplib` exception isn't used as `FTPIOError`
    48         argument.
     46        Ensure the `ftplib` exception isn't used as `FTPIOError` argument.
    4947        """
    5048        with pytest.raises(ftputil.error.FTPIOError) as exc_info:
     
    5755    def test_error_message_reuse(self):
    5856        """
    59         Test if the error message string is retained if the caught
    60         exception has more than one element in `args`.
     57        Test if the error message string is retained if the caught exception
     58        has more than one element in `args`.
    6159        """
    6260        # See ticket #76.
     
    6563            host = ftputil.FTPHost("localhost:21", "", "")
    6664        exc = exc_info.value
    67         # The error message may be different for different Python
    68         # versions.
     65        # The error message may be different for different Python versions.
    6966        assert "No address associated with hostname" in str(
    7067            exc
  • test/test_file.py

    r1858 r1935  
    2727
    2828    def test_inaccessible_dir(self):
    29         """Test whether opening a file at an invalid location fails."""
     29        """
     30        Test whether opening a file at an invalid location fails.
     31        """
    3032        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    3133        file_script = [
     
    4749
    4850    def test_caching_of_children(self):
    49         """Test whether `FTPFile` cache of `FTPHost` object works."""
     51        """
     52        Test whether `FTPFile` cache of `FTPHost` object works.
     53        """
    5054        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    5155        file1_script = [
     
    108112
    109113    def test_write_to_directory(self):
    110         """Test whether attempting to write to a directory fails."""
     114        """
     115        Test whether attempting to write to a directory fails.
     116        """
    111117        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    112118        file_script = [
     
    172178
    173179    def test_binary_read(self):
    174         """Read data from a binary file."""
     180        """
     181        Read data from a binary file.
     182        """
    175183        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    176184        file_script = [
     
    194202
    195203    def test_binary_write(self):
    196         """Write binary data with `write`."""
     204        """
     205        Write binary data with `write`.
     206        """
    197207        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    198208        file_script = [
     
    217227
    218228    def test_text_read(self):
    219         """Read text with plain `read`."""
     229        """
     230        Read text with plain `read`.
     231        """
    220232        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    221233        file_script = [
     
    254266
    255267    def test_text_write(self):
    256         """Write text with `write`."""
     268        """
     269        Write text with `write`.
     270        """
    257271        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    258272        file_script = [
     
    280294
    281295    def test_text_writelines(self):
    282         """Write text with `writelines`."""
     296        """
     297        Write text with `writelines`.
     298        """
    283299        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    284300        file_script = [
     
    313329
    314330    def test_binary_readline(self):
    315         """Read binary data with `readline`."""
     331        """
     332        Read binary data with `readline`.
     333        """
    316334        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    317335        file_script = [
     
    346364
    347365    def test_text_readline(self):
    348         """Read text with `readline`."""
     366        """
     367        Read text with `readline`.
     368        """
    349369        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    350370        file_script = [
     
    383403
    384404    def test_text_readlines(self):
    385         """Read text with `readlines`."""
     405        """
     406        Read text with `readlines`.
     407        """
    386408        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    387409        file_script = [
     
    471493
    472494    def test_read_unknown_file(self):
    473         """Test whether reading a file which isn't there fails."""
     495        """
     496        Test whether reading a file which isn't there fails.
     497        """
    474498        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    475499        file_script = [
     
    491515    def _failing_pwd(self, exception_class):
    492516        """
    493         Return a function that will be used instead of the
    494         `session.pwd` and will raise the exception
    495         `exception_to_raise`.
     517        Return a function that will be used instead of the `session.pwd` and
     518        will raise the exception `exception_to_raise`.
    496519        """
    497520
     
    537560            assert len(host._children) == 1
    538561            # Try to create a new file. Since `pwd` in
    539             # `FTPHost._available_child` raises an exception, a new
    540             # child session should be created.
     562            # `FTPHost._available_child` raises an exception, a new child
     563            # session should be created.
    541564            with host.open("/dummy2") as _:
    542565                pass
     
    545568    def test_pwd_with_error_temp(self):
    546569        """
    547         Test if an `error_temp` in `_session.pwd` skips the child
    548         session.
     570        Test if an `error_temp` in `_session.pwd` skips the child session.
    549571        """
    550572        self._test_with_pwd_error(ftplib.error_temp)
     
    552574    def test_pwd_with_error_reply(self):
    553575        """
    554         Test if an `error_reply` in `_session.pwd` skips the child
    555         session.
     576        Test if an `error_reply` in `_session.pwd` skips the child session.
    556577        """
    557578        self._test_with_pwd_error(ftplib.error_reply)
     
    559580    def test_pwd_with_OSError(self):
    560581        """
    561         Test if an `OSError` in `_session.pwd` skips the child
    562         session.
     582        Test if an `OSError` in `_session.pwd` skips the child session.
    563583        """
    564584        self._test_with_pwd_error(OSError)
     
    566586    def test_pwd_with_EOFError(self):
    567587        """
    568         Test if an `EOFError` in `_session.pwd` skips the child
    569         session.
     588        Test if an `EOFError` in `_session.pwd` skips the child session.
    570589        """
    571590        self._test_with_pwd_error(EOFError)
  • test/test_file_transfer.py

    r1926 r1935  
    2020
    2121class MockFile:
    22     """Class compatible with `LocalFile` and `RemoteFile`."""
     22    """
     23    Class compatible with `LocalFile` and `RemoteFile`.
     24    """
    2325
    2426    def __init__(self, mtime, mtime_precision):
     
    7981        day = 24 * hour
    8082        unknown = ftputil.stat.UNKNOWN_PRECISION
    81         # Define input arguments; modification datetimes are in seconds.
    82         # Fields are source datetime/precision, target datetime/precision,
    83         # expected comparison result.
     83        # Define input arguments; modification datetimes are in seconds. Fields
     84        # are source datetime/precision, target datetime/precision, expected
     85        # comparison result.
    8486        file_data = [
    8587            # Non-overlapping modification datetimes/precisions
     
    117119
    118120class FailingStringIO(io.BytesIO):
    119     """Mock class to test whether exceptions are passed on."""
     121    """
     122    Mock class to test whether exceptions are passed on.
     123    """
    120124
    121125    # Kind of nonsense; we just want to see this exception raised.
     
    128132class TestChunkwiseTransfer:
    129133    def _random_string(self, count):
    130         """Return a `BytesIO` object containing `count` "random" bytes."""
     134        """
     135        Return a `BytesIO` object containing `count` "random" bytes.
     136        """
    131137        ints = (random.randint(0, 255) for i in range(count))
    132138        return bytes(ints)
    133139
    134140    def test_chunkwise_transfer_without_remainder(self):
    135         """Check if we get four chunks with 256 Bytes each."""
     141        """
     142        Check if we get four chunks with 256 Bytes each.
     143        """
    136144        data = self._random_string(1024)
    137145        fobj = io.BytesIO(data)
     
    144152
    145153    def test_chunkwise_transfer_with_remainder(self):
    146         """Check if we get three chunks with 256 Bytes and one with 253."""
     154        """
     155        Check if we get three chunks with 256 Bytes and one with 253.
     156        """
    147157        data = self._random_string(1021)
    148158        fobj = io.BytesIO(data)
     
    155165
    156166    def test_chunkwise_transfer_with_exception(self):
    157         """Check if we see the exception raised during reading."""
     167        """
     168        Check if we see the exception raised during reading.
     169        """
    158170        data = self._random_string(1024)
    159171        fobj = FailingStringIO(data)
  • test/test_host.py

    r1934 r1935  
    5858    def test_open_and_close(self):
    5959        """
    60         Test if opening and closing an `FTPHost` object works as
    61         expected.
     60        Test if opening and closing an `FTPHost` object works as expected.
    6261        """
    6362        script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
     
    6867
    6968    def test_invalid_login(self):
    70         """Login to invalid host must fail."""
     69        """
     70        Login to invalid host must fail.
     71        """
    7172        script = [Call("__init__", result=ftplib.error_perm), Call("pwd", result="/")]
    7273        with pytest.raises(ftputil.error.FTPOSError):
     
    7980        script = [
    8081            Call("__init__"),
    81             # Deliberately return the current working directory with a
    82             # trailing slash to test if it's removed when stored in the
    83             # `FTPHost` instance.
     82            # Deliberately return the current working directory with a trailing
     83            # slash to test if it's removed when stored in the `FTPHost`
     84            # instance.
    8485            Call("pwd", result="/home/"),
    8586            Call("close"),
     
    9192class TestKeepAlive:
    9293    def test_succeeding_keep_alive(self):
    93         """Assume the connection is still alive."""
     94        """
     95        Assume the connection is still alive.
     96        """
    9497        script = [
    9598            Call("__init__"),
     
    103106
    104107    def test_failing_keep_alive(self):
    105         """Assume the connection has timed out, so `keep_alive` fails."""
     108        """
     109        Assume the connection has timed out, so `keep_alive` fails.
     110        """
    106111        script = [
    107112            Call("__init__"),
    108113            Call("pwd", result="/home"),
    109             # Simulate failing `pwd` call after the server closed the connection
    110             # due to a session timeout.
     114            # Simulate failing `pwd` call after the server closed the
     115            # connection due to a session timeout.
    111116            Call("pwd", result=ftplib.error_temp),
    112117            Call("close"),
     
    120125    class TrivialParser(ftputil.stat.Parser):
    121126        """
    122         An instance of this parser always returns the same result
    123         from its `parse_line` method. This is all we need to check
    124         if ftputil uses the set parser. No actual parsing code is
    125         required here.
     127        An instance of this parser always returns the same result from its
     128        `parse_line` method. This is all we need to check if ftputil uses the
     129        set parser. No actual parsing code is required here.
    126130        """
    127131
    128132        def __init__(self):
    129             # We can't use `os.stat("/home")` directly because we
    130             # later need the object's `_st_name` attribute, which
    131             # we can't set on a `os.stat` stat value.
     133            # We can't use `os.stat("/home")` directly because we later need
     134            # the object's `_st_name` attribute, which we can't set on a
     135            # `os.stat` stat value.
    132136            default_stat_result = ftputil.stat.StatResult(os.stat("/home"))
    133137            default_stat_result._st_name = "home"
     
    138142
    139143    def test_set_parser(self):
    140         """Test if the selected parser is used."""
     144        """
     145        Test if the selected parser is used.
     146        """
    141147        script = [
    142148            Call("__init__"),
     
    163169    def test_command_not_implemented_error(self):
    164170        """
    165         Test if we get the anticipated exception if a command isn't
    166         implemented by the server.
     171        Test if we get the anticipated exception if a command isn't implemented
     172        by the server.
    167173        """
    168174        script = [
     
    253259
    254260class TestUploadAndDownload:
    255     """Test upload and download."""
     261    """
     262    Test upload and download.
     263    """
    256264
    257265    def test_download(self, tmp_path):
    258         """Test mode download."""
     266        """
     267        Test mode download.
     268        """
    259269        remote_file_name = "dummy_name"
    260270        remote_file_content = b"dummy_content"
     
    326336    def test_conditional_upload_with_upload(self, tmp_path):
    327337        """
    328         If the target file is older or doesn't exist, the source file
    329         should be uploaded.
     338        If the target file is older or doesn't exist, the source file should be
     339        uploaded.
    330340        """
    331341        local_source = tmp_path / "test_source"
     
    383393    def test_conditional_download_without_target(self, tmp_path):
    384394        """
    385         Test conditional binary mode download when no target file
    386         exists.
     395        Test conditional binary mode download when no target file exists.
    387396        """
    388397        local_target = tmp_path / "test_target"
    389398        data = binary_data()
    390399        # Target does not exist, so download.
    391         #  There isn't a `dir` call to compare the datetimes of the
    392         #  remote and the target file because the local `exists` call
    393         #  for the local target returns `False` and the datetime
    394         #  comparison therefore isn't done.
     400        #  There isn't a `dir` call to compare the datetimes of the remote and
     401        #  the target file because the local `exists` call for the local target
     402        #  returns `False` and the datetime comparison therefore isn't done.
    395403        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    396404        file_script = [
     
    410418
    411419    def test_conditional_download_with_older_target(self, tmp_path):
    412         """Test conditional binary mode download with newer source file."""
     420        """
     421        Test conditional binary mode download with newer source file.
     422        """
    413423        local_target = tmp_path / "test_target"
    414424        # Make sure file exists for the timestamp comparison.
     
    416426        data = binary_data()
    417427        # Target is older, so download.
    418         #  Use a date in the future. That isn't realistic, but for the
    419         #  purpose of the test it's an easy way to make sure the source
    420         #  file is newer than the target file.
     428        #  Use a date in the future. That isn't realistic, but for the purpose
     429        #  of the test it's an easy way to make sure the source file is newer
     430        #  than the target file.
    421431        dir_result = test_base.dir_line(
    422432            mode_string="-rw-r--r--",
     
    449459
    450460    def test_conditional_download_with_newer_target(self, tmp_path):
    451         """Test conditional binary mode download with older source file."""
     461        """
     462        Test conditional binary mode download with older source file.
     463        """
    452464        local_target = tmp_path / "test_target"
    453465        # Make sure file exists for timestamp comparison.
    454466        local_target.touch()
    455467        data = binary_data()
    456         # Use date in the past, so the target file is newer and no
    457         # download happens.
     468        # Use date in the past, so the target file is newer and no download
     469        # happens.
    458470        dir_result = test_base.dir_line(
    459471            mode_string="-rw-r--r--",
     
    487499class TestTimeShift:
    488500
    489     # Helper mock class that frees us from setting up complicated
    490     # session scripts for the remote calls.
     501    # Helper mock class that frees us from setting up complicated session
     502    # scripts for the remote calls.
    491503    class _Path:
    492504        def split(self, path):
     
    516528
    517529    def test_rounded_time_shift(self):
    518         """Test if time shift is rounded correctly."""
     530        """
     531        Test if time shift is rounded correctly.
     532        """
    519533        script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    520534        multisession_factory = scripted_session.factory(script)
     
    541555
    542556    def test_assert_valid_time_shift(self):
    543         """Test time shift sanity checks."""
     557        """
     558        Test time shift sanity checks.
     559        """
    544560        script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    545561        multisession_factory = scripted_session.factory(script)
     
    556572            with pytest.raises(ftputil.error.TimeShiftError):
    557573                assert_time_shift(-25 * 3600)
    558             # Invalid time shift (too large deviation from 15-minute units
    559             # is unacceptable)
     574            # Invalid time shift (too large deviation from 15-minute units is
     575            # unacceptable)
    560576            with pytest.raises(ftputil.error.TimeShiftError):
    561577                assert_time_shift(8 * 60)
     
    564580
    565581    def test_synchronize_times(self):
    566         """Test time synchronization with server."""
     582        """
     583        Test time synchronization with server.
     584        """
    567585        host_script = [
    568586            Call("__init__"),
     
    620638
    621639    def test_synchronize_times_for_server_in_east(self):
    622         """Test for timestamp correction (see ticket #55)."""
     640        """
     641        Test for timestamp correction (see ticket #55).
     642        """
    623643        host_script = [
    624644            Call("__init__"),
     
    651671            # Set `mtime` to simulate a server east of us.
    652672            # In case the `time_shift` value for this host instance is 0.0
    653             # (as is to be expected before the time shift is determined),
    654             # the directory parser (more specifically
     673            # (as is to be expected before the time shift is determined), the
     674            # directory parser (more specifically
    655675            # `ftputil.stat.Parser.parse_unix_time`) will return a time which
    656             # is a year too far in the past. The `synchronize_times`
    657             # method needs to deal with this and add the year "back".
    658             # I don't think this is a bug in `parse_unix_time` because
    659             # the method should work once the time shift is set correctly.
     676            # is a year too far in the past. The `synchronize_times` method
     677            # needs to deal with this and add the year "back". I don't think
     678            # this is a bug in `parse_unix_time` because the method should work
     679            # once the time shift is set correctly.
    660680            client_time = datetime.datetime.utcnow().replace(
    661681                tzinfo=datetime.timezone.utc
     
    676696
    677697    def test_upload(self):
    678         """Test whether `upload` accepts either unicode or bytes."""
     698        """
     699        Test whether `upload` accepts either unicode or bytes.
     700        """
    679701        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    680702        file_script = [
     
    700722
    701723    def test_download(self, tmp_path):
    702         """Test whether `download` accepts either unicode or bytes."""
     724        """
     725        Test whether `download` accepts either unicode or bytes.
     726        """
    703727        local_target = tmp_path / "local_target"
    704728        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
     
    725749
    726750    def test_rename(self):
    727         """Test whether `rename` accepts either unicode or bytes."""
     751        """
     752        Test whether `rename` accepts either unicode or bytes.
     753        """
    728754        script = [
    729755            Call("__init__"),
     
    743769
    744770    def test_listdir(self):
    745         """Test whether `listdir` accepts either unicode or bytes."""
     771        """
     772        Test whether `listdir` accepts either unicode or bytes.
     773        """
    746774        top_level_dir_line = test_base.dir_line(
    747775            mode_string="drwxr-xr-x", date_=datetime.date.today(), name="ä"
     
    779807
    780808    def test_chmod(self):
    781         """Test whether `chmod` accepts either unicode or bytes."""
     809        """
     810        Test whether `chmod` accepts either unicode or bytes.
     811        """
    782812        script = [
    783813            Call("__init__"),
     
    812842
    813843    def test_chdir(self):
    814         """Test whether `chdir` accepts either unicode or bytes."""
     844        """
     845        Test whether `chdir` accepts either unicode or bytes.
     846        """
    815847        script = [
    816848            Call("__init__"),
     
    822854
    823855    def test_mkdir(self):
    824         """Test whether `mkdir` accepts either unicode or bytes."""
     856        """
     857        Test whether `mkdir` accepts either unicode or bytes.
     858        """
    825859        script = [
    826860            Call("__init__"),
     
    835869
    836870    def test_makedirs(self):
    837         """Test whether `makedirs` accepts either unicode or bytes."""
    838         script = [
    839             Call("__init__"),
    840             Call("pwd", result="/"),
    841             # To deal with ticket #86 (virtual directories), `makedirs` tries to
    842             # change into each directory and if it exists (changing doesn't raise
    843             # an exception), doesn't try to create it. That's why you don't see
    844             # an `mkd` calls here despite originally having a `makedirs` call.
     871        """
     872        Test whether `makedirs` accepts either unicode or bytes.
     873        """
     874        script = [
     875            Call("__init__"),
     876            Call("pwd", result="/"),
     877            # To deal with ticket #86 (virtual directories), `makedirs` tries
     878            # to change into each directory and if it exists (changing doesn't
     879            # raise an exception), doesn't try to create it. That's why you
     880            # don't see an `mkd` calls here despite originally having a
     881            # `makedirs` call.
    845882            Call("cwd", args=("/ä",)),
    846883            Call("cwd", args=("/ä/ö",)),
     
    851888
    852889    def test_rmdir(self):
    853         """Test whether `rmdir` accepts either unicode or bytes."""
     890        """
     891        Test whether `rmdir` accepts either unicode or bytes.
     892        """
    854893        dir_line = test_base.dir_line(
    855894            mode_string="drwxr-xr-x", date_=datetime.date.today(), name="empty_ä"
     
    903942
    904943    def test_remove(self):
    905         """Test whether `remove` accepts either unicode or bytes."""
     944        """
     945        Test whether `remove` accepts either unicode or bytes.
     946        """
    906947        dir_line = test_base.dir_line(
    907948            mode_string="-rw-r--r--", date_=datetime.date.today(), name="ö"
     
    923964
    924965    def test_rmtree(self):
    925         """Test whether `rmtree` accepts either unicode or bytes."""
     966        """
     967        Test whether `rmtree` accepts either unicode or bytes.
     968        """
    926969        dir_line = test_base.dir_line(
    927970            mode_string="drwxr-xr-x", date_=datetime.date.today(), name="empty_ä"
     
    9591002
    9601003    def test_lstat(self):
    961         """Test whether `lstat` accepts either unicode or bytes."""
     1004        """
     1005        Test whether `lstat` accepts either unicode or bytes.
     1006        """
    9621007        dir_line = test_base.dir_line(
    9631008            mode_string="-rw-r--r--", date_=datetime.date.today(), name="ä"
     
    9751020
    9761021    def test_stat(self):
    977         """Test whether `stat` accepts either unicode or bytes."""
     1022        """
     1023        Test whether `stat` accepts either unicode or bytes.
     1024        """
    9781025        dir_line = test_base.dir_line(
    9791026            mode_string="-rw-r--r--", date_=datetime.date.today(), name="ä"
     
    9911038
    9921039    def test_walk(self):
    993         """Test whether `walk` accepts either unicode or bytes."""
     1040        """
     1041        Test whether `walk` accepts either unicode or bytes.
     1042        """
    9941043        dir_line = test_base.dir_line(
    9951044            mode_string="-rw-r--r--", date_=datetime.date.today(), name="ä"
     
    10171066class TestFailingPickling:
    10181067    def test_failing_pickling(self):
    1019         """Test if pickling (intentionally) isn't supported."""
     1068        """
     1069        Test if pickling (intentionally) isn't supported.
     1070        """
    10201071        host_script = [Call("__init__"), Call("pwd", result="/"), Call("close")]
    10211072        file_script = [
  • test/test_path.py

    r1918 r1935  
    2727    """Test operations in `FTPHost.path`."""
    2828
    29     # TODO: Add unit tests for changes for ticket #113
    30     # (commits [b4c9b089b6b8] and [4027740cdd2d]).
     29    # TODO: Add unit tests for changes for ticket #113 (commits [b4c9b089b6b8]
     30    # and [4027740cdd2d]).
    3131    def test_regular_isdir_isfile_islink(self):
    32         """Test regular `FTPHost._Path.isdir/isfile/islink`."""
     32        """
     33        Test regular `FTPHost._Path.isdir/isfile/islink`.
     34        """
    3335        # Test a path which isn't there.
    3436        script = [
     
    195197
    196198    def test_workaround_for_spaces(self):
    197         """Test whether the workaround for space-containing paths is used."""
     199        """
     200        Test whether the workaround for space-containing paths is used.
     201        """
    198202        # Test a file name containing spaces.
    199203        test_file = "/home/dir with spaces/file with spaces"
     
    262266
    263267    def test_inaccessible_home_directory_and_whitespace_workaround(self):
    264         "Test combination of inaccessible home directory + whitespace in path."
     268        """
     269        Test combination of inaccessible home directory + whitespace in path.
     270        """
    265271        script = [
    266272            Call("__init__"),
     
    275281    def test_isdir_isfile_islink_with_dir_failure(self):
    276282        """
    277         Test failing `FTPHost._Path.isdir/isfile/islink` because of
    278         failing `_dir` call.
     283        Test failing `FTPHost._Path.isdir/isfile/islink` because of failing
     284        `_dir` call.
    279285        """
    280286        script = [
     
    301307    def test_isdir_isfile_with_infinite_link_chain(self):
    302308        """
    303         Test if `isdir` and `isfile` return `False` if they encounter
    304         an infinite link chain.
    305         """
    306         # `/home/bad_link` links to `/home/subdir/bad_link`, which
    307         # links back to `/home/bad_link` etc.
     309        Test if `isdir` and `isfile` return `False` if they encounter an
     310        infinite link chain.
     311        """
     312        # `/home/bad_link` links to `/home/subdir/bad_link`, which links back
     313        # to `/home/bad_link` etc.
    308314        dir_line1 = test_base.dir_line(
    309315            mode_string="dr-xr-xr-x", datetime_=datetime.datetime.now(), name="home"
     
    347353
    348354    def test_exists(self):
    349         """Test `FTPHost.path.exists`."""
     355        """
     356        Test `FTPHost.path.exists`.
     357        """
    350358        # Regular use of `exists`
    351359        dir_line1 = test_base.dir_line(
     
    413421
    414422    def test_methods_that_take_a_string_and_return_a_bool(self):
    415         """Test whether the methods accept byte and unicode strings."""
     423        """
     424        Test whether the methods accept byte and unicode strings.
     425        """
    416426        path_converter = self.path_converter
    417427        script = [
     
    528538    def test_getmtime(self):
    529539        """
    530         Test whether `FTPHost.path.getmtime` accepts byte and unicode
    531         paths.
     540        Test whether `FTPHost.path.getmtime` accepts byte and unicode paths.
    532541        """
    533542        path_converter = self.path_converter
     
    549558        ]
    550559        expected_mtime = now.timestamp()
    551         # We don't care about the _exact_ time, so don't bother with
    552         # timezone differences. Instead, do a simple sanity check.
     560        # We don't care about the _exact_ time, so don't bother with timezone
     561        # differences. Instead, do a simple sanity check.
    553562        day = 24 * 60 * 60  # seconds
    554563        mtime_makes_sense = (
     
    595604
    596605    def test_walk(self):
    597         """Test whether `FTPHost.path.walk` accepts bytes and unicode paths."""
     606        """
     607        Test whether `FTPHost.path.walk` accepts bytes and unicode paths.
     608        """
    598609        path_converter = self.path_converter
    599610        now = datetime.datetime.now()
  • test/test_public_servers.py

    r1848 r1935  
    1616def email_address():
    1717    """
    18     Return the email address used to identify the client to an
    19     FTP server.
     18    Return the email address used to identify the client to an FTP server.
    2019
    21     If the hostname is "warpy", use my (Stefan's) email address,
    22     else try to use the content of the `$EMAIL` environment variable.
    23     If that doesn't exist, use a dummy address.
     20    If the hostname is "warpy", use my (Stefan's) email address, else try to
     21    use the content of the `$EMAIL` environment variable. If that doesn't
     22    exist, use a dummy address.
    2423    """
    2524    hostname = socket.gethostname()
     
    4039def ftp_client_listing(server, directory):
    4140    """
    42     Log into the FTP server `server` using the command line client,
    43     then change to the `directory` and retrieve a listing with "dir".
     41    Log into the FTP server `server` using the command line client, then change
     42    to the `directory` and retrieve a listing with "dir".
     43
    4444    Return the list of items found as an `os.listdir` would return it.
    4545    """
     
    6969            name = parts[-1]
    7070        names.append(name)
    71     # Remove entries for current and parent directory since they
    72     # aren't included in the result of `FTPHost.listdir` either.
     71    # Remove entries for current and parent directory since they aren't
     72    # included in the result of `FTPHost.listdir` either.
    7373    names = [name for name in names if name not in (".", "..")]
    7474    return names
     
    7777class TestPublicServers:
    7878    """
    79     Get directory listings from various public FTP servers
    80     with a command line client and ftputil and compare both.
     79    Get directory listings from various public FTP servers with a command line
     80    client and ftputil and compare both.
    8181
    82     An important aspect is to test different "spellings" of
    83     the same directory. For example, to list the root directory
    84     which is usually set after login, use "" (nothing), ".",
    85     "/", "/.", "./.", "././", "..", "../.", "../.." etc.
     82    An important aspect is to test different "spellings" of the same directory.
     83    For example, to list the root directory which is usually set after login,
     84    use "" (nothing), ".", "/", "/.", "./.", "././", "..", "../.", "../.." etc.
    8685
    8786    The command line client `ftp` has to be in the path.
     
    9089    # Implementation note:
    9190    #
    92     # I (Stefan) implement the code so it works with Ubuntu's
    93     # client. Other clients may work or not. If you have problems
    94     # testing some other client, please send me a (small) patch.
    95     # Keep in mind that I don't plan supporting as many FTP
    96     # obscure commandline clients as servers. ;-)
     91    # I (Stefan) implement the code so it works with Ubuntu's client. Other
     92    # clients may work or not. If you have problems testing some other client,
     93    # please send me a (small) patch. Keep in mind that I don't plan supporting
     94    # as many FTP obscure commandline clients as servers. ;-)
    9795
    98     # List of pairs with server name and a directory "guaranteed
    99     # to exist" under the login directory which is assumed to be
    100     # the root directory.
     96    # List of pairs with server name and a directory "guaranteed to exist"
     97    # under the login directory which is assumed to be the root directory.
    10198    servers = [  # Posix format
    10299        ("ftp.de.debian.org", "debian"),
     
    105102        ("ftp.heise.de", "pub"),
    106103        # DOS/Microsoft format
    107         # Do you know any FTP servers that use Microsoft
    108         # format? `ftp.microsoft.com` doesn't seem to be
    109         # reachable anymore.
     104        # Do you know any FTP servers that use Microsoft format?
     105        # `ftp.microsoft.com` doesn't seem to be reachable anymore.
    110106    ]
    111107
    112     # This data structure contains the initial directories "." and
    113     # "DIR" (which will be replaced by a valid directory name for
    114     # each server). The list after the initial directory contains
    115     # paths that will be queried after changing into the initial
    116     # directory. All items in these lists are actually supposed to
    117     # yield the same directory contents.
     108    # This data structure contains the initial directories "." and "DIR" (which
     109    # will be replaced by a valid directory name for each server). The list
     110    # after the initial directory contains paths that will be queried after
     111    # changing into the initial directory. All items in these lists are
     112    # actually supposed to yield the same directory contents.
    118113    paths_table = [
    119114        (
     
    142137
    143138        Connect to the server `server`; if the string argument
    144         `initial_directory` has a true value, change to this
    145         directory. Then iterate over all strings in the sequence
    146         `paths`, comparing the results of a listdir call with the
    147         listing from the command line client.
     139        `initial_directory` has a true value, change to this directory. Then
     140        iterate over all strings in the sequence `paths`, comparing the results
     141        of a listdir call with the listing from the command line client.
    148142        """
    149143        canonical_names = ftp_client_listing(server, initial_directory)
     
    154148                path = path.replace("DIR", initial_directory)
    155149                # Make sure that we don't recycle directory entries, i. e.
    156                 # really repeatedly retrieve the directory contents
    157                 # (shouldn't happen anyway with the current implementation).
     150                # really repeatedly retrieve the directory contents (shouldn't
     151                # happen anyway with the current implementation).
    158152                host.stat_cache.clear()
    159153                names = host.listdir(path)
    160                 # Filter out "hidden" names since the FTP command line
    161                 # client won't include them in its listing either.
     154                # Filter out "hidden" names since the FTP command line client
     155                # won't include them in its listing either.
    162156                names = [
    163157                    name
     
    166160                        name.startswith(".")
    167161                        or
    168                         # The login directory of `ftp.microsoft.com`
    169                         # contains this "hidden" entry that ftputil
    170                         # finds but not the FTP command line client.
     162                        # The login directory of `ftp.microsoft.com` contains
     163                        # this "hidden" entry that ftputil finds but not the
     164                        # FTP command line client.
    171165                        name == "mscomtest"
    172166                    )
     
    184178        Test all servers in `self.servers`.
    185179
    186         For each server, get the listings for the login directory and
    187         one other directory which is known to exist. Use different
    188         "spellings" to retrieve each list via ftputil and compare with
    189         the results gotten with the command line client.
     180        For each server, get the listings for the login directory and one other
     181        directory which is known to exist. Use different "spellings" to
     182        retrieve each list via ftputil and compare with the results gotten with
     183        the command line client.
    190184        """
    191185        for server, actual_initial_directory in self.servers:
  • test/test_real_ftp.py

    r1928 r1935  
    55# Execute tests on a real FTP server (other tests use mock code).
    66#
    7 # This test writes some files and directories on the local client and
    8 # the remote server. You'll need write access in the login directory.
    9 # This test can take a few minutes because it has to wait to test the
    10 # timezone calculation.
     7# These tests write some files and directories on the local client and the
     8# remote server. You'll need write access in the login directory. These tests
     9# can take a few minutes because they have to wait to test the timezone
     10# calculation.
    1111
    1212import datetime
     
    5757class Cleaner:
    5858    """
    59     This class helps remove directories and files which might
    60     otherwise be left behind if a test fails in unexpected ways.
     59    This class helps remove directories and files which might otherwise be left
     60    behind if a test fails in unexpected ways.
    6161    """
    6262
     
    6868
    6969    def add_dir(self, path):
    70         """Schedule a directory with path `path` for removal."""
     70        """
     71        Schedule a directory with path `path` for removal.
     72        """
    7173        self._ftp_items.append(("d", self._host.path.abspath(path)))
    7274
    7375    def add_file(self, path):
    74         """Schedule a file with path `path` for removal."""
     76        """
     77        Schedule a file with path `path` for removal.
     78        """
    7579        self._ftp_items.append(("f", self._host.path.abspath(path)))
    7680
    7781    def clean(self):
    7882        """
    79         Remove the directories and files previously remembered.
    80         The removal works in reverse order of the scheduling with
    81         `add_dir` and `add_file`.
     83        Remove the directories and files previously remembered. The removal
     84        works in reverse order of the scheduling with `add_dir` and `add_file`.
    8285
    8386        Errors due to a removal are ignored.
     
    8790            try:
    8891                if type_ == "d":
    89                     # If something goes wrong in `rmtree` we might
    90                     # leave a mess behind.
     92                    # If something goes wrong in `rmtree` we might leave a mess
     93                    # behind.
    9194                    self._host.rmtree(path)
    9295                elif type_ == "f":
     
    115118        self.cleaner.add_file(path)
    116119        with self.host.open(path, "wb") as file_:
    117             # Write something. Otherwise the FTP server might not update
    118             # the time of last modification if the file existed before.
     120            # Write something. Otherwise the FTP server might not update the
     121            # time of last modification if the file existed before.
    119122            file_.write(b"\n")
    120123
    121124    def make_local_file(self):
    122         """Create a file on the local host (= on the client side)."""
     125        """
     126        Create a file on the local host (= on the client side).
     127        """
    123128        with open("_local_file_", "wb") as fobj:
    124129            fobj.write(b"abc\x12\x34def\t")
     
    233238        host = self.host
    234239        self.cleaner.add_dir("rootdir2/dir2")
    235         # Preparation: `rootdir2` exists but is only writable by root.
    236         # `dir2` is writable by regular ftp users. Both directories
    237         # below should work.
     240        # Preparation: `rootdir2` exists but is only writable by root. `dir2`
     241        # is writable by regular ftp users. Both directories below should work.
    238242        host.makedirs("rootdir2/dir2")
    239243        host.makedirs("rootdir2/dir2/dir3")
     
    330334
    331335    def _walk_test(self, expected_result, **walk_kwargs):
    332         """Walk the directory and test results."""
     336        """
     337        Walk the directory and test results.
     338        """
    333339        # Collect data using `walk`.
    334340        actual_result = []
     
    466472    def test_issomething_for_nonexistent_directory(self):
    467473        host = self.host
    468         # Check if we get the right results if even the containing
    469         # directory doesn't exist (see ticket #66).
     474        # Check if we get the right results if even the containing directory
     475        # doesn't exist (see ticket #66).
    470476        nonexistent_path = "/nonexistent/nonexistent"
    471477        assert not host.path.isdir(nonexistent_path)
     
    503509
    504510    def test_cache_auto_resizing(self):
    505         """Test if the cache is resized appropriately."""
     511        """
     512        Test if the cache is resized appropriately.
     513        """
    506514        host = self.host
    507515        cache = host.stat_cache._cache
     
    512520        cache.size = 2
    513521        entries = host.listdir("walk_test")
    514         # The adjusted cache size should be larger or equal to the
    515         # number of items in `walk_test` and its parent directory. The
    516         # latter is read implicitly upon `listdir`'s `isdir` call.
     522        # The adjusted cache size should be larger or equal to the number of
     523        # items in `walk_test` and its parent directory. The latter is read
     524        # implicitly upon `listdir`'s `isdir` call.
    517525        expected_min_cache_size = max(len(host.listdir(host.curdir)), len(entries))
    518526        assert cache.size >= expected_min_cache_size
     
    520528
    521529class TestUploadAndDownload(RealFTPTest):
    522     """Test upload and download (including time shift test)."""
     530    """
     531    Test upload and download (including time shift test).
     532    """
    523533
    524534    def test_time_shift(self):
     
    534544        # Make local file to upload.
    535545        self.make_local_file()
    536         # Wait, else small time differences between client and server
    537         # actually could trigger the update.
     546        # Wait, else small time differences between client and server actually
     547        # could trigger the update.
    538548        time.sleep(65)
    539549        try:
     
    564574        assert downloaded is True
    565575        try:
    566             # If the remote file, taking the datetime precision into
    567             # account, _might_ be newer, the file will be downloaded
    568             # again. To prevent this, wait a bit over a minute (the
    569             # remote precision), then "touch" the local file.
     576            # If the remote file, taking the datetime precision into account,
     577            # _might_ be newer, the file will be downloaded again. To prevent
     578            # this, wait a bit over a minute (the remote precision), then
     579            # "touch" the local file.
    570580            time.sleep(65)
    571581            # Create empty file.
     
    577587            # Re-make the remote file.
    578588            self.make_remote_file(remote_file)
    579             # Local file is present but possibly older (taking the
    580             # possible deviation because of the precision into account),
    581             # so should download.
     589            # Local file is present but possibly older (taking the possible
     590            # deviation because of the precision into account), so should
     591            # download.
    582592            downloaded = host.download_if_newer(remote_file, local_file)
    583593            assert downloaded is True
     
    674684    def assert_mode(self, path, expected_mode):
    675685        """
    676         Return an integer containing the allowed bits in the mode
    677         change command.
     686        Return an integer containing the allowed bits in the mode change
     687        command.
    678688
    679689        The `FTPHost` object to test against is `self.host`.
    680690        """
    681691        full_mode = self.host.stat(path).st_mode
    682         # Remove flags we can't set via `chmod`.
    683         # Allowed flags according to Python documentation
    684         # https://docs.python.org/library/stat.html
     692        # Remove flags we can't set via `chmod`. Allowed flags according to
     693        # Python documentation: https://docs.python.org/library/stat.html
    685694        allowed_flags = [
    686695            stat.S_ISUID,
     
    767776    def test_for_reading(self):
    768777        """
    769         If a `rest` argument is passed to `open`, the following read
    770         operation should start at the byte given by `rest`.
     778        If a `rest` argument is passed to `open`, the following read operation
     779        should start at the byte given by `rest`.
    771780        """
    772781        with self.host.open(self.TEST_FILE_NAME, "rb", rest=3) as fobj:
     
    776785    def test_for_writing(self):
    777786        """
    778         If a `rest` argument is passed to `open`, the following write
    779         operation should start writing at the byte given by `rest`.
     787        If a `rest` argument is passed to `open`, the following write operation
     788        should start writing at the byte given by `rest`.
    780789        """
    781790        with self.host.open(self.TEST_FILE_NAME, "wb", rest=3) as fobj:
     
    787796    def test_invalid_read_from_text_file(self):
    788797        """
    789         If the `rest` argument is used for reading from a text file,
    790         a `CommandNotImplementedError` should be raised.
     798        If the `rest` argument is used for reading from a text file, a
     799        `CommandNotImplementedError` should be raised.
    791800        """
    792801        with pytest.raises(ftputil.error.CommandNotImplementedError):
     
    795804    def test_invalid_write_to_text_file(self):
    796805        """
    797         If the `rest` argument is used for reading from a text file,
    798         a `CommandNotImplementedError` should be raised.
     806        If the `rest` argument is used for reading from a text file, a
     807        `CommandNotImplementedError` should be raised.
    799808        """
    800809        with pytest.raises(ftputil.error.CommandNotImplementedError):
    801810            self.host.open(self.TEST_FILE_NAME, "w", rest=3)
    802811
    803     # There are no tests for reading and writing beyond the end of a
    804     # file. For example, if the remote file is 10 bytes long and
    805     # `open(remote_file, "rb", rest=100)` is used, the server may
    806     # return an error status code or not.
     812    # There are no tests for reading and writing beyond the end of a file. For
     813    # example, if the remote file is 10 bytes long and
     814    # `open(remote_file, "rb", rest=100)` is used, the server may return an
     815    # error status code or not.
    807816    #
    808     # The server I use for testing returns a 554 status when
    809     # attempting to _read_ beyond the end of the file. On the other
    810     # hand, if attempting to _write_ beyond the end of the file, the
    811     # server accepts the request, but starts writing after the end of
    812     # the file, i. e. appends to the file.
     817    # The server I use for testing returns a 554 status when attempting to
     818    # _read_ beyond the end of the file. On the other hand, if attempting to
     819    # _write_ beyond the end of the file, the server accepts the request, but
     820    # starts writing after the end of the file, i. e. appends to the file.
    813821    #
    814     # Instead of expecting certain responses that may differ between
    815     # server implementations, I leave the bahavior for too large
    816     # `rest` arguments undefined. In practice, this shouldn't be a
    817     # problem because the `rest` argument should only be used for
    818     # error recovery, and in this case a valid byte count for the
    819     # `rest` argument should be known.
     822    # Instead of expecting certain responses that may differ between server
     823    # implementations, I leave the bahavior for too large `rest` arguments
     824    # undefined. In practice, this shouldn't be a problem because the `rest`
     825    # argument should only be used for error recovery, and in this case a valid
     826    # byte count for the `rest` argument should be known.
    820827
    821828
     
    828835        time.sleep(1)
    829836        # Depending on the FTP server, this might return a status code
    830         # unexpected by `ftplib` or block the socket connection until
    831         # a server-side timeout.
     837        # unexpected by `ftplib` or block the socket connection until a
     838        # server-side timeout.
    832839        file1.close()
    833840
     
    842849
    843850    def test_names_with_spaces(self):
    844         # Test if directories and files with spaces in their names
    845         # can be used.
     851        # Test if directories and files with spaces in their names can be used.
    846852        host = self.host
    847853        assert host.path.isdir("dir with spaces")
     
    856862
    857863    def test_synchronize_times_without_write_access(self):
    858         """Test failing synchronization because of non-writable directory."""
     864        """
     865        Test failing synchronization because of non-writable directory.
     866        """
    859867        host = self.host
    860868        # This isn't writable by the ftp account the tests are run under.
     
    865873    def test_listdir_with_non_ascii_byte_string(self):
    866874        """
    867         `listdir` should accept byte strings with non-ASCII
    868         characters and return non-ASCII characters in directory or
    869         file names.
     875        `listdir` should accept byte strings with non-ASCII characters and
     876        return non-ASCII characters in directory or file names.
    870877        """
    871878        host = self.host
     
    877884    def test_listdir_with_non_ascii_unicode_string(self):
    878885        """
    879         `listdir` should accept unicode strings with non-ASCII
    880         characters and return non-ASCII characters in directory or
    881         file names.
    882         """
    883         host = self.host
    884         # `ftplib` under Python 3 only works correctly if the unicode
    885         # strings are decoded from latin1.
     886        `listdir` should accept unicode strings with non-ASCII characters and
     887        return non-ASCII characters in directory or file names.
     888        """
     889        host = self.host
     890        # `ftplib` under Python 3 only works correctly if the unicode strings
     891        # are decoded from latin1.
    886892        path = "äbc".encode("UTF-8").decode("latin1")
    887893        names = host.listdir(path)
     
    896902        # Use some musical symbols. These are certainly not latin1.
    897903        path = "𝄞𝄢"
    898         # `UnicodeEncodeError` is also the exception that `ftplib`
    899         # raises if it gets a non-latin1 path.
     904        # `UnicodeEncodeError` is also the exception that `ftplib` raises if it
     905        # gets a non-latin1 path.
    900906        with pytest.raises(UnicodeEncodeError):
    901907            self.host.mkdir(path)
    902908
    903909    def test_list_a_option(self):
    904         # For this test to pass, the server must _not_ list "hidden"
    905         # files by default but instead only when the `LIST` `-a`
    906         # option is used.
     910        # For this test to pass, the server must _not_ list "hidden" files by
     911        # default but instead only when the `LIST` `-a` option is used.
    907912        host = self.host
    908913        assert not host.use_list_a_option
  • test/test_session.py

    r1848 r1935  
    1212class MockSession:
    1313    """
    14     Mock session base class to determine if all expected calls
    15     have happened.
     14    Mock session base class to determine if all expected calls have happened.
    1615    """
    1716
     
    4544class TestSessionFactory:
    4645    """
    47     Test if session factories created by
    48     `ftputil.session.session_factory` trigger the expected calls.
     46    Test if session factories created by `ftputil.session.session_factory`
     47    trigger the expected calls.
    4948    """
    5049
    5150    def test_defaults(self):
    52         """Test defaults (apart from base class)."""
     51        """
     52        Test defaults (apart from base class).
     53        """
    5354        factory = ftputil.session.session_factory(base_class=MockSession)
    5455        session = factory("host", "user", "password")
     
    5657
    5758    def test_different_port(self):
    58         """Test setting the command channel port with `port`."""
     59        """
     60        Test setting the command channel port with `port`.
     61        """
    5962        factory = ftputil.session.session_factory(base_class=MockSession, port=2121)
    6063        session = factory("host", "user", "password")
     
    6669    def test_use_passive_mode(self):
    6770        """
    68         Test explicitly setting passive/active mode with
    69         `use_passive_mode`.
     71        Test explicitly setting passive/active mode with `use_passive_mode`.
    7072        """
    7173        # Passive mode
     
    9193
    9294    def test_encrypt_data_channel(self):
    93         """Test request to call `prot_p` with `encrypt_data_channel`."""
     95        """
     96        Test request to call `prot_p` with `encrypt_data_channel`.
     97        """
    9498        # With encrypted data channel (default for encrypted session).
    9599        factory = ftputil.session.session_factory(base_class=EncryptedMockSession)
     
    118122
    119123    def test_debug_level(self):
    120         """Test setting the debug level on the session."""
     124        """
     125        Test setting the debug level on the session.
     126        """
    121127        factory = ftputil.session.session_factory(base_class=MockSession, debug_level=1)
    122128        session = factory("host", "user", "password")
  • test/test_stat.py

    r1917 r1935  
    540540
    541541    def test_time_shifts(self):
    542         """Test correct year depending on time shift value."""
     542        """
     543        Test correct year depending on time shift value.
     544        """
    543545        # 1. test: Client and server share the same time (UTC). This is true if
    544546        # the directory listing from the server is in UTC.
     
    572574
    573575    def test_repr(self):
    574         """Test if the `repr` result looks like a named tuple."""
     576        """
     577        Test if the `repr` result looks like a named tuple.
     578        """
    575579        script = [
    576580            Call("__init__"),
     
    597601
    598602    def test_failing_lstat(self):
    599         """Test whether `lstat` fails for a nonexistent path."""
     603        """
     604        Test whether `lstat` fails for a nonexistent path.
     605        """
    600606        # Directory with presumed file item doesn't exist.
    601607        script = [
     
    661667
    662668    def test_lstat_one_unix_file(self):
    663         """Test `lstat` for a file described in Unix-style format."""
     669        """
     670        Test `lstat` for a file described in Unix-style format.
     671        """
    664672        script = [
    665673            Call("__init__"),
     
    683691
    684692    def test_lstat_one_ms_file(self):
    685         """Test `lstat` for a file described in DOS-style format."""
     693        """
     694        Test `lstat` for a file described in DOS-style format.
     695        """
    686696        script = [
    687697            Call("__init__"),
     
    715725
    716726    def test_lstat_one_unix_dir(self):
    717         """Test `lstat` for a directory described in Unix-style format."""
     727        """
     728        Test `lstat` for a directory described in Unix-style format.
     729        """
    718730        script = [
    719731            Call("__init__"),
     
    757769
    758770    def test_lstat_one_ms_dir(self):
    759         """Test `lstat` for a directory described in DOS-style format."""
     771        """
     772        Test `lstat` for a directory described in DOS-style format.
     773        """
    760774        script = [
    761775            Call("__init__"),
     
    788802
    789803    def test_lstat_via_stat_module(self):
    790         """Test `lstat` indirectly via `stat` module."""
     804        """
     805        Test `lstat` indirectly via `stat` module.
     806        """
    791807        script = [
    792808            Call("__init__"),
     
    808824
    809825    def test_stat_following_link(self):
    810         """Test `stat` when invoked on a link."""
     826        """
     827        Test `stat` when invoked on a link.
     828        """
    811829        # Simple link
    812830        script = [
     
    904922    #
    905923    def test_parser_switching_with_permanent_error(self):
    906         """Test non-switching of parser format with `PermanentError`."""
     924        """
     925        Test non-switching of parser format with `PermanentError`.
     926        """
    907927        script = [
    908928            Call("__init__"),
     
    933953
    934954    def test_parser_switching_default_to_unix(self):
    935         """Test non-switching of parser format; stay with Unix."""
     955        """
     956        Test non-switching of parser format; stay with Unix.
     957        """
    936958        script = [
    937959            Call("__init__"),
     
    958980
    959981    def test_parser_switching_to_ms(self):
    960         """Test switching of parser from Unix to MS format."""
     982        """
     983        Test switching of parser from Unix to MS format.
     984        """
    961985        script = [
    962986            Call("__init__"),
     
    10161040
    10171041class TestListdir:
    1018     """Test `FTPHost.listdir`."""
     1042    """
     1043    Test `FTPHost.listdir`.
     1044    """
    10191045
    10201046    def test_failing_listdir(self):
    1021         """Test failing `FTPHost.listdir`."""
     1047        """
     1048        Test failing `FTPHost.listdir`.
     1049        """
    10221050        script = [
    10231051            Call("__init__"),
     
    10351063
    10361064    def test_succeeding_listdir(self):
    1037         """Test succeeding `FTPHost.listdir`."""
     1065        """
     1066        Test succeeding `FTPHost.listdir`.
     1067        """
    10381068        script = [
    10391069            Call("__init__"),
  • test/test_stat_cache.py

    r1848 r1935  
    5959
    6060    def test_max_age1(self):
    61         """Set expiration after setting a cache item."""
     61        """
     62        Set expiration after setting a cache item.
     63        """
    6264        self.cache["/path1"] = "test1"
    6365        # Expire after one second
     
    7274
    7375    def test_max_age2(self):
    74         """Set expiration before setting a cache item."""
     76        """
     77        Set expiration before setting a cache item.
     78        """
    7579        # Expire after one second
    7680        self.cache.max_age = 1
  • test/test_sync.py

    r1890 r1935  
    112112        local_root = ntpath.join("some", "directory")
    113113        syncer = ftputil.sync.Syncer(source, target)
    114         # If the following call raises any `AssertionError`s, the
    115         # test framework will catch them and show them.
     114        # If the following call raises any `AssertionError`s, the test
     115        # framework will catch them and show them.
    116116        syncer.sync(local_root, "not_used_by_ArgumentCheckingFTPHost")
  • test/test_tool.py

    r1900 r1935  
    2929    def _test_string(type_source, string, expected_result):
    3030        """
    31         Check if the result from `tool.same_string_type_as(type_source, string)`
    32         is the same as `expected_result`.
     31        Check if the result from `tool.same_string_type_as(type_source,
     32        string)` is the same as `expected_result`.
    3333
    3434        `type_source` must be a `bytes` or `str` object.
  • test/test_with_statement.py

    r1855 r1935  
    1616
    1717
    18 # Exception raised by client code, i. e. code using ftputil. Used to
    19 # test the behavior in case of client exceptions.
     18# Exception raised by client code, i. e. code using ftputil. Used to test the
     19# behavior in case of client exceptions.
    2020class ClientCodeException(Exception):
    2121    pass
     
    4646        """
    4747        script = [
    48             # Since `__init__` raises an exception, `pwd` isn't called. However,
    49             # `close` is called via the context manager.
     48            # Since `__init__` raises an exception, `pwd` isn't called.
     49            # However, `close` is called via the context manager.
    5050            Call(method_name="__init__", result=ftplib.error_perm),
    5151            Call(method_name="close"),
     
    5454            with test_base.ftp_host_factory(scripted_session.factory(script)) as host:
    5555                pass
    56         # We arrived here, that's fine. Because the `FTPHost` object
    57         # wasn't successfully constructed, the assignment to `host`
    58         # shouldn't have happened.
     56        # We arrived here, that's fine. Because the `FTPHost` object wasn't
     57        # successfully constructed, the assignment to `host` shouldn't have
     58        # happened.
    5959        assert "host" not in locals()
    6060
     
    127127            Call(method_name="cwd", result=None, args=("/",)),
    128128            Call(method_name="voidcmd", result=None, args=("TYPE I",)),
    129             # Raise exception. `voidresp` therefore won't be called, but `close`
    130             # will be called by the context manager.
     129            # Raise exception. `voidresp` therefore won't be called, but
     130            # `close` will be called by the context manager.
    131131            Call(
    132132                method_name="transfercmd",
     
    143143                with host.open("/inaccessible", "w") as fobj:
    144144                    pass
    145             # The file construction shouldn't have succeeded, so `fobj`
    146             # should be absent from the local namespace.
     145            # The file construction shouldn't have succeeded, so `fobj` should
     146            # be absent from the local namespace.
    147147            assert "fobj" not in locals()
    148148
Note: See TracChangeset for help on using the changeset viewer.