Changeset 2042:ddaa0b9bbaee


Ignore:
Timestamp:
Jan 28, 2021, 6:05:44 PM (10 months ago)
Author:
Stefan Schwarzer <sschwarzer@…>
Branch:
default
amend_source:
9bad712aa36da9aaea613f15ad98500d4e9dfbf5
Message:
Allow `encoding` argument only for `FTP` subclasses

If an explicit encoding, i. e. a `str` is passed to `session_factory`,
the `base_class` argument must be `ftplib.FTP` or a subclass of it.

The reason is that we already use a heuristic for Python 3.8 and
earlier vs. Python 3.9 and earlier. This heuristic may work or fail -
possibly silently - for classes that aren't a subclass of
`ftplib.FTP` (including `ftplib.FTP` itself).

With this check in place, the heuristic may look safe, but it actually
isn't if, for example, a class inherits from `ftplib.FTP` in Python
3.9, but overrides the constructor so that it doesn't support the
`encoding` argument anymore.

ticket: 143
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • ftputil/session.py

    r2034 r2042  
    4646    default), don't call the method.
    4747
    48     encoding: Encoding (str) to use for directory and file paths. Unicode paths
    49     will be encoded with this encoding. Bytes paths are assumed to be in this
    50     encoding. The default (equivalent to passing `None`) is to use the default
    51     encoding of the `base_class` argument. Note that this has changed from
    52     Python 3.8 to 3.9. In Python 3.8 and lower, the default encoding is
    53     "latin-1"; in Python 3.9, the default encoding is "utf-8". Therefore, if
    54     you want an encoding that's independent of the Python version, pass an
    55     explicit `encoding`.
     48    encoding: Encoding (str) to use for directory and file paths, or `None`.
     49    Unicode (`str`) paths will be encoded with this encoding. Bytes paths are
     50    assumed to be in this encoding. The default (equivalent to passing `None`)
     51    is to use the default encoding of the `base_class` argument. Note that this
     52    encoding has changed from Python 3.8 to 3.9.
     53
     54    In Python 3.8 and lower, the default path encoding is "latin-1"; in Python
     55    3.9, the default path encoding is "utf-8". Therefore, if you want an
     56    encoding that's independent of the Python version, pass an explicit
     57    `encoding`.
     58
     59    Using a non-`None` `encoding` is only supported if `base_class` is
     60    `ftplib.FTP` or a subclass of it.
    5661
    5762    debug_level: Debug level (integer) to be set on a session instance. The
     
    7277        ...
    7378    """
     79    if not isinstance(base_class, type):
     80        raise ValueError(f"`base_class` must be a class, but is {base_class!r}")
     81    if (encoding is not None) and (not issubclass(base_class, ftplib.FTP)):
     82        raise ValueError(
     83            f"`encoding` is only supported for `ftplib.FTP` and subclasses, "
     84            f"but base class is {base_class!r}"
     85        )
    7486
    7587    class Session(base_class):
     
    8799        # Python 3.9 is the first Python version to have a documented way to
    88100        # set a custom encoding (per instance).
     101        #
     102        # XXX: The following heuristic doesn't cover the case that we run under
     103        # Python 3.8 or earlier _and_ have a base class with an `encoding`
     104        # argument. Also, the heuristic will fail if we run under Python 3.9,
     105        # but have a base class that overrides the constructor so that it
     106        # doesn't support the `encoding` argument anymore.
    89107        def __init__(self, host, user, password):
    90108            if (
     
    105123                self.prot_p()
    106124
    107     # fmt: off
    108125    if (encoding is not None) and not ftputil.path_encoding.RUNNING_UNDER_PY39_AND_UP:
    109126        Session.encoding = encoding
    110     # fmt: on
    111127    return Session
  • test/test_session.py

    r2037 r2042  
    77"""
    88
     9import ftplib
    910import sys
    1011
     
    1415
    1516
    16 class MockSession:
     17# Inherit from `ftplib.FTP` to get past the subclass check in
     18# `ftputil.session.session_factory`.
     19class MockSession(ftplib.FTP):
    1720    """
    1821    Mock session base class to determine if all expected calls have happened.
Note: See TracChangeset for help on using the changeset viewer.