Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-scp for openSUSE:Factory checked in at 2026-08-01 18:30:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-scp (Old) and /work/SRC/openSUSE:Factory/.python-scp.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-scp" Sat Aug 1 18:30:25 2026 rev:16 rq:1368549 version:0.16.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-scp/python-scp.changes 2025-06-12 15:56:02.896118483 +0200 +++ /work/SRC/openSUSE:Factory/.python-scp.new.16738/python-scp.changes 2026-08-01 18:31:51.665026124 +0200 @@ -1,0 +2,10 @@ +Thu Jul 30 06:14:23 UTC 2026 - Martin Hauke <[email protected]> + +- Update to version 0.16.1 + * Fix missing import for paramiko, causing exception handling to + fail in close(). +- Update to version 0.16.0 + * Fix typing for __init__(). + * Wait for remote scp processs to exit before closing channel. + +------------------------------------------------------------------- Old: ---- scp-0.15.0.tar.gz New: ---- scp-0.16.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-scp.spec ++++++ --- /var/tmp/diff_new_pack.JJCwuP/_old 2026-08-01 18:31:52.545057239 +0200 +++ /var/tmp/diff_new_pack.JJCwuP/_new 2026-08-01 18:31:52.549057381 +0200 @@ -1,8 +1,8 @@ # # spec file for package python-scp # -# Copyright (c) 2025 SUSE LLC -# Copyright (c) 2017-2021, Martin Hauke <[email protected]> +# Copyright (c) 2026 SUSE LLC and contributors +# Copyright (c) 2017-2026, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?sle15_python_module_pythons} Name: python-scp -Version: 0.15.0 +Version: 0.16.1 Release: 0 Summary: SSH scp module for paramiko License: LGPL-2.1-or-later ++++++ scp-0.15.0.tar.gz -> scp-0.16.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/.ci/setup_dropbear.sh new/scp.py-0.16.1/.ci/setup_dropbear.sh --- old/scp.py-0.15.0/.ci/setup_dropbear.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/scp.py-0.16.1/.ci/setup_dropbear.sh 2026-07-29 17:59:23.000000000 +0200 @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +docker run -d --name dropbear -p 2222:22 ubuntu:24.04 sh -c "apt-get update && apt-get install -qq dropbear-bin && cd /root && cd /root && dropbearkey -t rsa -f id_rsa && dropbear -r id_rsa -F -E" + +WAITED=0 +while ! nc -z localhost 2222; do + if [ $WAITED -ge 120 ]; then + echo "dropbear didn't come up after $WAITED" >&2 + exit 1 + fi + sleep 3 + WAITED=$((WAITED + 3)) +done diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/.ci/setup_openssh.sh new/scp.py-0.16.1/.ci/setup_openssh.sh --- old/scp.py-0.15.0/.ci/setup_openssh.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/scp.py-0.16.1/.ci/setup_openssh.sh 2026-07-29 17:59:23.000000000 +0200 @@ -0,0 +1,67 @@ +#!/bin/sh + +set -e + +# Sets up an SSH server +rm -Rf /tmp/ssh_server +mkdir /tmp/ssh_server +cd /tmp/ssh_server + +# Server config file +cat >config <<'EOF' +Port 10022 +ListenAddress 127.0.0.1 + +Protocol 2 +HostKey /tmp/ssh_server/key_rsa + +# Authentication +LoginGraceTime 10 +PermitRootLogin yes +StrictModes no +UsePAM no + +PubkeyAuthentication yes +AuthorizedKeysFile /tmp/ssh_server/client/id_rsa.pub + +PrintMotd yes +EOF + +# Server keys +ssh-keygen -f key_rsa -N '' -t rsa + +# Client keys +umask 077 +mkdir client || true +ssh-keygen -f client/id_rsa -N '' -t rsa +umask 022 + +# Create "privilege separation directory" +if ! [ -d /run/sshd ]; then + mkdir -p /run/sshd +fi + +# Starts the server +/usr/sbin/sshd -f config -h key_rsa -p 10022 + +# Sets up the client +umask 077 +mkdir ~/.ssh || true +cp client/id_rsa ~/.ssh/id_rsa +umask 022 +rm -f ~/.ssh/known_hosts + +# If GitHub Actions: link from real HOME to GitHub HOME +REAL_HOME="$(getent passwd "$(id -u)" | cut -d: -f6)" +if [ "$REAL_HOME" != "$HOME" ]; then + printf 'Linking %s to %s\n' "$REAL_HOME/.ssh" "$HOME/.ssh" >&2 + rm -Rf $REAL_HOME/.ssh + ln -s $HOME/.ssh $REAL_HOME/.ssh +fi + +# ssh-keyscan is bugged, don't use it +# ssh-keyscan -v -p 10022 -t rsa 127.0.0.1 >> ~/.ssh/known_hosts +ssh -o StrictHostKeyChecking=no \ + -o PasswordAuthentication=no \ + -p 10022 127.0.0.1 exit +cat ~/.ssh/known_hosts diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/.ci/setup_ssh.sh new/scp.py-0.16.1/.ci/setup_ssh.sh --- old/scp.py-0.15.0/.ci/setup_ssh.sh 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/.ci/setup_ssh.sh 1970-01-01 01:00:00.000000000 +0100 @@ -1,70 +0,0 @@ -#!/bin/sh - -set -e - -# Sets up an SSH server -rm -Rf /tmp/ssh_server -mkdir /tmp/ssh_server -cd /tmp/ssh_server - -# Server config file -cat >config <<'EOF' -Port 10022 -ListenAddress 127.0.0.1 - -Protocol 2 -HostKey /tmp/ssh_server/key_rsa -HostKey /tmp/ssh_server/key_dsa -UsePrivilegeSeparation no - -# Authentication -LoginGraceTime 10 -PermitRootLogin yes -StrictModes no -UsePAM no - -RSAAuthentication yes -PubkeyAuthentication yes -AuthorizedKeysFile /tmp/ssh_server/client/id_rsa.pub - -PrintMotd yes -EOF - -# Server keys -ssh-keygen -f key_rsa -N '' -t rsa -ssh-keygen -f key_dsa -N '' -t dsa - -# Client keys -umask 077 -mkdir client || true -ssh-keygen -f client/id_rsa -N '' -t rsa -umask 022 - -# Create "privilege separation directory" -if ! [ -d /run/sshd ]; then - mkdir -p /run/sshd -fi - -# Starts the server -/usr/sbin/sshd -f config -h key_rsa -h key_dsa -p 10022 - -# Sets up the client -umask 077 -mkdir ~/.ssh || true -cp client/id_rsa ~/.ssh/id_rsa -umask 022 -rm -f ~/.ssh/known_hosts - -# If GitHub Actions: link from real HOME to GitHub HOME -REAL_HOME="$(getent passwd "$(id -u)" | cut -d: -f6)" -if [ "$REAL_HOME" != "$HOME" ]; then - printf 'Linking %s to %s\n' "$REAL_HOME/.ssh" "$HOME/.ssh" >&2 - ln -s $HOME/.ssh $REAL_HOME/.ssh -fi - -# ssh-keyscan is bugged, don't use it -# ssh-keyscan -v -p 10022 -t rsa 127.0.0.1 >> ~/.ssh/known_hosts -ssh -o StrictHostKeyChecking=no \ - -o PasswordAuthentication=no \ - -p 10022 127.0.0.1 exit -cat ~/.ssh/known_hosts diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/.github/workflows/test.yml new/scp.py-0.16.1/.github/workflows/test.yml --- old/scp.py-0.15.0/.github/workflows/test.yml 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/.github/workflows/test.yml 2026-07-29 17:59:23.000000000 +0200 @@ -5,49 +5,94 @@ - pull_request jobs: - build: + test-openssh: strategy: matrix: - os: [ubuntu-latest] python-version: + - "2.7" + - "3.5" - "3.7" - "3.8" - - "3.9" - include: - - os: ubuntu-20.04 - python-version: "3.5" - runs-on: ${{ matrix.os }} + - "3.10" + - "3.13" + runs-on: ubuntu-latest + container: + image: python:${{ matrix.python-version }} steps: + - name: Update APT repositories + run: | + if [ $PYTHON_VERSION = 2.7 ] || [ $PYTHON_VERSION = 3.5 ]; then + # Disable 'deb.debian.org' and 'security.debian.org' sources + # Enable 'snapshot.debian.org' sources + sed 's@^\(deb https\?://\(deb\.debian\.org\|security\.debian\.org\)/\)@# \1@; s@^# \(deb http://snapshot\.debian\.org\/\)@\1@' < /etc/apt/sources.list > /etc/apt/sources.list.new + mv /etc/apt/sources.list.new /etc/apt/sources.list + apt-get update -o Acquire::Check-Valid-Until=false + else + apt-get update + fi + env: + PYTHON_VERSION: ${{ matrix.python-version }} - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - run: pip install readme_renderer - - name: Install SSH + - name: Install SSH client + run: | + apt-get install -qq openssh-client + - name: Install SSH server run: | - sudo apt-get update -qq - sudo apt-get install -qq openssh-client openssh-server + apt-get install -qq openssh-server + - run: pip install setuptools + - run: pip install contextlib2 + if: ${{ startsWith(matrix.python-version, '2.') }} - name: Set up SSH - run: .ci/setup_ssh.sh + run: .ci/setup_openssh.sh - run: pip install -e . - run: SCPPY_PORT=10022 python test.py - - run: python setup.py check -r -s - build-container: + test-dropbear: strategy: matrix: python-version: - - "2.7" + - "3.13" runs-on: ubuntu-latest container: image: python:${{ matrix.python-version }} + services: + dropbear: + image: ubuntu:24.04 + ports: + - 10022 + command: sh -c "useradd user && mkdir /home/user && chown user:user /home/user && echo 'user:pass' | chpasswd && usermod -s /bin/bash user && apt-get update && apt-get install -qq dropbear-bin openssh-client && cd /root && cd /root && dropbearkey -t rsa -f id_rsa && dropbear -r id_rsa -F -E -p 10022" steps: + - name: Update APT repositories + run: | + if [ $PYTHON_VERSION = 2.7 ] || [ $PYTHON_VERSION = 3.5 ]; then + # Disable 'deb.debian.org' and 'security.debian.org' sources + # Enable 'snapshot.debian.org' sources + sed 's@^\(deb https\?://\(deb\.debian\.org\|security\.debian\.org\)/\)@# \1@; s@^# \(deb http://snapshot\.debian\.org\/\)@\1@' < /etc/apt/sources.list > /etc/apt/sources.list.new + mv /etc/apt/sources.list.new /etc/apt/sources.list + apt-get update -o Acquire::Check-Valid-Until=false + else + apt-get update + fi + env: + PYTHON_VERSION: ${{ matrix.python-version }} - uses: actions/checkout@v3 - - name: Install SSH + - name: Install SSH client run: | - apt-get update -qq - apt-get install -qq openssh-client openssh-server - - name: Set up SSH - run: .ci/setup_ssh.sh + apt-get install -qq openssh-client + - run: pip install setuptools + - run: pip install contextlib2 + if: ${{ startsWith(matrix.python-version, '2.') }} + - name: Wait for SSH server + run: | + apt-get install netcat-openbsd + WAITED=0 + while ! nc -z dropbear 10022; do + if [ $WAITED -ge 120 ]; then + echo "dropbear didn't come up after $WAITED" >&2 + exit 1 + fi + sleep 3 + WAITED=$((WAITED + 3)) + done - run: pip install -e . - - run: SCPPY_PORT=10022 python test.py + - run: SCPPY_HOSTNAME=dropbear SCPPY_PORT=10022 SCPPY_USERNAME=user SCPPY_PASSWORD=pass python test.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/CHANGELOG.md new/scp.py-0.16.1/CHANGELOG.md --- old/scp.py-0.15.0/CHANGELOG.md 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/CHANGELOG.md 2026-07-29 17:59:23.000000000 +0200 @@ -1,5 +1,14 @@ # Changelog +## 0.16.1 (2026-07-29) + +- Fix missing import for `paramiko`, causing exception handling to fail in close() + +## 0.16.0 (2026-07-13) + +- Fix typing for `__init__()` +- Wait for remote scp processs to exit before closing channel + ## 0.15.0 (2024-05-23) - Add bandwidth control (`limit_bw`) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/scp.py new/scp.py-0.16.1/scp.py --- old/scp.py-0.15.0/scp.py 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/scp.py 2026-07-29 17:59:23.000000000 +0200 @@ -5,10 +5,12 @@ Utilities for sending files over ssh using the scp1 protocol. """ -__version__ = '0.15.0' +__version__ = '0.16.1' import locale import os +import paramiko +import paramiko.transport import re from socket import timeout as SocketTimeout @@ -29,10 +31,7 @@ pass try: - from typing import IO, TYPE_CHECKING, AnyStr, Callable, Iterable, Optional, Tuple, Union - - if TYPE_CHECKING: - import paramiko.transport + from typing import IO, AnyStr, Callable, Iterable, Optional, Tuple, Union # this is some magic to make sure pyright doesn't get too confused with pathlib potentially being a nullable variable import pathlib @@ -116,7 +115,7 @@ """ def __init__(self, transport, buff_size=16384, socket_timeout=10.0, progress=None, progress4=None, sanitize=_sh_quote, limit_bw=None): - # type: (paramiko.transport.Transport, int, float, Optional[Callable[[bytes, int, int], None]], Optional[Callable[[bytes, int, int, Tuple[str, int]], None]], Callable[[bytes], bytes]) -> None + # type: (paramiko.transport.Transport, int, Optional[float], Optional[Callable[[bytes, int, int], None]], Optional[Callable[[bytes, int, int, Tuple[str, int]], None]], Callable[[bytes], bytes], Optional[int]) -> None """ Create an scp1 client. @@ -125,7 +124,7 @@ @param buff_size: size of the scp send buffer. @type buff_size: int @param socket_timeout: channel socket timeout in seconds - @type socket_timeout: float + @type socket_timeout: Optional[float] @param progress: callback - called with (filename, size, sent) during transfers @param progress4: callback - called with (filename, size, sent, peername) @@ -296,6 +295,11 @@ def close(self): """close scp channel""" if self.channel is not None: + try: + self.channel.shutdown_write() + self.channel.recv_exit_status() + except (EOFError, OSError, paramiko.SSHException): + pass self.channel.close() self.channel = None @@ -447,7 +451,7 @@ times = cmd.split(b' ') mtime = int(times[0]) atime = int(times[2]) or mtime - except: + except (ValueError, IndexError, AssertionError): self.channel.send(b'\x01') raise SCPException('Bad time format') # save for later @@ -471,7 +475,7 @@ name = parts[2] assert not os.path.isabs(name) path = os.path.join(asbytes(self._recv_dir), name) - except: + except (ValueError, IndexError, AssertionError): chan.send('\x01') chan.close() raise SCPException('Bad file format') @@ -537,7 +541,7 @@ assert not os.path.isabs(name) path = os.path.join(asbytes(self._recv_dir), name) self._depth += 1 - except: + except (ValueError, IndexError, AssertionError): self.channel.send(b'\x01') raise SCPException('Bad directory format') try: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/setup.py new/scp.py-0.16.1/setup.py --- old/scp.py-0.15.0/setup.py 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/setup.py 2026-07-29 17:59:23.000000000 +0200 @@ -10,7 +10,7 @@ description = fp.read() setup( name = 'scp', - version = '0.15.0', + version = '0.16.1', author = 'James Bardin', author_email = '[email protected]', maintainer="Remi Rampin", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/scp.py-0.15.0/test.py new/scp.py-0.16.1/test.py --- old/scp.py-0.15.0/test.py 2024-05-23 23:36:51.000000000 +0200 +++ new/scp.py-0.16.1/test.py 2026-07-29 17:59:23.000000000 +0200 @@ -1,5 +1,9 @@ from __future__ import print_function +try: + import contextlib2 as contextlib +except ImportError: + import contextlib from io import BytesIO import os import paramiko @@ -21,6 +25,7 @@ 'hostname': os.environ.get('SCPPY_HOSTNAME', '127.0.0.1'), 'port': int(os.environ.get('SCPPY_PORT', 22)), 'username': os.environ.get('SCPPY_USERNAME', None), + 'password': os.environ.get('SCPPY_PASSWORD', None), } @@ -94,7 +99,8 @@ "Python 3" if PY3 else "Python 2")) def download_test(self, filename, recursive, destination=None, - expected_win=[], expected_posix=[], limit_bw=None): + expected_win=[], expected_posix=[], limit_bw=None, + scp=None): # Make a temporary directory temp = tempfile.mkdtemp(prefix='scp-py_test_') # Add some unicode in the path @@ -111,11 +117,13 @@ os.chdir(temp_in) cb3 = lambda filename, size, sent: None try: - with SCPClient( - self.ssh.get_transport(), - progress=cb3, - limit_bw=limit_bw, - ) as scp: + with contextlib.ExitStack() as ctx: + if scp is None: + scp = ctx.enter_context(SCPClient( + self.ssh.get_transport(), + progress=cb3, + limit_bw=limit_bw, + )) scp.get(filename, destination if destination is not None else u'.', preserve_times=True, recursive=recursive) @@ -148,6 +156,22 @@ False, None, [u'file', u'b\xE8te'], [b'file', b'b\xC3\xA8te']) + def test_get_bytes_multi(self): + with SCPClient( + self.ssh.get_transport(), + ) as scp: + self.download_test(b'/tmp/r\xC3\xA9mi', False, b'target', + [u'target'], [b'target'], scp=scp) + self.download_test(b'/tmp/r\xC3\xA9mi', False, u'target', + [u'target'], [b'target'], + limit_bw=2, scp=scp) + self.download_test(b'/tmp/r\xC3\xA9mi', False, None, + [u'r\xE9mi'], [b'r\xC3\xA9mi'], scp=scp) + self.download_test([b'/tmp/bien rang\xC3\xA9/file', + b'/tmp/bien rang\xC3\xA9/b\xC3\xA8te'], + False, None, + [u'file', u'b\xE8te'], [b'file', b'b\xC3\xA8te'], scp=scp) + def test_get_unicode(self): self.download_test(u'/tmp/r\xE9mi', False, b'target', [u'target'], [b'target']) @@ -213,7 +237,8 @@ def tearDownClass(cls): shutil.rmtree(cls._temp) - def upload_test(self, filenames, recursive, expected=[], fl=None, limit_bw=None): + def upload_test(self, filenames, recursive, expected=[], + fl=None, limit_bw=None, scp=None): destination = b'/tmp/upp\xC3\xA9' + next(unique_names) chan = self.ssh.get_transport().open_session() chan.exec_command(b'mkdir ' + destination) @@ -222,11 +247,13 @@ cb4 = lambda filename, size, sent, peername: None try: os.chdir(self._temp) - with SCPClient( - self.ssh.get_transport(), - progress4=cb4, - limit_bw=limit_bw, - ) as scp: + with contextlib.ExitStack() as ctx: + if scp is None: + scp = ctx.enter_context(SCPClient( + self.ssh.get_transport(), + progress4=cb4, + limit_bw=limit_bw, + )) if not fl: scp.put(filenames, destination, recursive) else: @@ -299,6 +326,35 @@ b'dossi\xC3\xA9/bien rang\xC3\xA9/test', b'r\xC3\xA9mi']) + def test_put_unicode_multi(self): + with SCPClient( + self.ssh.get_transport(), + ) as scp: + self.upload_test(u'cl\xE9/r\xE9mi', False, [b'r\xC3\xA9mi'], scp=scp) + self.upload_test(u'cl\xE9/dossi\xE9/bien rang\xE9/test', False, + [b'test'], scp=scp) + self.upload_test(u'cl\xE9/dossi\xE9', True, + [b'dossi\xC3\xA9', + b'dossi\xC3\xA9/bien rang\xC3\xA9', + b'dossi\xC3\xA9/bien rang\xC3\xA9/test'], scp=scp) + self.upload_test([u'cl\xE9/dossi\xE9/bien rang\xE9', + u'cl\xE9/r\xE9mi'], True, + [b'bien rang\xC3\xA9', + b'bien rang\xC3\xA9/test', + b'r\xC3\xA9mi'], scp=scp) + g = (n for n in (u'cl\xE9/dossi\xE9/bien rang\xE9', u'cl\xE9/r\xE9mi')) + assert isinstance(g, types.GeneratorType) + self.upload_test(g, True, + [b'bien rang\xC3\xA9', + b'bien rang\xC3\xA9/test', + b'r\xC3\xA9mi'], scp=scp) + self.upload_test([u'cl\xE9/dossi\xE9', + u'cl\xE9/r\xE9mi'], True, + [b'dossi\xC3\xA9', + b'dossi\xC3\xA9/bien rang\xC3\xA9', + b'dossi\xC3\xA9/bien rang\xC3\xA9/test', + b'r\xC3\xA9mi'], scp=scp) + @unittest.skipUnless(pathlib, "pathlib not available") def test_pathlib(self): self.upload_test(pathlib.Path(u'cl\xE9/dossi\xE9'), True, @@ -335,14 +391,14 @@ '''send and receive files with the same client''' previous = os.getcwd() testfile = os.path.join(self._temp, 'testfile') - testfile_sent = os.path.join(self._temp, 'testfile_sent') + remote = '/tmp/testfile-up-and-down' testfile_rcvd = os.path.join(self._temp, 'testfile_rcvd') try: os.chdir(self._temp) with open(testfile, 'w') as f: f.write("TESTING\n") - put(self.ssh.get_transport(), testfile, testfile_sent) - get(self.ssh.get_transport(), testfile_sent, testfile_rcvd) + put(self.ssh.get_transport(), testfile, remote) + get(self.ssh.get_transport(), remote, testfile_rcvd) with open(testfile_rcvd) as f: self.assertEqual(f.read(), 'TESTING\n')
