From 3cabcadd7e9f1a1328a13fe2de774c5a06c50a59 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 16 Aug 2025 14:22:32 +0000 Subject: [PATCH 01/56] mismatched sock --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 0637efd4..e894197a 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1138,7 +1138,7 @@ def close(self): try: # Close cached channels for channel in self._channels: - if not channel.closed: + if not channel.sock.closed: channel.close() # Close the transport. if self._transport and self._transport.is_active(): From c73db278b3d74e482eea9e62af6ff9e9491a6aaa Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:12:09 +0000 Subject: [PATCH 02/56] adding python 3.13 to the test suite and as the default to the publish and document workflows. version bump --- .github/workflows/document.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- README.rst | 2 +- docs/changes.rst | 8 ++++++-- docs/conf.py | 4 ++-- docs/index.rst | 2 +- pyproject.toml | 3 ++- 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index a2a9f9cc..77d719d1 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -16,7 +16,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - python-version: ['3.12'] + python-version: ['3.13'] steps: - name: Clone Repository diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 214f3731..f230e330 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - python-version: ['3.12'] + python-version: ['3.13'] steps: - name: Clone Repository diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b13aa699..590ea12b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [macos-13, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] exclude: - os: macos-latest python-version: 3.7 diff --git a/README.rst b/README.rst index b77e4d1a..9ecb6991 100644 --- a/README.rst +++ b/README.rst @@ -138,6 +138,6 @@ paramiko >= 2.7.0 Supports -------- -Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 +Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 diff --git a/docs/changes.rst b/docs/changes.rst index 8a2b22a1..1c1dc1f2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,10 +1,14 @@ Change Log ========== -1.1.9 (current, released 2025-8-06) +1.1.10 (current, released 2025-8-16) +----------------------------------- + * regression fix for properly closing channel cache sockets. + +1.1.9 (released 2025-8-06) ----------------------------------- - * removing ssh-dss key type as it was deprecated in paramiko in 4.0.0. * adding channel cache to place upper limit on creation overhead. + * removing ssh-dss key type as it was deprecated in paramiko in 4.0.0. 1.1.8 (released 2025-6-13) -------------------------- diff --git a/docs/conf.py b/docs/conf.py index 23e78afa..b3fee7c5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = '1.1.9' +version = '1.1.10' # The full version, including alpha/beta/rc tags. -release = '1.1.9' +release = '1.1.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index b138d237..6c4ed36b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -138,7 +138,7 @@ paramiko >= 2.7.0 Supports -------- -Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 +Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 Contents -------- diff --git a/pyproject.toml b/pyproject.toml index 2d0d62f0..53c98f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython' ] dependencies = [ @@ -46,7 +47,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.1.9' +version = '1.1.10' [project.scripts] sftpretty = 'sftpretty:Connection' From c6093c5a490db4db176fabfcdb8d7f2149fbe9f8 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:40:21 +0000 Subject: [PATCH 03/56] addition of python 3.13 requires another call to drivedrop to avoid new behavior that returns two leading // for certain normalized windows paths --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index e894197a..d9d02ee0 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1520,7 +1520,7 @@ def pwd(self): with self._sftp_channel() as channel: pwd = channel.normalize('.') - return pwd + return drivedrop(pwd) @property def remote_server_key(self): From 8c24763fd8c673b87686e909e9a64bfbdbe77cde Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:53:40 +0000 Subject: [PATCH 04/56] unfortunately this is a deeper issue that requires upstream parity. Workaround in place as unsure when and how paramiko will handle this. --- sftpretty/__init__.py | 2 +- tests/test_issue_xx.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index d9d02ee0..5b6d7a2e 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1520,7 +1520,7 @@ def pwd(self): with self._sftp_channel() as channel: pwd = channel.normalize('.') - return drivedrop(pwd) + return pwd.replace('//', '/') @property def remote_server_key(self): diff --git a/tests/test_issue_xx.py b/tests/test_issue_xx.py index 8ff00632..122d4cd3 100644 --- a/tests/test_issue_xx.py +++ b/tests/test_issue_xx.py @@ -19,7 +19,7 @@ def test_issue_xx_sftpserver_plugin(sftpserver): with sftp.cd(): sftp.chdir('pub') assert sftp.pwd == testpath.as_posix() - assert home == '/home/test' + assert home == testpath.parent.as_posix() def test_issue_xx_local_sftpserver(lsftp): From 8e41a59d7df3cc7f43d03b4948b7c5ffc2330ed2 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 20 Aug 2025 03:25:22 +0000 Subject: [PATCH 05/56] and there's a trailing backslash now as well, 3.13 support off to a rough start --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 5b6d7a2e..56101633 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1520,7 +1520,7 @@ def pwd(self): with self._sftp_channel() as channel: pwd = channel.normalize('.') - return pwd.replace('//', '/') + return pwd.replace('//', '/').rstrip('/') @property def remote_server_key(self): From 4c382b5f8b7e18fcfaf7e964d68583bc179df119 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:06:26 +0000 Subject: [PATCH 06/56] tests caught something yay! --- sftpretty/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 56101633..b53d2bd9 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -305,18 +305,19 @@ def _sftp_channel(self): channel.settimeout(self._timeout) log.debug(f'Channel Name: [{channel_name}]') - if self._default_path is not None: - _channel.chdir(drivedrop(self._default_path)) - log.info(('Current Working Directory: ' - f'[{self._default_path}]')) - self._cache.channel = _channel self._channels.append(_channel) log.debug(f'Thread Cached: [{get_ident()}]') else: + _channel.chdir(None) channel = _channel.get_channel() channel.settimeout(self._timeout) + if self._default_path is not None: + _channel.chdir(drivedrop(self._default_path)) + log.info(('Current Working Directory: ' + f'[{self._default_path}]')) + yield _channel except Exception as err: _channel.close() From 236f47854ea4fbe717988cae84bee8ed1c21cc78 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 25 Aug 2025 01:46:09 +0000 Subject: [PATCH 07/56] is this a bug? --- sftpretty/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index b53d2bd9..9b826cbb 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,9 +309,10 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{get_ident()}]') else: - _channel.chdir(None) + _channel.chdir(path=None) channel = _channel.get_channel() channel.settimeout(self._timeout) + log.debug(f'Using Cached Thread: [{get_ident()}]') if self._default_path is not None: _channel.chdir(drivedrop(self._default_path)) From fa5cfa0c09339f7c21de37af384dfcc1ff79b50c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 25 Aug 2025 02:08:03 +0000 Subject: [PATCH 08/56] resetting to root on channel reuse --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 9b826cbb..3a572f95 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,7 +309,7 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{get_ident()}]') else: - _channel.chdir(path=None) + _channel.chdir(path='/') channel = _channel.get_channel() channel.settimeout(self._timeout) log.debug(f'Using Cached Thread: [{get_ident()}]') From 08f02b8c286d13d3120cca2c01ab2ae5516508fb Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:05:31 +0000 Subject: [PATCH 09/56] bring the test to parity with similar cd ones, just use channel name instead of relying on get_ident --- sftpretty/__init__.py | 8 ++++---- tests/test_issue_65.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 3a572f95..3771b706 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -14,7 +14,7 @@ from socket import gaierror from stat import S_ISDIR, S_ISREG from tempfile import mkstemp -from threading import get_ident, local as cache +from threading import local as cache from uuid import uuid4 @@ -307,12 +307,12 @@ def _sftp_channel(self): self._cache.channel = _channel self._channels.append(_channel) - log.debug(f'Thread Cached: [{get_ident()}]') + log.debug(f'Thread Cached: [{channel_name}]') else: - _channel.chdir(path='/') + _channel.chdir(path=None) channel = _channel.get_channel() channel.settimeout(self._timeout) - log.debug(f'Using Cached Thread: [{get_ident()}]') + log.debug(f'Using Cached Thread: [{channel.get_name()}]') if self._default_path is not None: _channel.chdir(drivedrop(self._default_path)) diff --git a/tests/test_issue_65.py b/tests/test_issue_65.py index 536ecfe2..8604f8d7 100644 --- a/tests/test_issue_65.py +++ b/tests/test_issue_65.py @@ -18,4 +18,4 @@ def test_issue_65(sftpserver): with sftp.cd(pubpath.as_posix()): pass - assert sftp.getcwd() == '/' + assert sftp.getcwd() == pubpath.parent.as_posix() From bd5776c9400cff356eb827cf90a555f9349d18fa Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:30:20 +0000 Subject: [PATCH 10/56] coming into focus --- sftpretty/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 3771b706..b6a83a6f 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,7 +309,6 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{channel_name}]') else: - _channel.chdir(path=None) channel = _channel.get_channel() channel.settimeout(self._timeout) log.debug(f'Using Cached Thread: [{channel.get_name()}]') @@ -1083,7 +1082,7 @@ def cd(self, remotepath=None): except Exception as err: raise err finally: - self.chdir(original_path) + self._default_path = original_path def chdir(self, remotepath): '''Change the current working directory on the remote From b3daf934646380e7cfdf648e7b4a2a2a7f34c032 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:40:12 +0000 Subject: [PATCH 11/56] lets do both --- sftpretty/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index b6a83a6f..967afed3 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,6 +309,7 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{channel_name}]') else: + _channel.chdir(None) channel = _channel.get_channel() channel.settimeout(self._timeout) log.debug(f'Using Cached Thread: [{channel.get_name()}]') @@ -1083,6 +1084,7 @@ def cd(self, remotepath=None): raise err finally: self._default_path = original_path + self.chdir(original_path) def chdir(self, remotepath): '''Change the current working directory on the remote From a641b3eeb35e08d6c9c59cdfcb99c89bc06c8398 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:48:12 +0000 Subject: [PATCH 12/56] now what is going on with Windows python3.13 --- tests/test_issue_65.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_issue_65.py b/tests/test_issue_65.py index 8604f8d7..4ac95b05 100644 --- a/tests/test_issue_65.py +++ b/tests/test_issue_65.py @@ -18,4 +18,4 @@ def test_issue_65(sftpserver): with sftp.cd(pubpath.as_posix()): pass - assert sftp.getcwd() == pubpath.parent.as_posix() + assert sftp.getcwd() == pubpath.root From 61b2a84e0e06f281512e8d3d1fb14ad750d5f905 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:52:40 +0000 Subject: [PATCH 13/56] this could be the cause of my windows python 3.13 issues --- sftpretty/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 967afed3..927ca6fd 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,7 +309,6 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{channel_name}]') else: - _channel.chdir(None) channel = _channel.get_channel() channel.settimeout(self._timeout) log.debug(f'Using Cached Thread: [{channel.get_name()}]') From 5b52585e59a705b11aacba24fb3aafca63575394 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 26 Aug 2025 19:41:46 +0000 Subject: [PATCH 14/56] keepin it posix for test asserts, definitely need the chdir reset in _sftp_channel for cached channels --- sftpretty/__init__.py | 1 + tests/test_issue_65.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 927ca6fd..967afed3 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -309,6 +309,7 @@ def _sftp_channel(self): self._channels.append(_channel) log.debug(f'Thread Cached: [{channel_name}]') else: + _channel.chdir(None) channel = _channel.get_channel() channel.settimeout(self._timeout) log.debug(f'Using Cached Thread: [{channel.get_name()}]') diff --git a/tests/test_issue_65.py b/tests/test_issue_65.py index 4ac95b05..46c82fb0 100644 --- a/tests/test_issue_65.py +++ b/tests/test_issue_65.py @@ -2,14 +2,14 @@ location''' from common import conn, VFS -from pathlib import Path +from pathlib import PurePosixPath from sftpretty import Connection def test_issue_65(sftpserver): '''using the .cd() context manager prior to setting a directory via chdir causes an error''' - pubpath = Path('/home/test').joinpath('pub') + pubpath = PurePosixPath('/home/test').joinpath('pub') with sftpserver.serve_content(VFS): cnn = conn(sftpserver) cnn['default_path'] = None From fff996f558b73bd745a4db5014e1d8d00d2939ce Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 27 Aug 2025 04:17:09 +0000 Subject: [PATCH 15/56] I suspect this is all attributable to the stricter absolute path definition in 3.13 clashing with posix paths --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 967afed3..b5a15ded 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1078,12 +1078,12 @@ def cd(self, remotepath=None): try: if remotepath is not None: + self._default_path = original_path self.chdir(remotepath) yield except Exception as err: raise err finally: - self._default_path = original_path self.chdir(original_path) def chdir(self, remotepath): From 023a26dec2029d2d913ebc7500919d07a2696b2f Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 27 Aug 2025 04:21:37 +0000 Subject: [PATCH 16/56] need both apparently --- sftpretty/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index b5a15ded..28f5a4cf 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1084,6 +1084,7 @@ def cd(self, remotepath=None): except Exception as err: raise err finally: + self._default_path = original_path self.chdir(original_path) def chdir(self, remotepath): From 1156977c38d4b3a90cba84c256cf0c1bc451e2ea Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 27 Aug 2025 04:30:19 +0000 Subject: [PATCH 17/56] absolutely sus --- sftpretty/__init__.py | 1 - tests/test_cd.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 28f5a4cf..967afed3 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1078,7 +1078,6 @@ def cd(self, remotepath=None): try: if remotepath is not None: - self._default_path = original_path self.chdir(remotepath) yield except Exception as err: diff --git a/tests/test_cd.py b/tests/test_cd.py index 8f2695a7..c9624a2b 100644 --- a/tests/test_cd.py +++ b/tests/test_cd.py @@ -3,13 +3,13 @@ import pytest from common import conn, VFS -from pathlib import Path +from pathlib import Path, PurePosixPath from sftpretty import Connection def test_cd_none(sftpserver): '''test sftpretty.cd with None''' - pubpath = Path('/home/test').joinpath('pub') + pubpath = PurePosixPath('/home/test').joinpath('pub') with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: home = sftp.pwd @@ -21,7 +21,7 @@ def test_cd_none(sftpserver): def test_cd_path(sftpserver): '''test sftpretty.cd with a path''' - pubpath = Path('/home/test').joinpath('pub') + pubpath = PurePosixPath('/home/test').joinpath('pub') with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: home = sftp.pwd @@ -32,7 +32,7 @@ def test_cd_path(sftpserver): def test_cd_nested(sftpserver): '''test nested cd's''' - pubpath = Path('/home/test').joinpath('pub') + pubpath = PurePosixPath('/home/test').joinpath('pub') with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: home = sftp.pwd From 44bbc3cff3299bc702da994a3f0f7bc7cbb87e09 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 30 Aug 2025 23:29:59 +0000 Subject: [PATCH 18/56] unfortunate complexity --- sftpretty/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 967afed3..49c0e047 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1096,6 +1096,10 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: + if not Path(remotepath).is_absolute(): + root = self._default_path or channel.getcwd() + remotepath = Path(root).joinpath( + remotepath).as_posix() channel.chdir(drivedrop(remotepath)) self._default_path = channel.normalize('.') From 6a4d3a99b1532586b94362b64422eb5b14e8831b Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 30 Aug 2025 23:36:31 +0000 Subject: [PATCH 19/56] taking care of the none case for the root --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 49c0e047..2542fc21 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1097,7 +1097,7 @@ def chdir(self, remotepath): ''' with self._sftp_channel() as channel: if not Path(remotepath).is_absolute(): - root = self._default_path or channel.getcwd() + root = self._default_path or channel.getcwd() or '/' remotepath = Path(root).joinpath( remotepath).as_posix() channel.chdir(drivedrop(remotepath)) From 02949132ea3b9686c9edf07a568d32aa58882b7d Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 02:22:01 +0000 Subject: [PATCH 20/56] additional logging --- sftpretty/__init__.py | 10 +++++----- tests/test_cd.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 2542fc21..b278475f 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1075,10 +1075,13 @@ def cd(self, remotepath=None): :raises: IOError, if remote path doesn't exist ''' original_path = self.pwd + log.info(f'Original: {original_path}') try: if remotepath is not None: + log.info(f'Remote: {remotepath}') self.chdir(remotepath) + log.info(f'Remote After: {self._default_path}') yield except Exception as err: raise err @@ -1096,10 +1099,6 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: - if not Path(remotepath).is_absolute(): - root = self._default_path or channel.getcwd() or '/' - remotepath = Path(root).joinpath( - remotepath).as_posix() channel.chdir(drivedrop(remotepath)) self._default_path = channel.normalize('.') @@ -1526,8 +1525,9 @@ def pwd(self): ''' with self._sftp_channel() as channel: pwd = channel.normalize('.') + log.info(f'Working Directory: {pwd}') - return pwd.replace('//', '/').rstrip('/') + return pwd @property def remote_server_key(self): diff --git a/tests/test_cd.py b/tests/test_cd.py index c9624a2b..10188561 100644 --- a/tests/test_cd.py +++ b/tests/test_cd.py @@ -3,7 +3,7 @@ import pytest from common import conn, VFS -from pathlib import Path, PurePosixPath +from pathlib import PurePosixPath from sftpretty import Connection From 42dd8276fd3486d712366194a9833fdbd5eced69 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 02:40:54 +0000 Subject: [PATCH 21/56] upstream issue is spreading in 3.13 --- sftpretty/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index b278475f..9ba58053 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1075,13 +1075,10 @@ def cd(self, remotepath=None): :raises: IOError, if remote path doesn't exist ''' original_path = self.pwd - log.info(f'Original: {original_path}') try: if remotepath is not None: - log.info(f'Remote: {remotepath}') self.chdir(remotepath) - log.info(f'Remote After: {self._default_path}') yield except Exception as err: raise err @@ -1524,8 +1521,7 @@ def pwd(self): :returns: (str) Current working directory. ''' with self._sftp_channel() as channel: - pwd = channel.normalize('.') - log.info(f'Working Directory: {pwd}') + pwd = drivedrop(channel.normalize('.')) return pwd From da686fdf8791c6ef2f25ff16326b7e8563533a2e Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 02:57:44 +0000 Subject: [PATCH 22/56] need another test --- sftpretty/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 9ba58053..734a2bb1 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1096,8 +1096,11 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: + log.info(f'Check: {remotepath}') channel.chdir(drivedrop(remotepath)) - self._default_path = channel.normalize('.') + cwd = drivedrop(channel.normalize('.')) + log.info(f'After: {cwd}') + self._default_path = cwd def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. From 47e5949f55c76d5e0ba2a4637bc56e358ad1ad80 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 04:48:31 +0000 Subject: [PATCH 23/56] refactor reached --- sftpretty/__init__.py | 71 +++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 734a2bb1..2a5fed94 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,10 +191,10 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() + self._cache.cwd = default_path self._channels = [] self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) - self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -294,36 +294,48 @@ def _set_username(self, username): @contextmanager def _sftp_channel(self): '''Establish new SFTP channel.''' - _channel = getattr(self._cache, 'channel', None) + channel = None - try: - if _channel is None or _channel.get_channel().closed: - _channel = SFTPClient.from_transport(self._transport) - channel = _channel.get_channel() - channel_name = uuid4().hex - channel.set_name(channel_name) - channel.settimeout(self._timeout) - log.debug(f'Channel Name: [{channel_name}]') - - self._cache.channel = _channel - self._channels.append(_channel) - log.debug(f'Thread Cached: [{channel_name}]') - else: - _channel.chdir(None) - channel = _channel.get_channel() - channel.settimeout(self._timeout) - log.debug(f'Using Cached Thread: [{channel.get_name()}]') + for ch, in_use in self._channels: + chan = ch.get_channel() + if not in_use and not chan.closed: + channel = ch + in_use = True + log.debug(f'Cached Thread: [{chan.get_name()}]') + break + + if channel is None: + channel = SFTPClient.from_transport(self._transport) + + chan = channel.get_channel() + channel_name = uuid4().hex + chan.set_name(channel_name) + chan.settimeout(self._timeout) + log.debug(f'Channel Name: [{channel_name}]') - if self._default_path is not None: - _channel.chdir(drivedrop(self._default_path)) - log.info(('Current Working Directory: ' - f'[{self._default_path}]')) + self._channels.append([channel, True]) - yield _channel + try: + default_path = getattr(self._cache, 'cwd', None) + if default_path: + try: + channel.chdir(drivedrop(default_path)) + log.info(f'Current Working Directory: [{default_path}]') + except IOError: + log.error(f'Failed directory change to [{default_path}]') + raise + + yield channel except Exception as err: - _channel.close() - self._cache.channel = None + channel.close() raise err + finally: + if channel and not chan.closed: + channel.chdir(None) + for i, (ch, _) in enumerate(self._channels): + if ch == channel: + self._channels[i][1] = False + break def _start_transport(self, host, port): '''Start the transport and set connection options if specified.''' @@ -655,7 +667,7 @@ def get_r(self, remotedir, localdir, callback=None, self.chdir(remotedir) lwd = Path(localdir).absolute().as_posix() - rwd = self._default_path + rwd = self._cache.cwd tree = {} tree[rwd] = [(rwd, lwd)] @@ -1083,7 +1095,6 @@ def cd(self, remotepath=None): except Exception as err: raise err finally: - self._default_path = original_path self.chdir(original_path) def chdir(self, remotepath): @@ -1100,7 +1111,7 @@ def chdir(self, remotepath): channel.chdir(drivedrop(remotepath)) cwd = drivedrop(channel.normalize('.')) log.info(f'After: {cwd}') - self._default_path = cwd + self._cache.cwd = cwd def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. @@ -1143,7 +1154,7 @@ def close(self): '''Terminate transport connection and clean up the bits.''' try: # Close cached channels - for channel in self._channels: + for channel, _ in self._channels: if not channel.sock.closed: channel.close() # Close the transport. From d0eb86fcbb9d43cd3243603487799db2afdc8df8 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:18:14 +0000 Subject: [PATCH 24/56] normalize not feeling so normal --- sftpretty/__init__.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 2a5fed94..53531cca 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -195,6 +195,7 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, self._channels = [] self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) + self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -296,23 +297,21 @@ def _sftp_channel(self): '''Establish new SFTP channel.''' channel = None - for ch, in_use in self._channels: + for i, (ch, in_use) in enumerate(self._channels): chan = ch.get_channel() if not in_use and not chan.closed: channel = ch - in_use = True + self._channels[i][1] = True log.debug(f'Cached Thread: [{chan.get_name()}]') break if channel is None: channel = SFTPClient.from_transport(self._transport) - chan = channel.get_channel() channel_name = uuid4().hex chan.set_name(channel_name) chan.settimeout(self._timeout) log.debug(f'Channel Name: [{channel_name}]') - self._channels.append([channel, True]) try: @@ -321,9 +320,9 @@ def _sftp_channel(self): try: channel.chdir(drivedrop(default_path)) log.info(f'Current Working Directory: [{default_path}]') - except IOError: - log.error(f'Failed directory change to [{default_path}]') - raise + except IOError as err: + log.error(f'Failed Directory Change: [{default_path}]') + raise err yield channel except Exception as err: @@ -331,8 +330,8 @@ def _sftp_channel(self): raise err finally: if channel and not chan.closed: - channel.chdir(None) - for i, (ch, _) in enumerate(self._channels): + channel.chdir(self._default_path) + for i, (ch, in_use) in enumerate(self._channels): if ch == channel: self._channels[i][1] = False break @@ -1107,11 +1106,8 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: - log.info(f'Check: {remotepath}') channel.chdir(drivedrop(remotepath)) - cwd = drivedrop(channel.normalize('.')) - log.info(f'After: {cwd}') - self._cache.cwd = cwd + self._cache.cwd = drivedrop(channel.normalize('.')) def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. @@ -1351,7 +1347,7 @@ def normalize(self, remotepath): with self._sftp_channel() as channel: expanded_path = channel.normalize(drivedrop(remotepath)) - return expanded_path + return drivedrop(expanded_path) def open(self, remotefile, bufsize=-1, mode='r'): '''Open a file on the remote server. @@ -1381,7 +1377,7 @@ def readlink(self, remotelink): remotelink = drivedrop(remotelink) link_destination = channel.normalize(channel.readlink(remotelink)) - return link_destination + return drivedrop(link_destination) def remotetree(self, container, remotedir, localdir, recurse=True): '''Recursively map remote directory tree to a dictionary container. From e6b09043c5d4ecf15e8db6443ea1a03a0a5a3201 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:39:07 +0000 Subject: [PATCH 25/56] moving timeout setter ordering in channel creation as well as channel cwd reset. --- sftpretty/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 53531cca..396bf0fd 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -310,27 +310,28 @@ def _sftp_channel(self): chan = channel.get_channel() channel_name = uuid4().hex chan.set_name(channel_name) - chan.settimeout(self._timeout) log.debug(f'Channel Name: [{channel_name}]') self._channels.append([channel, True]) try: + chan.settimeout(self._timeout) + channel.chdir(self._default_path) default_path = getattr(self._cache, 'cwd', None) + if default_path: try: channel.chdir(drivedrop(default_path)) log.info(f'Current Working Directory: [{default_path}]') except IOError as err: - log.error(f'Failed Directory Change: [{default_path}]') - raise err + log.error(f'Failed Directory Change: [{default_path}]') + raise err yield channel except Exception as err: channel.close() raise err finally: - if channel and not chan.closed: - channel.chdir(self._default_path) + if not chan.closed: for i, (ch, in_use) in enumerate(self._channels): if ch == channel: self._channels[i][1] = False From 9108b4bd51edcebdd03ec386cee8b99b1e4aa39c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:22:14 +0000 Subject: [PATCH 26/56] final attempt for the night --- sftpretty/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 396bf0fd..d234c27d 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -307,15 +307,15 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) - chan = channel.get_channel() + channel.chdir(self._default_path) channel_name = uuid4().hex + chan = channel.get_channel() chan.set_name(channel_name) log.debug(f'Channel Name: [{channel_name}]') self._channels.append([channel, True]) try: chan.settimeout(self._timeout) - channel.chdir(self._default_path) default_path = getattr(self._cache, 'cwd', None) if default_path: @@ -1261,7 +1261,7 @@ def listdir(self, remotepath='.'): return directory def listdir_attr(self, remotepath='.'): - '''Return a non-sorted list of SFTPAttribute objects for the remote + '''Return a sorted list of SFTPAttribute objects for the remote directory contents. Will not include the special entries '.' and '..'. The returned SFTPAttributes objects will each have an additional field: From 37efb3b1cb07aa1d275cc170264708cab8ee2d66 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:32:21 +0000 Subject: [PATCH 27/56] this is the last one --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index d234c27d..8d82aba5 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -307,7 +307,7 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) - channel.chdir(self._default_path) + channel.chdir(self._cache.cwd) channel_name = uuid4().hex chan = channel.get_channel() chan.set_name(channel_name) From bcf2ffe352a95c708d4ee41eb0c016a5c2e5a693 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:38:48 +0000 Subject: [PATCH 28/56] I will go to sleep after this --- sftpretty/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 8d82aba5..440baaf3 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -307,7 +307,7 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) - channel.chdir(self._cache.cwd) + channel.chdir(None) channel_name = uuid4().hex chan = channel.get_channel() chan.set_name(channel_name) @@ -316,7 +316,7 @@ def _sftp_channel(self): try: chan.settimeout(self._timeout) - default_path = getattr(self._cache, 'cwd', None) + default_path = getattr(self._cache, 'cwd', self._default_path) if default_path: try: From ac48ad094b82a1dccebed687095ee9fde56ad50a Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:44:10 +0000 Subject: [PATCH 29/56] how could it not --- sftpretty/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 440baaf3..6fd64b45 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -307,7 +307,7 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) - channel.chdir(None) + channel.chdir(getattr(self._cache, 'cwd', self._default_path)) channel_name = uuid4().hex chan = channel.get_channel() chan.set_name(channel_name) @@ -316,7 +316,7 @@ def _sftp_channel(self): try: chan.settimeout(self._timeout) - default_path = getattr(self._cache, 'cwd', self._default_path) + default_path = getattr(self._cache, 'cwd', None) if default_path: try: From 84d98569307f573f217e016b7d422ddd44b4e85a Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:00:48 +0000 Subject: [PATCH 30/56] adding _default_path to both scenarios --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 6fd64b45..a022f3c8 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -316,7 +316,7 @@ def _sftp_channel(self): try: chan.settimeout(self._timeout) - default_path = getattr(self._cache, 'cwd', None) + default_path = getattr(self._cache, 'cwd', self._default_path) if default_path: try: From b0ca43c9dc3fccd4a0ff5abe26350c73c8e63653 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:31:33 +0000 Subject: [PATCH 31/56] setting a default cwd cache wise, then setting each threads version as well --- sftpretty/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index a022f3c8..36b6d4fa 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,11 +191,10 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() - self._cache.cwd = default_path + self._cache.__dict__.setdefault('cwd', default_path) self._channels = [] self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) - self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -302,12 +301,11 @@ def _sftp_channel(self): if not in_use and not chan.closed: channel = ch self._channels[i][1] = True - log.debug(f'Cached Thread: [{chan.get_name()}]') + log.debug(f'Cached Channel: [{chan.get_name()}]') break if channel is None: channel = SFTPClient.from_transport(self._transport) - channel.chdir(getattr(self._cache, 'cwd', self._default_path)) channel_name = uuid4().hex chan = channel.get_channel() chan.set_name(channel_name) @@ -316,7 +314,7 @@ def _sftp_channel(self): try: chan.settimeout(self._timeout) - default_path = getattr(self._cache, 'cwd', self._default_path) + self._cache.cwd, default_path = getattr(self._cache, 'cwd', None) if default_path: try: From 6af229483e192fc37b5d81e0bedcac6002bb5f7f Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:38:05 +0000 Subject: [PATCH 32/56] what I get for language hopping --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 36b6d4fa..4917bd25 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -314,7 +314,7 @@ def _sftp_channel(self): try: chan.settimeout(self._timeout) - self._cache.cwd, default_path = getattr(self._cache, 'cwd', None) + self._cache.cwd = default_path = getattr(self._cache, 'cwd', None) if default_path: try: From c63ed8880dcdeb73aeee35042ae02bcd3ea14156 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:20:45 +0000 Subject: [PATCH 33/56] refactored channel cache and default_path usage --- sftpretty/__init__.py | 62 ++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 4917bd25..42991940 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,10 +191,10 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() - self._cache.__dict__.setdefault('cwd', default_path) - self._channels = [] + self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) + self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -296,32 +296,40 @@ def _sftp_channel(self): '''Establish new SFTP channel.''' channel = None - for i, (ch, in_use) in enumerate(self._channels): - chan = ch.get_channel() - if not in_use and not chan.closed: - channel = ch - self._channels[i][1] = True - log.debug(f'Cached Channel: [{chan.get_name()}]') - break + try: + channel_name, data = next( + (key, value) + for key, value in self._channels.items() + if not value['busy'] + ) + meta = data['meta'] + if not meta.closed: + channel = data['channel'] + self._channels[channel_name]['busy'] = True + log.debug(f'Cached Channel: [{channel_name}]') + except StopIteration: + pass if channel is None: channel = SFTPClient.from_transport(self._transport) channel_name = uuid4().hex - chan = channel.get_channel() - chan.set_name(channel_name) + meta = channel.get_channel() + meta.set_name(channel_name) log.debug(f'Channel Name: [{channel_name}]') - self._channels.append([channel, True]) + self._channels[channel_name] = { + 'busy': True, 'channel': channel, 'meta': meta + } try: - chan.settimeout(self._timeout) - self._cache.cwd = default_path = getattr(self._cache, 'cwd', None) + meta.settimeout(self._timeout) + self._cache.__dict__.setdefault('cwd', self._default_path) - if default_path: + if self._cache.cwd: try: - channel.chdir(drivedrop(default_path)) - log.info(f'Current Working Directory: [{default_path}]') + channel.chdir(drivedrop(self._cache.cwd)) + log.info(f'Current Working Directory: [{self._cache.cwd}]') except IOError as err: - log.error(f'Failed Directory Change: [{default_path}]') + log.error(f'Failed Directory Change: [{self._cache.cwd}]') raise err yield channel @@ -329,11 +337,8 @@ def _sftp_channel(self): channel.close() raise err finally: - if not chan.closed: - for i, (ch, in_use) in enumerate(self._channels): - if ch == channel: - self._channels[i][1] = False - break + if not meta.closed: + self._channels[channel_name]['busy'] = False def _start_transport(self, host, port): '''Start the transport and set connection options if specified.''' @@ -1107,6 +1112,7 @@ def chdir(self, remotepath): with self._sftp_channel() as channel: channel.chdir(drivedrop(remotepath)) self._cache.cwd = drivedrop(channel.normalize('.')) + self._default_path = self._cache.cwd def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. @@ -1149,15 +1155,15 @@ def close(self): '''Terminate transport connection and clean up the bits.''' try: # Close cached channels - for channel, _ in self._channels: - if not channel.sock.closed: - channel.close() + for channel_name, data in self._channels.items(): + if not data['channel'].sock.closed: + data['channel'].close() + # Close the transport. if self._transport and self._transport.is_active(): self._transport.close() - self._cache = cache() - self._channels = [] + self._channels = {} self._transport = None # Clean up any loggers From 98daf0fb90cfce9928d2fd03d0418b240257258b Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sun, 7 Sep 2025 07:52:16 +0000 Subject: [PATCH 34/56] the blight of windows 3.13 continues --- sftpretty/__init__.py | 1 + sftpretty/helpers.py | 5 +++-- tests/common.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 42991940..b9eb2b48 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -312,6 +312,7 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) + channel.chdir(drivedrop(self._default_path)) channel_name = uuid4().hex meta = channel.get_channel() meta.set_name(channel_name) diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index f9180b9f..1487ba88 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -17,8 +17,9 @@ def _callback(filename, bytes_so_far, bytes_total, logger=None): def drivedrop(filepath): - if PureWindowsPath(filepath).drive: - filepath = Path('/').joinpath(*Path(filepath).parts[1:]).as_posix() + if filepath: + if PureWindowsPath(filepath).drive: + filepath = Path('/').joinpath(*Path(filepath).parts[1:]).as_posix() return filepath diff --git a/tests/common.py b/tests/common.py index 446e41b4..602cadd9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -28,7 +28,7 @@ def conn(sftpsrv): '''return a dictionary holding argument info for the sftpretty client''' - cnopts = CnOpts(knownhosts='sftpserver.pub') + cnopts = CnOpts(knownhosts='sftpserver.pub', log_level='debug') return {'cnopts': cnopts, 'default_path': '/home/test', 'host': sftpsrv.host, 'port': sftpsrv.port, 'private_key': 'id_sftpretty', 'private_key_pass': PASS, From accb70ca223d1de1287d5660e14edbaf8b2d64ab Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sun, 7 Sep 2025 07:58:01 +0000 Subject: [PATCH 35/56] debug log for failing tests --- tests/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index 602cadd9..7317f273 100644 --- a/tests/common.py +++ b/tests/common.py @@ -28,7 +28,8 @@ def conn(sftpsrv): '''return a dictionary holding argument info for the sftpretty client''' - cnopts = CnOpts(knownhosts='sftpserver.pub', log_level='debug') + cnopts = CnOpts(knownhosts='sftpserver.pub') + cnopts.log_level = 'debug' return {'cnopts': cnopts, 'default_path': '/home/test', 'host': sftpsrv.host, 'port': sftpsrv.port, 'private_key': 'id_sftpretty', 'private_key_pass': PASS, From ddf0c993d7ca0657b2c8e7cd435c3352d1a981fe Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 8 Sep 2025 04:12:31 +0000 Subject: [PATCH 36/56] another one --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index b9eb2b48..992d2c17 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,6 +191,7 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() + self._cache.__dict__.setdefault('cwd', default_path) self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) @@ -312,7 +313,6 @@ def _sftp_channel(self): if channel is None: channel = SFTPClient.from_transport(self._transport) - channel.chdir(drivedrop(self._default_path)) channel_name = uuid4().hex meta = channel.get_channel() meta.set_name(channel_name) From d56982ad291eae1e523c35ff8e598c9374e3286c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 13 Sep 2025 04:16:08 +0000 Subject: [PATCH 37/56] where does it have to be set? --- sftpretty/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 992d2c17..65726f52 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1111,6 +1111,7 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: + self._cache.cwd = drivedrop(channel.normalize('.')) channel.chdir(drivedrop(remotepath)) self._cache.cwd = drivedrop(channel.normalize('.')) self._default_path = self._cache.cwd From 7736a9cba24faf1d30816d131863eeb8fe306322 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 20 Sep 2025 16:57:14 +0000 Subject: [PATCH 38/56] pwd is returning something other than on 3.13, this is the fix, hopefully --- docs/changes.rst | 6 +++--- docs/cookbook.rst | 17 +++++++++-------- sftpretty/__init__.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1c1dc1f2..32b46199 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,8 +1,8 @@ -Change Log -========== -1.1.10 (current, released 2025-8-16) + +1.1.10 (current, released 2025-9-19) ----------------------------------- + * fix for channel re-use limitation. * regression fix for properly closing channel cache sockets. 1.1.9 (released 2025-8-06) diff --git a/docs/cookbook.rst b/docs/cookbook.rst index 11abc7ff..3425f96d 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -99,7 +99,7 @@ private key or password authentication. # do stuff here Config options always take precedence over parameters if both exist. Keep in -mind there will more than likely be a delta between the security option +mind there it's quite likely there will be a delta between the security option algorithms your verion of SSH supports and those supported by the underlying paramiko dependency. @@ -127,7 +127,7 @@ always attempted unless an alternative is passed. If you wish to disable host key checking, **NOT ADVISED**, you will need to modify the default CnOpts and set the knownhosts to None if no such file exists. You can still modify an existing CnOpts by setting cnopts.hostkeys to None if a default known_hosts -exists or an alternative file was passed when CnOpts was created. +exists or an alternative file was passed when the CnOpts was created. .. code-block:: python @@ -146,7 +146,7 @@ exists or an alternative file was passed when CnOpts was created. with sftpretty.Connection('host', username='me', password='pass', cnopts=cnopts): # do stuff here -To use a completely different known_hosts file, you can override CnOpts looking +To use a completely different known_hosts file, you can override CnOpts search for ``~/.ssh/known_hosts`` by specifying the file when instantiating. .. code-block:: python @@ -201,7 +201,8 @@ Just send the dict into the connection object like so. import sftpretty - cinfo = {'host':'hostname', 'username':'me', 'password':'secret', 'port':2222} + cinfo = {'host': 'hostname', 'username': 'me', + 'password': 'secret', 'port': 2222} with sftpretty.Connection(**cinfo) as sftp: # # ... do sftp operations @@ -219,7 +220,7 @@ the modification times on the local copy match those on the server. # ... sftp.get('myfile', preserve_mtime=True) -Now with the ability to resume a previously started download. Based on local +Resuming a previously initiated download is supported and based on local destination path matching. .. code-block:: python @@ -258,7 +259,7 @@ connections from above still applies. -------------------------------- In addition to the normal paramiko call, you can optionally set the ``preserve_mtime`` parameter to ``True`` and the operation will make sure that -the modification times on the server copy match those on the local. +the modification times on the server copy match those of the local. .. code-block:: python @@ -266,8 +267,8 @@ the modification times on the server copy match those on the local. # preserving modification time sftp.put('myfile', preserve_mtime=True) -Now with the ability to resume a prematurely ended upload. Based on remote -destination path matching. +Resume a prematurely ended upload if desired, still based on destination path +matching. .. code-block:: python diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 65726f52..2f0eb6ee 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1111,7 +1111,6 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: - self._cache.cwd = drivedrop(channel.normalize('.')) channel.chdir(drivedrop(remotepath)) self._cache.cwd = drivedrop(channel.normalize('.')) self._default_path = self._cache.cwd @@ -1539,6 +1538,7 @@ def pwd(self): ''' with self._sftp_channel() as channel: pwd = drivedrop(channel.normalize('.')) + self._default_path = pwd return pwd From 62ab69c452978581aeb4caebf9c1d2f2e1df70de Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 13 Oct 2025 17:02:08 +0000 Subject: [PATCH 39/56] moving .closed check out of the finally block --- sftpretty/__init__.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 2f0eb6ee..75b1d51a 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -311,17 +311,17 @@ def _sftp_channel(self): except StopIteration: pass - if channel is None: - channel = SFTPClient.from_transport(self._transport) - channel_name = uuid4().hex - meta = channel.get_channel() - meta.set_name(channel_name) - log.debug(f'Channel Name: [{channel_name}]') - self._channels[channel_name] = { - 'busy': True, 'channel': channel, 'meta': meta - } - try: + if channel is None: + channel = SFTPClient.from_transport(self._transport) + channel_name = uuid4().hex + meta = channel.get_channel() + meta.set_name(channel_name) + log.debug(f'Channel Name: [{channel_name}]') + self._channels[channel_name] = { + 'busy': True, 'channel': channel, 'meta': meta + } + meta.settimeout(self._timeout) self._cache.__dict__.setdefault('cwd', self._default_path) @@ -333,13 +333,13 @@ def _sftp_channel(self): log.error(f'Failed Directory Change: [{self._cache.cwd}]') raise err + if not meta.closed: + self._channels[channel_name]['busy'] = False + yield channel except Exception as err: channel.close() raise err - finally: - if not meta.closed: - self._channels[channel_name]['busy'] = False def _start_transport(self, host, port): '''Start the transport and set connection options if specified.''' @@ -1112,8 +1112,10 @@ def chdir(self, remotepath): ''' with self._sftp_channel() as channel: channel.chdir(drivedrop(remotepath)) - self._cache.cwd = drivedrop(channel.normalize('.')) - self._default_path = self._cache.cwd + self._default_path = drivedrop(channel.normalize('.')) + self._cache.__dict__.setdefault( + 'cwd', self._default_path + ) def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. From ac6f432caaf41aeabec1ed27d58ad9b5b52e7e0e Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:09:33 +0000 Subject: [PATCH 40/56] additional path logic included in cd to try and handle the 3.13 windows edge case --- sftpretty/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 75b1d51a..6cb18ed8 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -308,10 +308,7 @@ def _sftp_channel(self): channel = data['channel'] self._channels[channel_name]['busy'] = True log.debug(f'Cached Channel: [{channel_name}]') - except StopIteration: - pass - try: if channel is None: channel = SFTPClient.from_transport(self._transport) channel_name = uuid4().hex @@ -337,8 +334,11 @@ def _sftp_channel(self): self._channels[channel_name]['busy'] = False yield channel + except StopIteration: + pass except Exception as err: - channel.close() + if channel: + channel.close() raise err def _start_transport(self, host, port): @@ -1094,7 +1094,14 @@ def cd(self, remotepath=None): try: if remotepath is not None: - self.chdir(remotepath) + if not all ([ + PurePosixPath(remotepath).root, + PureWindowsPath(remotepath).root + ]): + cwd = Path(original_path).joinpath(remotepath).as_posix() + self.chdir(cwd) + else: + self.chdir(remotepath) yield except Exception as err: raise err From 4b1c5d8c8f3eb56bc1d91494dacd09220874eeda Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:14:48 +0000 Subject: [PATCH 41/56] turns out you have to import functions in python --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 6cb18ed8..24a5089a 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -7,7 +7,7 @@ from paramiko import (Agent, hostkeys, SFTPClient, SSHConfig, Transport, ConfigParseError, PasswordRequiredException, SSHException, ECDSAKey, Ed25519Key, RSAKey) -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from sftpretty.exceptions import (CredentialException, ConnectionException, HostKeysException, LoggingException) from sftpretty.helpers import _callback, drivedrop, hash, localtree, retry From 622cf382af2520fe9a5ccd66c815de8a22d0e4f9 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:22:03 +0000 Subject: [PATCH 42/56] stop iteration pass in a singular try catch not panning out --- sftpretty/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 24a5089a..bd22217a 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -308,7 +308,10 @@ def _sftp_channel(self): channel = data['channel'] self._channels[channel_name]['busy'] = True log.debug(f'Cached Channel: [{channel_name}]') + except StopIteration: + pass + try: if channel is None: channel = SFTPClient.from_transport(self._transport) channel_name = uuid4().hex @@ -323,19 +326,16 @@ def _sftp_channel(self): self._cache.__dict__.setdefault('cwd', self._default_path) if self._cache.cwd: - try: - channel.chdir(drivedrop(self._cache.cwd)) - log.info(f'Current Working Directory: [{self._cache.cwd}]') - except IOError as err: - log.error(f'Failed Directory Change: [{self._cache.cwd}]') - raise err + channel.chdir(drivedrop(self._cache.cwd)) + log.info(f'Current Working Directory: [{self._cache.cwd}]') if not meta.closed: self._channels[channel_name]['busy'] = False yield channel - except StopIteration: - pass + except IOError as err: + log.error(f'Failed Directory Change: [{self._cache.cwd}]') + raise err except Exception as err: if channel: channel.close() From e7f79080a3f093530d1a0544603a124fb6d24873 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 03:24:09 +0000 Subject: [PATCH 43/56] only setting default_path on new connections otherwise using self._cache.cwd --- sftpretty/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index bd22217a..dc8fe922 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,7 +191,6 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() - self._cache.__dict__.setdefault('cwd', default_path) self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) @@ -322,8 +321,10 @@ def _sftp_channel(self): 'busy': True, 'channel': channel, 'meta': meta } + if self._default_path: + self._cache.__dict__.setdefault('cwd', self._default_path) + meta.settimeout(self._timeout) - self._cache.__dict__.setdefault('cwd', self._default_path) if self._cache.cwd: channel.chdir(drivedrop(self._cache.cwd)) @@ -1119,9 +1120,8 @@ def chdir(self, remotepath): ''' with self._sftp_channel() as channel: channel.chdir(drivedrop(remotepath)) - self._default_path = drivedrop(channel.normalize('.')) self._cache.__dict__.setdefault( - 'cwd', self._default_path + 'cwd', drivedrop(channel.normalize('.') ) def chmod(self, remotepath, mode=700): @@ -1546,10 +1546,11 @@ def pwd(self): :returns: (str) Current working directory. ''' with self._sftp_channel() as channel: - pwd = drivedrop(channel.normalize('.')) - self._default_path = pwd + self._cache.__dict__.setdefault( + 'cwd', drivedrop(channel.normalize('.') + ) - return pwd + return self._cache.cwd @property def remote_server_key(self): From 501ee276f8a8d0e17af65d99523c0781308c8217 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 03:28:08 +0000 Subject: [PATCH 44/56] lint trapped --- sftpretty/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index dc8fe922..65afeee9 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1121,7 +1121,7 @@ def chdir(self, remotepath): with self._sftp_channel() as channel: channel.chdir(drivedrop(remotepath)) self._cache.__dict__.setdefault( - 'cwd', drivedrop(channel.normalize('.') + 'cwd', drivedrop(channel.normalize('.')) ) def chmod(self, remotepath, mode=700): @@ -1547,7 +1547,7 @@ def pwd(self): ''' with self._sftp_channel() as channel: self._cache.__dict__.setdefault( - 'cwd', drivedrop(channel.normalize('.') + 'cwd', drivedrop(channel.normalize('.')) ) return self._cache.cwd From d048bd4bb11961ef24c1fa1e39374d7d0521a9cc Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 04:12:02 +0000 Subject: [PATCH 45/56] looking good feeling better --- sftpretty/__init__.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 65afeee9..1dfe88c5 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -317,22 +317,17 @@ def _sftp_channel(self): meta = channel.get_channel() meta.set_name(channel_name) log.debug(f'Channel Name: [{channel_name}]') + self._cache.__dict__.setdefault('cwd', self._default_path) self._channels[channel_name] = { 'busy': True, 'channel': channel, 'meta': meta } - if self._default_path: - self._cache.__dict__.setdefault('cwd', self._default_path) - meta.settimeout(self._timeout) if self._cache.cwd: channel.chdir(drivedrop(self._cache.cwd)) log.info(f'Current Working Directory: [{self._cache.cwd}]') - if not meta.closed: - self._channels[channel_name]['busy'] = False - yield channel except IOError as err: log.error(f'Failed Directory Change: [{self._cache.cwd}]') @@ -341,6 +336,9 @@ def _sftp_channel(self): if channel: channel.close() raise err + finally: + if not meta.closed: + self._channels[channel_name]['busy'] = False def _start_transport(self, host, port): '''Start the transport and set connection options if specified.''' @@ -1095,14 +1093,7 @@ def cd(self, remotepath=None): try: if remotepath is not None: - if not all ([ - PurePosixPath(remotepath).root, - PureWindowsPath(remotepath).root - ]): - cwd = Path(original_path).joinpath(remotepath).as_posix() - self.chdir(cwd) - else: - self.chdir(remotepath) + self.chdir(remotepath) yield except Exception as err: raise err @@ -1119,7 +1110,15 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: - channel.chdir(drivedrop(remotepath)) + if not all ([ + PurePosixPath(remotepath).root, + PureWindowsPath(remotepath).root + ]) and self._cache.cwd: + cwd = Path(self._cache.cwd).joinpath(remotepath).as_posix() + else: + cwd = drivedrop(remotepath) + + channel.chdir(cwd) self._cache.__dict__.setdefault( 'cwd', drivedrop(channel.normalize('.')) ) From 3c0d9baf451c3ba779d59874d4b6180b40e40acd Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 04:22:06 +0000 Subject: [PATCH 46/56] need to set the _cache default for cwd field in the Connection init instead of _sftp_channel --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 1dfe88c5..54681ec3 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,6 +191,7 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() + self._cache.__dict__.setdefault('cwd', default_path) self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) @@ -317,7 +318,6 @@ def _sftp_channel(self): meta = channel.get_channel() meta.set_name(channel_name) log.debug(f'Channel Name: [{channel_name}]') - self._cache.__dict__.setdefault('cwd', self._default_path) self._channels[channel_name] = { 'busy': True, 'channel': channel, 'meta': meta } From 422d02b013168a54f1d85b3725bc83290e441349 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 05:29:53 +0000 Subject: [PATCH 47/56] only using setdefault once --- sftpretty/__init__.py | 41 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 54681ec3..ae6c7d3d 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -7,7 +7,7 @@ from paramiko import (Agent, hostkeys, SFTPClient, SSHConfig, Transport, ConfigParseError, PasswordRequiredException, SSHException, ECDSAKey, Ed25519Key, RSAKey) -from pathlib import Path, PurePosixPath, PureWindowsPath +from pathlib import Path from sftpretty.exceptions import (CredentialException, ConnectionException, HostKeysException, LoggingException) from sftpretty.helpers import _callback, drivedrop, hash, localtree, retry @@ -195,7 +195,6 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) - self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -311,17 +310,17 @@ def _sftp_channel(self): except StopIteration: pass - try: - if channel is None: - channel = SFTPClient.from_transport(self._transport) - channel_name = uuid4().hex - meta = channel.get_channel() - meta.set_name(channel_name) - log.debug(f'Channel Name: [{channel_name}]') - self._channels[channel_name] = { - 'busy': True, 'channel': channel, 'meta': meta - } + if channel is None: + channel = SFTPClient.from_transport(self._transport) + channel_name = uuid4().hex + meta = channel.get_channel() + meta.set_name(channel_name) + log.debug(f'Channel Name: [{channel_name}]') + self._channels[channel_name] = { + 'busy': True, 'channel': channel, 'meta': meta + } + try: meta.settimeout(self._timeout) if self._cache.cwd: @@ -1110,18 +1109,8 @@ def chdir(self, remotepath): :raises: IOError, if path does not exist ''' with self._sftp_channel() as channel: - if not all ([ - PurePosixPath(remotepath).root, - PureWindowsPath(remotepath).root - ]) and self._cache.cwd: - cwd = Path(self._cache.cwd).joinpath(remotepath).as_posix() - else: - cwd = drivedrop(remotepath) - - channel.chdir(cwd) - self._cache.__dict__.setdefault( - 'cwd', drivedrop(channel.normalize('.')) - ) + channel.chdir(drivedrop(remotepath)) + self._cache.cwd = drivedrop(channel.normalize('.')) def chmod(self, remotepath, mode=700): '''Set the permission mode of a remotepath, where mode is an octal. @@ -1545,9 +1534,7 @@ def pwd(self): :returns: (str) Current working directory. ''' with self._sftp_channel() as channel: - self._cache.__dict__.setdefault( - 'cwd', drivedrop(channel.normalize('.')) - ) + self._cache.cwd = drivedrop(channel.normalize('.')) return self._cache.cwd From 16ab107cff0e673c1e18e8a8ed7a9af33226cf61 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:27:41 +0000 Subject: [PATCH 48/56] setdefault at Connection init not enough to set for reused threads --- sftpretty/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index ae6c7d3d..6302a424 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -191,10 +191,10 @@ def __init__(self, host, cnopts=None, default_path=None, password=None, port=22, private_key=None, private_key_pass=None, timeout=None, username=None): self._cache = cache() - self._cache.__dict__.setdefault('cwd', default_path) self._channels = {} self._cnopts = cnopts or CnOpts() self._config = self._cnopts.get_config(host) + self._default_path = default_path self._set_logging() self._timeout = self._config.get('connecttimeout') or timeout self._transport = None @@ -322,10 +322,13 @@ def _sftp_channel(self): try: meta.settimeout(self._timeout) + self._cache.__dict__.setdefault('cwd', self._default_path) if self._cache.cwd: channel.chdir(drivedrop(self._cache.cwd)) - log.info(f'Current Working Directory: [{self._cache.cwd}]') + else: + self._cache.cwd = '/' + log.info(f'Current Working Directory: [{self._cache.cwd}]') yield channel except IOError as err: From 75c3da68006cd7a1eb835960b90d5a20eab6cf97 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:11:56 +0000 Subject: [PATCH 49/56] using cache.cwd in get tree operations --- sftpretty/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 6302a424..56024c90 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -566,6 +566,7 @@ def get_d(self, remotedir, localdir, callback=None, :raises: Any exception raised by operations will be passed through. ''' + remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() filelist = self.listdir_attr(remotedir) if not Path(localdir).is_dir(): @@ -669,10 +670,10 @@ def get_r(self, remotedir, localdir, callback=None, :raises: Any exception raised by operations will be passed through. ''' - self.chdir(remotedir) + remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() lwd = Path(localdir).absolute().as_posix() - rwd = self._cache.cwd + rwd = remotedir tree = {} tree[rwd] = [(rwd, lwd)] From f665b8f16dc1781f8bb10a5cbee8c01a7b65736c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:18:16 +0000 Subject: [PATCH 50/56] making sure the _cache.cwd attribute is in scope for get_r --- sftpretty/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 56024c90..8336e198 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -670,7 +670,8 @@ def get_r(self, remotedir, localdir, callback=None, :raises: Any exception raised by operations will be passed through. ''' - remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() + with self._sftp_channel() as channel: + remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() lwd = Path(localdir).absolute().as_posix() rwd = remotedir From 493a07e0d34ae9a51c0fbf24bfad2e19772c77c6 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:47:47 +0000 Subject: [PATCH 51/56] fix the pwd expectation in get_r tests now that we utilize _cache.cwd --- tests/test_get_r.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/test_get_r.py b/tests/test_get_r.py index 8b3132e6..c4dfa0b3 100644 --- a/tests/test_get_r.py +++ b/tests/test_get_r.py @@ -11,12 +11,13 @@ def test_get_r(sftpserver): with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: localpath = Path(mkdtemp()).as_posix() - sftp.get_r('.', localpath) + remotepath = '.' + sftp.get_r(remotepath, localpath) local_tree = {} remote_tree = {} - remote_cwd = sftp.pwd + remote_cwd = Path(sftp.pwd).joinpath(remotepath).as_posix() localtree(local_tree, localpath, remote_cwd) sftp.remotetree(remote_tree, remote_cwd, localpath) @@ -35,12 +36,13 @@ def test_get_r_pwd(sftpserver): with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: localpath = Path(mkdtemp()).as_posix() - sftp.get_r('pub/foo2', localpath) + remotepath = 'pub/foo2' + sftp.get_r(remotepath, localpath) local_tree = {} remote_tree = {} - remote_cwd = sftp.pwd + remote_cwd = Path(sftp.pwd).joinpath(remotepath).as_posix() localtree(local_tree, localpath, remote_cwd) sftp.remotetree(remote_tree, remote_cwd, localpath) @@ -58,14 +60,15 @@ def test_get_r_pathed(sftpserver): '''test the get_r for localpath, starting deeper then pwd ''' with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: - sftp.chdir('pub/foo2') localpath = Path(mkdtemp()).as_posix() - sftp.get_r('./bar1', localpath) + remotepath = './bar1' + sftp.chdir('pub/foo2') + sftp.get_r(remotepath, localpath) local_tree = {} remote_tree = {} - remote_cwd = sftp.pwd + remote_cwd = Path(sftp.pwd).joinpath(remotepath).as_posix() localtree(local_tree, localpath, remote_cwd) sftp.remotetree(remote_tree, remote_cwd, localpath) @@ -86,13 +89,14 @@ def test_get_r_cdd(sftpserver): with sftpserver.serve_content(VFS): with Connection(**conn(sftpserver)) as sftp: localpath = Path(mkdtemp()).as_posix() + remotepath = '.' sftp.chdir('pub/foo2') - sftp.get_r('.', localpath) + sftp.get_r(remotepath, localpath) local_tree = {} remote_tree = {} - remote_cwd = sftp.pwd + remote_cwd = Path(sftp.pwd).joinpath(remotepath).as_posix() localtree(local_tree, localpath, remote_cwd) sftp.remotetree(remote_tree, remote_cwd, localpath) From 882cf5d958cf492d04e63d34222174b5fa6a1210 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 19:26:40 +0000 Subject: [PATCH 52/56] updating release date, and removing unused variable --- docs/changes.rst | 4 +--- sftpretty/__init__.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 32b46199..77ea3f1d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,4 @@ - - -1.1.10 (current, released 2025-9-19) +1.1.10 (current, released 2025-10-14) ----------------------------------- * fix for channel re-use limitation. * regression fix for properly closing channel cache sockets. diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 8336e198..020ea528 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -670,7 +670,7 @@ def get_r(self, remotedir, localdir, callback=None, :raises: Any exception raised by operations will be passed through. ''' - with self._sftp_channel() as channel: + with self._sftp_channel(): remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() lwd = Path(localdir).absolute().as_posix() From da1660e4e1d4f1801d9957c64c7b996b09aacc8d Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:11:51 +0000 Subject: [PATCH 53/56] updating workflow until upstream is settled, moving to new macos-15-intel runner for intel based mac testing --- .github/workflows/test.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 590ea12b..a8dbb4cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,13 +12,11 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-13, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] + os: [macos-15-intel, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] exclude: - - os: macos-latest - python-version: 3.7 - - os: ubuntu-latest - python-version: 3.7 + - os: windows-latest + python-version: 3.13 steps: - name: Clone Repository From 9e10298077c6c7cc800fd75e9e2f7b6c662911fd Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:15:53 +0000 Subject: [PATCH 54/56] adding pythong 3.14 to test suite --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8dbb4cf..678a5402 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,8 +13,10 @@ jobs: fail-fast: false matrix: os: [macos-15-intel, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] exclude: + - os: ubuntu-latest + python-version: 3.7 - os: windows-latest python-version: 3.13 From 45fbf70a4d8efe03fc9d9b1a5d8e013ca8c3ae7b Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:20:40 +0000 Subject: [PATCH 55/56] updating readme and pyproject to reflect 3.14 testing, removed a macos and ubuntu exclude for 3.7 that is still needed in test workflow --- .github/workflows/test.yml | 2 ++ README.rst | 2 +- docs/index.rst | 2 +- pyproject.toml | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 678a5402..f20d76e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ jobs: os: [macos-15-intel, macos-latest, ubuntu-22.04, ubuntu-latest, windows-latest] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] exclude: + - os: macos-latest + python-version: 3.7 - os: ubuntu-latest python-version: 3.7 - os: windows-latest diff --git a/README.rst b/README.rst index 9ecb6991..8f4f2297 100644 --- a/README.rst +++ b/README.rst @@ -138,6 +138,6 @@ paramiko >= 2.7.0 Supports -------- -Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 +Tested on Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 diff --git a/docs/index.rst b/docs/index.rst index 6c4ed36b..ca1339c2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -138,7 +138,7 @@ paramiko >= 2.7.0 Supports -------- -Tested on Python 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 +Tested on Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 Contents -------- diff --git a/pyproject.toml b/pyproject.toml index 53c98f4a..6a158c49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython' ] dependencies = [ From 107d5176a70925f58c474f2d379a790faa90d879 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:24:52 +0000 Subject: [PATCH 56/56] same issue exists in 3.14 windows --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f20d76e6..daace509 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,8 @@ jobs: python-version: 3.7 - os: windows-latest python-version: 3.13 + - os: windows-latest + python-version: 3.14 steps: - name: Clone Repository