From 3e3152af40b82cdd1ad63aeae42b28a6a1cef59bc2e9d43339d474b26d9bacba Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Tue, 19 Sep 2023 05:15:05 +0000 Subject: [PATCH] - Add patch support-python-312.patch, support Python 3.12+ - Stop using greedy globs in %files - Switch to autosetup and pyproject macros - Remove Python 2 leftovers - Enable running the tests again OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-pyftpdlib?expand=0&rev=42 --- python-pyftpdlib.changes | 9 + python-pyftpdlib.spec | 72 ++-- support-python-312.patch | 896 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 934 insertions(+), 43 deletions(-) create mode 100644 support-python-312.patch diff --git a/python-pyftpdlib.changes b/python-pyftpdlib.changes index 40f4e70..e2a7be7 100644 --- a/python-pyftpdlib.changes +++ b/python-pyftpdlib.changes @@ -1,3 +1,12 @@ +------------------------------------------------------------------- +Tue Sep 19 05:14:22 UTC 2023 - Steve Kowalik + +- Add patch support-python-312.patch, support Python 3.12+ +- Stop using greedy globs in %files +- Switch to autosetup and pyproject macros +- Remove Python 2 leftovers +- Enable running the tests again + ------------------------------------------------------------------- Fri Apr 21 12:30:41 UTC 2023 - Dirk Müller diff --git a/python-pyftpdlib.spec b/python-pyftpdlib.spec index e022b1b..6e266e6 100644 --- a/python-pyftpdlib.spec +++ b/python-pyftpdlib.spec @@ -17,23 +17,24 @@ # -%{?!python_module:%define python_module() python-%{**} python3-%{**}} -%bcond_without python2 %{?sle15_python_module_pythons} Name: python-pyftpdlib Version: 1.5.7 Release: 0 Summary: Asynchronous FTP server library for Python License: MIT -Group: Development/Languages/Python URL: https://github.com/giampaolo/pyftpdlib/ Source: https://files.pythonhosted.org/packages/source/p/pyftpdlib/pyftpdlib-%{version}.tar.gz Source1: keycert.pem +# PATCH-FIX-UPSTREAM gh#giampaolo/pyftpdlib#605 +Patch0: support-python-312.patch +BuildRequires: %{python_module pip} BuildRequires: %{python_module psutil} BuildRequires: %{python_module pyOpenSSL} BuildRequires: %{python_module pysendfile} BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-pyOpenSSL @@ -41,13 +42,6 @@ Requires(post): update-alternatives Requires(postun):update-alternatives Recommends: python-pysendfile BuildArch: noarch -%if %{with python2} -BuildRequires: python-ipaddress -BuildRequires: python-mock -%endif -%ifpython2 -Requires: python-ipaddress -%endif %python_subpackages %description @@ -55,49 +49,40 @@ The Python FTP server library provides a high-level interface to write very asynchronous FTP servers with Python. %prep -%setup -q -n pyftpdlib-%{version} +%autosetup -p1 -n pyftpdlib-%{version} sed -i '1 {/env python/ d}' pyftpdlib/test/*.py pyftpdlib/_compat.py cp %{SOURCE1} pyftpdlib/test %build -%python_build +%pyproject_wheel %install -%python_install +%pyproject_install %python_clone -a %{buildroot}%{_bindir}/ftpbench %python_expand %fdupes %{buildroot}%{$python_sitelib} # Note: Do not remove tests. Other packages import them %check -## # Tests reported as randomly failing in 2016 against v1.5.0: -## # https://github.com/giampaolo/pyftpdlib/issues/386 -## # If they re-occur, please update the issue with backtraces, -## # and disable only related tests. -## donttest="(TestFtpStoreDataTLSMixin and test_rest_on_stor)" -## donttest+=" or (TestFtpStoreDataTLSMixin and test_stor_ascii)" -## # https://github.com/giampaolo/pyftpdlib/issues/550 -## donttest+=" or test_masquerade_address" -## ignorebuild="--ignore build" -## %%{python_expand # expand to python flavor, not to the binary name, then strip the trailing _ -## builddir=_build.$python_ -## ignorebuild+=" --ignore ${builddir%%_}" -## } -## cat > pytest.ini < pytest.ini < +Date: Thu, 3 Aug 2023 12:04:08 +0800 +Subject: [PATCH 1/4] copy asyncore.py and asynchat.py from python 3.11 + +--- + MANIFEST.in | 2 + + Makefile | 2 +- + pyftpdlib/_asynchat.py | 217 +++++++++++++++ + pyftpdlib/_asyncore.py | 587 +++++++++++++++++++++++++++++++++++++++++ + pyftpdlib/handlers.py | 7 +- + pyftpdlib/ioloop.py | 10 +- + 6 files changed, 821 insertions(+), 4 deletions(-) + create mode 100644 pyftpdlib/_asynchat.py + create mode 100644 pyftpdlib/_asyncore.py + +Index: pyftpdlib-1.5.7/MANIFEST.in +=================================================================== +--- pyftpdlib-1.5.7.orig/MANIFEST.in ++++ pyftpdlib-1.5.7/MANIFEST.in +@@ -35,6 +35,8 @@ include docs/tutorial.rst + include make.bat + include pyftpdlib/__init__.py + include pyftpdlib/__main__.py ++include pyftpdlib/_asynchat.py ++include pyftpdlib/_asyncore.py + include pyftpdlib/_compat.py + include pyftpdlib/authorizers.py + include pyftpdlib/filesystems.py +Index: pyftpdlib-1.5.7/pyftpdlib/_asynchat.py +=================================================================== +--- /dev/null ++++ pyftpdlib-1.5.7/pyftpdlib/_asynchat.py +@@ -0,0 +1,217 @@ ++# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp ++# Author: Sam Rushing ++ ++# ====================================================================== ++# Copyright 1996 by Sam Rushing ++# ++# All Rights Reserved ++# ++# Permission to use, copy, modify, and distribute this software and ++# its documentation for any purpose and without fee is hereby ++# granted, provided that the above copyright notice appear in all ++# copies and that both that copyright notice and this permission ++# notice appear in supporting documentation, and that the name of Sam ++# Rushing not be used in advertising or publicity pertaining to ++# distribution of the software without specific, written prior ++# permission. ++# ++# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, ++# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN ++# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR ++# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ++# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ++# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ++# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ++# ====================================================================== ++ ++from collections import deque ++ ++from . import _asyncore as asyncore ++ ++ ++class async_chat(asyncore.dispatcher): ++ ac_in_buffer_size = 65536 ++ ac_out_buffer_size = 65536 ++ use_encoding = 0 ++ encoding = 'latin-1' ++ ++ def __init__(self, sock=None, map=None): ++ self.ac_in_buffer = b'' ++ self.incoming = [] ++ self.producer_fifo = deque() ++ asyncore.dispatcher.__init__(self, sock, map) ++ ++ def collect_incoming_data(self, data): ++ raise NotImplementedError("must be implemented in subclass") ++ ++ def _collect_incoming_data(self, data): ++ self.incoming.append(data) ++ ++ def _get_data(self): ++ d = b''.join(self.incoming) ++ del self.incoming[:] ++ return d ++ ++ def found_terminator(self): ++ raise NotImplementedError("must be implemented in subclass") ++ ++ def set_terminator(self, term): ++ if isinstance(term, str) and self.use_encoding: ++ term = bytes(term, self.encoding) ++ elif isinstance(term, int) and term < 0: ++ raise ValueError('the number of received bytes must be positive') ++ self.terminator = term ++ ++ def get_terminator(self): ++ return self.terminator ++ ++ def handle_read(self): ++ ++ try: ++ data = self.recv(self.ac_in_buffer_size) ++ except BlockingIOError: ++ return ++ except OSError: ++ self.handle_error() ++ return ++ ++ if isinstance(data, str) and self.use_encoding: ++ data = bytes(str, self.encoding) ++ self.ac_in_buffer = self.ac_in_buffer + data ++ ++ while self.ac_in_buffer: ++ lb = len(self.ac_in_buffer) ++ terminator = self.get_terminator() ++ if not terminator: ++ # no terminator, collect it all ++ self.collect_incoming_data(self.ac_in_buffer) ++ self.ac_in_buffer = b'' ++ elif isinstance(terminator, int): ++ # numeric terminator ++ n = terminator ++ if lb < n: ++ self.collect_incoming_data(self.ac_in_buffer) ++ self.ac_in_buffer = b'' ++ self.terminator = self.terminator - lb ++ else: ++ self.collect_incoming_data(self.ac_in_buffer[:n]) ++ self.ac_in_buffer = self.ac_in_buffer[n:] ++ self.terminator = 0 ++ self.found_terminator() ++ else: ++ terminator_len = len(terminator) ++ index = self.ac_in_buffer.find(terminator) ++ if index != -1: ++ if index > 0: ++ self.collect_incoming_data(self.ac_in_buffer[:index]) ++ self.ac_in_buffer = self.ac_in_buffer[index + ++ terminator_len:] ++ self.found_terminator() ++ else: ++ index = find_prefix_at_end(self.ac_in_buffer, terminator) ++ if index: ++ if index != lb: ++ self.collect_incoming_data( ++ self.ac_in_buffer[:-index]) ++ self.ac_in_buffer = self.ac_in_buffer[-index:] ++ break ++ else: ++ self.collect_incoming_data(self.ac_in_buffer) ++ self.ac_in_buffer = b'' ++ ++ def handle_write(self): ++ self.initiate_send() ++ ++ def handle_close(self): ++ self.close() ++ ++ def push(self, data): ++ if not isinstance(data, (bytes, bytearray, memoryview)): ++ raise TypeError('data argument must be byte-ish (%r)', ++ type(data)) ++ sabs = self.ac_out_buffer_size ++ if len(data) > sabs: ++ for i in range(0, len(data), sabs): ++ self.producer_fifo.append(data[i:i + sabs]) ++ else: ++ self.producer_fifo.append(data) ++ self.initiate_send() ++ ++ def push_with_producer(self, producer): ++ self.producer_fifo.append(producer) ++ self.initiate_send() ++ ++ def readable(self): ++ return 1 ++ ++ def writable(self): ++ return self.producer_fifo or (not self.connected) ++ ++ def close_when_done(self): ++ self.producer_fifo.append(None) ++ ++ def initiate_send(self): ++ while self.producer_fifo and self.connected: ++ first = self.producer_fifo[0] ++ if not first: ++ del self.producer_fifo[0] ++ if first is None: ++ self.handle_close() ++ return ++ ++ obs = self.ac_out_buffer_size ++ try: ++ data = first[:obs] ++ except TypeError: ++ data = first.more() ++ if data: ++ self.producer_fifo.appendleft(data) ++ else: ++ del self.producer_fifo[0] ++ continue ++ ++ if isinstance(data, str) and self.use_encoding: ++ data = bytes(data, self.encoding) ++ ++ try: ++ num_sent = self.send(data) ++ except OSError: ++ self.handle_error() ++ return ++ ++ if num_sent: ++ if num_sent < len(data) or obs < len(first): ++ self.producer_fifo[0] = first[num_sent:] ++ else: ++ del self.producer_fifo[0] ++ return ++ ++ def discard_buffers(self): ++ # Emergencies only! ++ self.ac_in_buffer = b'' ++ del self.incoming[:] ++ self.producer_fifo.clear() ++ ++ ++class simple_producer: ++ ++ def __init__(self, data, buffer_size=512): ++ self.data = data ++ self.buffer_size = buffer_size ++ ++ def more(self): ++ if len(self.data) > self.buffer_size: ++ result = self.data[:self.buffer_size] ++ self.data = self.data[self.buffer_size:] ++ return result ++ else: ++ result = self.data ++ self.data = b'' ++ return result ++ ++ ++def find_prefix_at_end(haystack, needle): ++ ll = len(needle) - 1 ++ while ll and not haystack.endswith(needle[:ll]): ++ ll -= 1 ++ return ll +Index: pyftpdlib-1.5.7/pyftpdlib/_asyncore.py +=================================================================== +--- /dev/null ++++ pyftpdlib-1.5.7/pyftpdlib/_asyncore.py +@@ -0,0 +1,587 @@ ++# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp ++# Author: Sam Rushing ++ ++# ====================================================================== ++# Copyright 1996 by Sam Rushing ++# ++# All Rights Reserved ++# ++# Permission to use, copy, modify, and distribute this software and ++# its documentation for any purpose and without fee is hereby ++# granted, provided that the above copyright notice appear in all ++# copies and that both that copyright notice and this permission ++# notice appear in supporting documentation, and that the name of Sam ++# Rushing not be used in advertising or publicity pertaining to ++# distribution of the software without specific, written prior ++# permission. ++# ++# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, ++# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN ++# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR ++# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ++# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ++# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ++# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ++# ====================================================================== ++ ++ ++import os ++import select ++import socket ++import sys ++import time ++import warnings ++from errno import EAGAIN ++from errno import EALREADY ++from errno import EBADF ++from errno import ECONNABORTED ++from errno import ECONNRESET ++from errno import EINPROGRESS ++from errno import EINVAL ++from errno import EISCONN ++from errno import ENOTCONN ++from errno import EPIPE ++from errno import ESHUTDOWN ++from errno import EWOULDBLOCK ++from errno import errorcode ++ ++ ++_DISCONNECTED = frozenset( ++ {ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, EBADF}) ++ ++try: ++ socket_map ++except NameError: ++ socket_map = {} ++ ++ ++def _strerror(err): ++ try: ++ return os.strerror(err) ++ except (ValueError, OverflowError, NameError): ++ if err in errorcode: ++ return errorcode[err] ++ return "Unknown error %s" % err ++ ++ ++class ExitNow(Exception): ++ pass ++ ++ ++_reraised_exceptions = (ExitNow, KeyboardInterrupt, SystemExit) ++ ++ ++def read(obj): ++ try: ++ obj.handle_read_event() ++ except _reraised_exceptions: ++ raise ++ except Exception: ++ obj.handle_error() ++ ++ ++def write(obj): ++ try: ++ obj.handle_write_event() ++ except _reraised_exceptions: ++ raise ++ except Exception: ++ obj.handle_error() ++ ++ ++def _exception(obj): ++ try: ++ obj.handle_expt_event() ++ except _reraised_exceptions: ++ raise ++ except Exception: ++ obj.handle_error() ++ ++ ++def readwrite(obj, flags): ++ try: ++ if flags & select.POLLIN: ++ obj.handle_read_event() ++ if flags & select.POLLOUT: ++ obj.handle_write_event() ++ if flags & select.POLLPRI: ++ obj.handle_expt_event() ++ if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL): ++ obj.handle_close() ++ except OSError as e: ++ if e.errno not in _DISCONNECTED: ++ obj.handle_error() ++ else: ++ obj.handle_close() ++ except _reraised_exceptions: ++ raise ++ except Exception: ++ obj.handle_error() ++ ++ ++def poll(timeout=0.0, map=None): ++ if map is None: ++ map = socket_map ++ if map: ++ r = [] ++ w = [] ++ e = [] ++ for fd, obj in list(map.items()): ++ is_r = obj.readable() ++ is_w = obj.writable() ++ if is_r: ++ r.append(fd) ++ if is_w and not obj.accepting: ++ w.append(fd) ++ if is_r or is_w: ++ e.append(fd) ++ if [] == r == w == e: ++ time.sleep(timeout) ++ return ++ ++ r, w, e = select.select(r, w, e, timeout) ++ ++ for fd in r: ++ obj = map.get(fd) ++ if obj is None: ++ continue ++ read(obj) ++ ++ for fd in w: ++ obj = map.get(fd) ++ if obj is None: ++ continue ++ write(obj) ++ ++ for fd in e: ++ obj = map.get(fd) ++ if obj is None: ++ continue ++ _exception(obj) ++ ++ ++def poll2(timeout=0.0, map=None): ++ if map is None: ++ map = socket_map ++ if timeout is not None: ++ timeout = int(timeout * 1000) ++ pollster = select.poll() ++ if map: ++ for fd, obj in list(map.items()): ++ flags = 0 ++ if obj.readable(): ++ flags |= select.POLLIN | select.POLLPRI ++ if obj.writable() and not obj.accepting: ++ flags |= select.POLLOUT ++ if flags: ++ pollster.register(fd, flags) ++ ++ r = pollster.poll(timeout) ++ for fd, flags in r: ++ obj = map.get(fd) ++ if obj is None: ++ continue ++ readwrite(obj, flags) ++ ++ ++poll3 = poll2 # Alias for backward compatibility ++ ++ ++def loop(timeout=30.0, use_poll=False, map=None, count=None): ++ if map is None: ++ map = socket_map ++ ++ if use_poll and hasattr(select, 'poll'): ++ poll_fun = poll2 ++ else: ++ poll_fun = poll ++ ++ if count is None: ++ while map: ++ poll_fun(timeout, map) ++ ++ else: ++ while map and count > 0: ++ poll_fun(timeout, map) ++ count = count - 1 ++ ++ ++class dispatcher: ++ ++ debug = False ++ connected = False ++ accepting = False ++ connecting = False ++ closing = False ++ addr = None ++ ignore_log_types = frozenset({'warning'}) ++ ++ def __init__(self, sock=None, map=None): ++ if map is None: ++ self._map = socket_map ++ else: ++ self._map = map ++ ++ self._fileno = None ++ ++ if sock: ++ sock.setblocking(False) ++ self.set_socket(sock, map) ++ self.connected = True ++ try: ++ self.addr = sock.getpeername() ++ except OSError as err: ++ if err.errno in (ENOTCONN, EINVAL): ++ self.connected = False ++ else: ++ self.del_channel(map) ++ raise ++ else: ++ self.socket = None ++ ++ def __repr__(self): ++ status = [self.__class__.__module__ + ++ "." + self.__class__.__qualname__] ++ if self.accepting and self.addr: ++ status.append('listening') ++ elif self.connected: ++ status.append('connected') ++ if self.addr is not None: ++ try: ++ status.append('%s:%d' % self.addr) ++ except TypeError: ++ status.append(repr(self.addr)) ++ return '<%s at %#x>' % (' '.join(status), id(self)) ++ ++ def add_channel(self, map=None): ++ # self.log_info('adding channel %s' % self) ++ if map is None: ++ map = self._map ++ map[self._fileno] = self ++ ++ def del_channel(self, map=None): ++ fd = self._fileno ++ if map is None: ++ map = self._map ++ if fd in map: ++ del map[fd] ++ self._fileno = None ++ ++ def create_socket(self, family=socket.AF_INET, type=socket.SOCK_STREAM): ++ self.family_and_type = family, type ++ sock = socket.socket(family, type) ++ sock.setblocking(False) ++ self.set_socket(sock) ++ ++ def set_socket(self, sock, map=None): ++ self.socket = sock ++ self._fileno = sock.fileno() ++ self.add_channel(map) ++ ++ def set_reuse_addr(self): ++ try: ++ self.socket.setsockopt( ++ socket.SOL_SOCKET, socket.SO_REUSEADDR, ++ self.socket.getsockopt(socket.SOL_SOCKET, ++ socket.SO_REUSEADDR) | 1 ++ ) ++ except OSError: ++ pass ++ ++ def readable(self): ++ return True ++ ++ def writable(self): ++ return True ++ ++ def listen(self, num): ++ self.accepting = True ++ if os.name == 'nt' and num > 5: ++ num = 5 ++ return self.socket.listen(num) ++ ++ def bind(self, addr): ++ self.addr = addr ++ return self.socket.bind(addr) ++ ++ def connect(self, address): ++ self.connected = False ++ self.connecting = True ++ err = self.socket.connect_ex(address) ++ if err in (EINPROGRESS, EALREADY, EWOULDBLOCK) \ ++ or err == EINVAL and os.name == 'nt': ++ self.addr = address ++ return ++ if err in (0, EISCONN): ++ self.addr = address ++ self.handle_connect_event() ++ else: ++ raise OSError(err, errorcode[err]) ++ ++ def accept(self): ++ try: ++ conn, addr = self.socket.accept() ++ except TypeError: ++ return None ++ except OSError as why: ++ if why.errno in (EWOULDBLOCK, ECONNABORTED, EAGAIN): ++ return None ++ else: ++ raise ++ else: ++ return conn, addr ++ ++ def send(self, data): ++ try: ++ result = self.socket.send(data) ++ return result ++ except OSError as why: ++ if why.errno == EWOULDBLOCK: ++ return 0 ++ elif why.errno in _DISCONNECTED: ++ self.handle_close() ++ return 0 ++ else: ++ raise ++ ++ def recv(self, buffer_size): ++ try: ++ data = self.socket.recv(buffer_size) ++ if not data: ++ self.handle_close() ++ return b'' ++ else: ++ return data ++ except OSError as why: ++ if why.errno in _DISCONNECTED: ++ self.handle_close() ++ return b'' ++ else: ++ raise ++ ++ def close(self): ++ self.connected = False ++ self.accepting = False ++ self.connecting = False ++ self.del_channel() ++ if self.socket is not None: ++ try: ++ self.socket.close() ++ except OSError as why: ++ if why.errno not in (ENOTCONN, EBADF): ++ raise ++ ++ def log(self, message): ++ sys.stderr.write('log: %s\n' % str(message)) ++ ++ def log_info(self, message, type='info'): ++ if type not in self.ignore_log_types: ++ print('%s: %s' % (type, message)) # noqa ++ ++ def handle_read_event(self): ++ if self.accepting: ++ # accepting sockets are never connected, they "spawn" new ++ # sockets that are connected ++ self.handle_accept() ++ elif not self.connected: ++ if self.connecting: ++ self.handle_connect_event() ++ self.handle_read() ++ else: ++ self.handle_read() ++ ++ def handle_connect_event(self): ++ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) ++ if err != 0: ++ raise OSError(err, _strerror(err)) ++ self.handle_connect() ++ self.connected = True ++ self.connecting = False ++ ++ def handle_write_event(self): ++ if self.accepting: ++ return ++ ++ if not self.connected: ++ if self.connecting: ++ self.handle_connect_event() ++ self.handle_write() ++ ++ def handle_expt_event(self): ++ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) ++ if err != 0: ++ self.handle_close() ++ else: ++ self.handle_expt() ++ ++ def handle_error(self): ++ nil, t, v, tbinfo = compact_traceback() ++ try: ++ self_repr = repr(self) ++ except Exception: ++ self_repr = '<__repr__(self) failed for object at %0x>' % id(self) ++ ++ self.log_info( ++ 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( ++ self_repr, ++ t, ++ v, ++ tbinfo ++ ), ++ 'error' ++ ) ++ self.handle_close() ++ ++ def handle_expt(self): ++ self.log_info('unhandled incoming priority event', 'warning') ++ ++ def handle_read(self): ++ self.log_info('unhandled read event', 'warning') ++ ++ def handle_write(self): ++ self.log_info('unhandled write event', 'warning') ++ ++ def handle_connect(self): ++ self.log_info('unhandled connect event', 'warning') ++ ++ def handle_accept(self): ++ pair = self.accept() ++ if pair is not None: ++ self.handle_accepted(*pair) ++ ++ def handle_accepted(self, sock, addr): ++ sock.close() ++ self.log_info('unhandled accepted event', 'warning') ++ ++ def handle_close(self): ++ self.log_info('unhandled close event', 'warning') ++ self.close() ++ ++ ++class dispatcher_with_send(dispatcher): ++ ++ def __init__(self, sock=None, map=None): ++ dispatcher.__init__(self, sock, map) ++ self.out_buffer = b'' ++ ++ def initiate_send(self): ++ num_sent = 0 ++ num_sent = dispatcher.send(self, self.out_buffer[:65536]) ++ self.out_buffer = self.out_buffer[num_sent:] ++ ++ def handle_write(self): ++ self.initiate_send() ++ ++ def writable(self): ++ return (not self.connected) or len(self.out_buffer) ++ ++ def send(self, data): ++ if self.debug: ++ self.log_info('sending %s' % repr(data)) ++ self.out_buffer = self.out_buffer + data ++ self.initiate_send() ++ ++# --------------------------------------------------------------------------- ++# used for debugging. ++# --------------------------------------------------------------------------- ++ ++ ++def compact_traceback(): ++ t, v, tb = sys.exc_info() ++ tbinfo = [] ++ if not tb: # Must have a traceback ++ raise AssertionError("traceback does not exist") ++ while tb: ++ tbinfo.append(( ++ tb.tb_frame.f_code.co_filename, ++ tb.tb_frame.f_code.co_name, ++ str(tb.tb_lineno) ++ )) ++ tb = tb.tb_next ++ ++ # just to be safe ++ del tb ++ ++ file, function, line = tbinfo[-1] ++ info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) ++ return (file, function, line), t, v, info ++ ++ ++def close_all(map=None, ignore_all=False): ++ if map is None: ++ map = socket_map ++ for x in list(map.values()): ++ try: ++ x.close() ++ except OSError as x: ++ if x.errno == EBADF: ++ pass ++ elif not ignore_all: ++ raise ++ except _reraised_exceptions: ++ raise ++ except Exception: ++ if not ignore_all: ++ raise ++ map.clear() ++ ++ ++if os.name == 'posix': ++ class file_wrapper: ++ # Here we override just enough to make a file ++ # look like a socket for the purposes of asyncore. ++ # The passed fd is automatically os.dup()'d ++ ++ def __init__(self, fd): ++ self.fd = os.dup(fd) ++ ++ def __del__(self): ++ if self.fd >= 0: ++ warnings.warn("unclosed file %r" % self, ResourceWarning, ++ source=self, stacklevel=2) ++ self.close() ++ ++ def recv(self, *args): ++ return os.read(self.fd, *args) ++ ++ def send(self, *args): ++ return os.write(self.fd, *args) ++ ++ def getsockopt(self, level, optname, buflen=None): ++ if (level == socket.SOL_SOCKET and ++ optname == socket.SO_ERROR and ++ not buflen): ++ return 0 ++ raise NotImplementedError("Only asyncore specific behaviour " ++ "implemented.") ++ ++ read = recv ++ write = send ++ ++ def close(self): ++ if self.fd < 0: ++ return ++ fd = self.fd ++ self.fd = -1 ++ os.close(fd) ++ ++ def fileno(self): ++ return self.fd ++ ++ class file_dispatcher(dispatcher): ++ ++ def __init__(self, fd, map=None): ++ dispatcher.__init__(self, None, map) ++ self.connected = True ++ try: ++ fd = fd.fileno() ++ except AttributeError: ++ pass ++ self.set_file(fd) ++ # set it to non-blocking mode ++ os.set_blocking(fd, False) ++ ++ def set_file(self, fd): ++ self.socket = file_wrapper(fd) ++ self._fileno = self.socket.fileno() ++ self.add_channel() +Index: pyftpdlib-1.5.7/pyftpdlib/handlers.py +=================================================================== +--- pyftpdlib-1.5.7.orig/pyftpdlib/handlers.py ++++ pyftpdlib-1.5.7/pyftpdlib/handlers.py +@@ -2,7 +2,6 @@ + # Use of this source code is governed by MIT license that can be + # found in the LICENSE file. + +-import asynchat + import contextlib + import errno + import glob +@@ -57,6 +56,12 @@ from .log import debug + from .log import logger + + ++if sys.version_info[:2] >= (3, 12): ++ from . import _asynchat as asynchat ++else: ++ import asynchat ++ ++ + CR_BYTE = ord('\r') + + +Index: pyftpdlib-1.5.7/pyftpdlib/ioloop.py +=================================================================== +--- pyftpdlib-1.5.7.orig/pyftpdlib/ioloop.py ++++ pyftpdlib-1.5.7/pyftpdlib/ioloop.py +@@ -56,8 +56,6 @@ server = Server('localhost', 8021) + IOLoop.instance().loop() + """ + +-import asynchat +-import asyncore + import errno + import heapq + import os +@@ -80,6 +78,14 @@ from .log import is_logging_configured + from .log import logger + + ++if sys.version_info[:2] >= (3, 12): ++ from . import _asynchat as asynchat ++ from . import _asyncore as asyncore ++else: ++ import asynchat ++ import asyncore ++ ++ + timer = getattr(time, 'monotonic', time.time) + _read = asyncore.read + _write = asyncore.write