From 0fe4e437edeeb249316f3a9bf7178af42f8e8850 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:31:50 +0000 Subject: [PATCH 01/10] quality of life improvement to exception handling in _sftp_channel. version bump. --- docs/changes.rst | 10 ++-- docs/conf.py | 6 +-- pyproject.toml | 2 +- sftpretty/__init__.py | 104 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 77ea3f1d..f8313cef 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,10 +1,14 @@ -1.1.10 (current, released 2025-10-14) ------------------------------------ +1.1.11 (current, released 2025-10-22) +------------------------------------- + * Improved exception handling in _sftp_channel. + +1.1.10 (released 2025-10-14) +---------------------------- * fix for channel re-use limitation. * regression fix for properly closing channel cache sockets. 1.1.9 (released 2025-8-06) ------------------------------------ +-------------------------- * 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. diff --git a/docs/conf.py b/docs/conf.py index b3fee7c5..fca8b50c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,16 +47,16 @@ # General information about the project. project = u'sftpretty' -copyright = u'2020, byteskeptical' +copyright = u'2025, byteskeptical' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.1.10' +version = '1.1.11' # The full version, including alpha/beta/rc tags. -release = '1.1.10' +release = '1.1.11' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyproject.toml b/pyproject.toml index 6a158c49..9058367f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.1.10' +version = '1.1.11' [project.scripts] sftpretty = 'sftpretty:Connection' diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 020ea528..709554b2 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -295,6 +295,7 @@ def _set_username(self, username): def _sftp_channel(self): '''Establish new SFTP channel.''' channel = None + fatal = False try: channel_name, data = next( @@ -310,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) @@ -331,15 +332,88 @@ def _sftp_channel(self): log.info(f'Current Working Directory: [{self._cache.cwd}]') yield channel - except IOError as err: - log.error(f'Failed Directory Change: [{self._cache.cwd}]') + except socket.timeout: + fatal = True + _message = ( + f'Channel [{channel_name}] operation timed out after ' + f'{self._timeout}s while accessing: [{self._cache.cwd}]' + ) + log.error(_message) + raise TimeoutError(_message) + except SFTPError as err: + _message_map = { + SFTP_FAILURE: ( + 'A generic failure occurred on the SFTP server for path: ' + f'[{self._cache.cwd}]' + ), + SFTP_NO_SUCH_FILE: ( + f'Directory or file does not exist: [{self._cache.cwd}]' + ), + SFTP_OP_UNSUPPORTED: ( + 'Operation (e.g., chdir) unsupported by server for path: ' + f'[{self._cache.cwd}]' + ), + SFTP_PERMISSION_DENIED: ( + f'Permission denied for: [{self._cache.cwd}]' + ), + } + _message = _message_map.get( + err.errno, + ('Unhandled SFTP error on directory change to ' + f'[{self._cache.cwd}] (Code {err.errno}): {err}') + ) + log.error(_message) + raise err + except ChannelException as err: + fatal = True + log.error(f'Channel [{channel_name}] is invalid or closed: {err}') + raise err + except SSHException as err: + fatal = True + log.error( + (f'Protocol error occurred during channel [{channel_name}] ' + f'setup: {err}') + ) + raise err + except OSError as err: + fatal = True + _message = (f'Channel [{channel_name}] experienced an OS-level network error ' + f'(Code: {err.errno} - {errno.errorcode.get(err.errno)}): ' + f'{err}') + + if err.errno == errno.ECONNRESET: + _message = ( + f'Channel [{channel_name}] connection forcefully reset by ' + f'the remote host: {err}' + ) + elif err.errno == errno.EPIPE: + _message = ( + f'Channel [{channel_name}] connection was broken ' + f'(broken pipe): {err}' + ) + + log.error(_message) raise err except Exception as err: - if channel: - channel.close() + err_type = type(err).__name__ + fatal = True + log.error( + (f'An unexpected error of type [{err_type}] occurred in channel ' + f'[{channel_name}]: {err}') + ) raise err finally: - if not meta.closed: + if fatal and channel: + channel.close() + log.debug( + (f'Closed compromised channel [{channel_name}] due to ' + 'fatal error!') + ) + self._channels.pop(channel_name, None) + elif channel and not meta.closed: + log.debug( + f'Recycling channel [{channel_name}] back to the pool.' + ) self._channels[channel_name]['busy'] = False def _start_transport(self, host, port): From 640ec98ddee5e4229d04f22f3856baa4294110eb Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:14:23 +0000 Subject: [PATCH 02/10] updating contribution instructions and fixing left over lint --- docs/contributing.rst | 14 ++++++++------ sftpretty/__init__.py | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 677d5ec1..c805c1fb 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -13,13 +13,15 @@ Code #. Fork the repository `sftpretty `_ #. Install supporting software packages and sftpretty in --editable mode - a. Make a virtualenv, clone the repos, install the deps from pip install -r requirements-dev.txt - b. Install sftpretty in editable mode, pip install -e . + a. Make a virtualenv, python3 -m venv .sftpretty + b. Clone the repo, git clone https://github.com/`username`/sftpretty + c. Install sftpretty and it's dependencies in editable mode, python3 -m pip install -e .[dev,lint,test] + #. Write any new tests needed and ensure existing tests continue to pass without modification. - a. Setup CI testing for your Fork. Currently testing is done on Github Actions but feel free to use the testing framework of your choosing. - b. Testing features that concern chmod, chown on Windows is NOT supported. Testing compression has to be ran against a local compatible sshd and not the plugin as it does NOT support this test. - c. You will need to setup an ssh daemon on your local machine and create a user: copy the contents of id_sftpretty.pub to the newly created user's authorized_keys file -- Tests that can only be run locally are skipped using the @skip_if_ci decorator so they don't fail when the test suite is run on the CI server. + a. Setup CI testing for your fork. Currently testing is done on Github Actions but feel free to use the framework of your choosing. + b. Testing features that concern chmod, chown on Windows is NOT supported. Testing compression has to be ran against a local compatible sshd and not the pytest-sftpserver plugin as it does NOT support this feature. + c. You will need to setup an ssh daemon on your local machine and create a user: copy the contents of id_sftpretty.pub to the newly created user's authorized_keys file -- Tests that can only be ran locally are skipped using the @skip_if_ci decorator so they don't fail when the test suite runs on the CI server. #. Ensure that your name is added to the end of the :doc:`authors` file using the format Name (url), where the (url) portion is optional. #. Submit a Pull Request to the project. @@ -40,4 +42,4 @@ This section lists the priority that will be assigned to an issue: Testing ------- -Tests specific to an issue should be put in the tests/ directory and the module should be named test_issue_xx.py The tests within that module should be named test_issue_xx or test_issue_xx_YYYYYY if more than one test. Pull requests should not modify existing tests (exceptions apply). See tests/test_issue_xx.py for a template and further explanation. +Tests specific to an issue should be placed inside the tests/ directory and the file should be named test_issue_xx.py. The tests within that module should be named test_issue_xx or test_issue_xx_YYYYYY if more than one test exists. Pull requests should not modify existing tests with extremely rare exception. See tests/test_issue_xx.py for a template and additional context. diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 709554b2..597ea5f5 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1,17 +1,20 @@ from concurrent.futures import as_completed, ThreadPoolExecutor from contextlib import contextmanager +from errno ECONNRESET, EPIPE, errorcode from functools import partial from logging import (DEBUG, ERROR, FileHandler, Formatter, getLogger, INFO, StreamHandler, WARN) from os import environ, SEEK_END, utime -from paramiko import (Agent, hostkeys, SFTPClient, SSHConfig, Transport, - ConfigParseError, PasswordRequiredException, - SSHException, ECDSAKey, Ed25519Key, RSAKey) +from paramiko import (Agent, ChannelException, ConfigParseError, ECDSAKey, + Ed25519Key, hostkeys, PasswordRequiredException, + SFTPClient, SFTPError, SFTP_FAILURE, SFTP_NO_SUCH_FILE, + SFTP_OP_UNSUPPORTED, SFTP_PERMISSION_DENIED, SSHConfig, + SSHException, RSAKey, Transport) from pathlib import Path from sftpretty.exceptions import (CredentialException, ConnectionException, HostKeysException, LoggingException) from sftpretty.helpers import _callback, drivedrop, hash, localtree, retry -from socket import gaierror +from socket import gaierror, timeout from stat import S_ISDIR, S_ISREG from tempfile import mkstemp from threading import local as cache @@ -332,7 +335,7 @@ def _sftp_channel(self): log.info(f'Current Working Directory: [{self._cache.cwd}]') yield channel - except socket.timeout: + except timeout: fatal = True _message = ( f'Channel [{channel_name}] operation timed out after ' @@ -378,15 +381,15 @@ def _sftp_channel(self): except OSError as err: fatal = True _message = (f'Channel [{channel_name}] experienced an OS-level network error ' - f'(Code: {err.errno} - {errno.errorcode.get(err.errno)}): ' + f'(Code: {err.errno} - {errorcode.get(err.errno)}): ' f'{err}') - if err.errno == errno.ECONNRESET: + if err.errno == ECONNRESET: _message = ( f'Channel [{channel_name}] connection forcefully reset by ' f'the remote host: {err}' ) - elif err.errno == errno.EPIPE: + elif err.errno == EPIPE: _message = ( f'Channel [{channel_name}] connection was broken ' f'(broken pipe): {err}' From 002d758185badc4d611c837bb9e773a50fe7f422 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:16:05 +0000 Subject: [PATCH 03/10] you know what I meant python --- sftpretty/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 597ea5f5..11fc09ca 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1,6 +1,6 @@ from concurrent.futures import as_completed, ThreadPoolExecutor from contextlib import contextmanager -from errno ECONNRESET, EPIPE, errorcode +from errno import ECONNRESET, EPIPE, errorcode from functools import partial from logging import (DEBUG, ERROR, FileHandler, Formatter, getLogger, INFO, StreamHandler, WARN) From 1638ebeb6f225dbb745b6551bf4e22932daa4a51 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:58:24 +0000 Subject: [PATCH 04/10] lint trap --- sftpretty/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 11fc09ca..85976522 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -380,20 +380,20 @@ def _sftp_channel(self): raise err except OSError as err: fatal = True - _message = (f'Channel [{channel_name}] experienced an OS-level network error ' - f'(Code: {err.errno} - {errorcode.get(err.errno)}): ' - f'{err}') + _message = (f'Channel [{channel_name}] experienced an OS-level ' + f'network error (Code: {err.errno} - ' + f'{errorcode.get(err.errno)}): {err}') if err.errno == ECONNRESET: _message = ( f'Channel [{channel_name}] connection forcefully reset by ' f'the remote host: {err}' - ) + ) elif err.errno == EPIPE: _message = ( f'Channel [{channel_name}] connection was broken ' f'(broken pipe): {err}' - ) + ) log.error(_message) raise err @@ -401,8 +401,8 @@ def _sftp_channel(self): err_type = type(err).__name__ fatal = True log.error( - (f'An unexpected error of type [{err_type}] occurred in channel ' - f'[{channel_name}]: {err}') + (f'An unexpected error of type [{err_type}] occurred in ' + f'channel [{channel_name}]: {err}') ) raise err finally: From 6315fda364bfb15f2d55e6035c0777c771161a01 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 10 Dec 2025 05:15:50 +0000 Subject: [PATCH 05/10] .stem usage causing failures when path names have dots, switching to .parts --- docs/changes.rst | 8 ++++++-- docs/conf.py | 4 ++-- pyproject.toml | 2 +- sftpretty/__init__.py | 8 ++++---- sftpretty/helpers.py | 3 ++- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f8313cef..22052f4e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,6 +1,10 @@ -1.1.11 (current, released 2025-10-22) +1.1.12 (current, released 2025-12-11) ------------------------------------- - * Improved exception handling in _sftp_channel. + * fix for parsing limitation of .stem causing issues with dots in path names. + +1.1.11 (released 2025-10-22) +---------------------------- + * improved exception handling in _sftp_channel. 1.1.10 (released 2025-10-14) ---------------------------- diff --git a/docs/conf.py b/docs/conf.py index fca8b50c..269d1070 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = '1.1.11' +version = '1.1.12' # The full version, including alpha/beta/rc tags. -release = '1.1.11' +release = '1.1.12' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyproject.toml b/pyproject.toml index 9058367f..5e4d6aef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.1.11' +version = '1.1.12' [project.scripts] sftpretty = 'sftpretty:Connection' diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 85976522..3db55127 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -961,7 +961,7 @@ def put_d(self, localdir, remotedir, callback=None, confirm=True, ''' localdir = Path(localdir) - self.mkdir_p(Path(remotedir).joinpath(localdir.stem).as_posix()) + self.mkdir_p(Path(remotedir).joinpath(localdir.parts[-1]).as_posix()) paths = [ (localpath.as_posix(), @@ -1409,11 +1409,11 @@ def mkdir_p(self, remotedir, mode=700): 'already exists.')) else: parent = Path(remotedir).parent.as_posix() - stem = Path(remotedir).stem + stem = Path(remotedir).parts[-1] if parent != remotedir: if not self.isdir(parent): self.mkdir_p(parent, mode=mode) - if stem: + if stem and stem != Path(remotedir).root: self.mkdir(remotedir, mode=mode) except Exception as err: raise err @@ -1487,7 +1487,7 @@ def remotetree(self, container, remotedir, localdir, recurse=True): remote = Path(remotedir).joinpath( attribute.filename).as_posix() local = Path(localdir).joinpath( - Path(remote).stem).as_posix() + Path(remote).parts[-1]).as_posix() if remotedir in container.keys(): container[remotedir].append((remote, local)) else: diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index 1487ba88..e17a2e73 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -88,7 +88,8 @@ def localtree(container, localdir, remotedir, recurse=True): for localpath in Path(localdir).iterdir(): if localpath.is_dir(): local = localpath.as_posix() - remote = Path(remotedir).joinpath(localpath.stem).as_posix() + stem = localpath.parts[-1] + remote = Path(remotedir).joinpath(stem).as_posix() if localdir.as_posix() in container.keys(): container[localdir.as_posix()].append((local, remote)) else: From 04ce190d17ecf9dc430fe9f02d076a06edc34dff Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:29:28 +0000 Subject: [PATCH 06/10] fix for put_* --- sftpretty/helpers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index e17a2e73..e1cea44b 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -89,7 +89,8 @@ def localtree(container, localdir, remotedir, recurse=True): if localpath.is_dir(): local = localpath.as_posix() stem = localpath.parts[-1] - remote = Path(remotedir).joinpath(stem).as_posix() + remote = Path(remotedir).joinpath(localpath.relative_to( + localdir).as_posix()).as_posix() if localdir.as_posix() in container.keys(): container[localdir.as_posix()].append((local, remote)) else: From 1769dfca33d417365a68f1f1d658c683436c6453 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 19 Dec 2025 03:09:42 +0000 Subject: [PATCH 07/10] bumping major version, removing unused variable, enabling windows test on 3.13 & 3.14 --- .github/workflows/test.yml | 4 ---- docs/changes.rst | 4 ++-- docs/conf.py | 4 ++-- pyproject.toml | 2 +- sftpretty/helpers.py | 1 - 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daace509..c3d3f1ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,6 @@ jobs: python-version: 3.7 - os: ubuntu-latest python-version: 3.7 - - os: windows-latest - python-version: 3.13 - - os: windows-latest - python-version: 3.14 steps: - name: Clone Repository diff --git a/docs/changes.rst b/docs/changes.rst index 22052f4e..b34bf1f3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,5 +1,5 @@ -1.1.12 (current, released 2025-12-11) -------------------------------------- +1.2.0 (current, released 2025-12-19) +------------------------------------ * fix for parsing limitation of .stem causing issues with dots in path names. 1.1.11 (released 2025-10-22) diff --git a/docs/conf.py b/docs/conf.py index 269d1070..1ec0b7fd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = '1.1.12' +version = '1.2.0' # The full version, including alpha/beta/rc tags. -release = '1.1.12' +release = '1.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pyproject.toml b/pyproject.toml index 5e4d6aef..18381556 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.1.12' +version = '1.2.0' [project.scripts] sftpretty = 'sftpretty:Connection' diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index e1cea44b..81ec418b 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -88,7 +88,6 @@ def localtree(container, localdir, remotedir, recurse=True): for localpath in Path(localdir).iterdir(): if localpath.is_dir(): local = localpath.as_posix() - stem = localpath.parts[-1] remote = Path(remotedir).joinpath(localpath.relative_to( localdir).as_posix()).as_posix() if localdir.as_posix() in container.keys(): From 0e398c7f674ed559e59216e512369b39f0c2d975 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:34:09 +0000 Subject: [PATCH 08/10] forcing posix compliance --- sftpretty/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index 81ec418b..3701b4b9 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -1,7 +1,7 @@ from functools import wraps from hashlib import new, sha3_512 from io import BytesIO, IOBase -from pathlib import Path, PureWindowsPath +from pathlib import Path, PurePosixPath, PureWindowsPath from stat import S_IMODE from time import sleep @@ -19,7 +19,8 @@ def _callback(filename, bytes_so_far, bytes_total, logger=None): def drivedrop(filepath): if filepath: if PureWindowsPath(filepath).drive: - filepath = Path('/').joinpath(*Path(filepath).parts[1:]).as_posix() + filepath = PurePosixPath('/').joinpath( + *Path(filepath).parts[1:]).as_posix() return filepath From 9485aaeb2eb67c3c4585a50d2eab9504229fd4b6 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:11:53 +0000 Subject: [PATCH 09/10] probably should have changed all the Path calls --- sftpretty/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index 3701b4b9..4ed65795 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -20,7 +20,7 @@ def drivedrop(filepath): if filepath: if PureWindowsPath(filepath).drive: filepath = PurePosixPath('/').joinpath( - *Path(filepath).parts[1:]).as_posix() + *PurePosixPath(filepath).parts[1:]).as_posix() return filepath From 81ed5723e3ec50ea401d04ed60486d4c314f991d Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:38:52 +0000 Subject: [PATCH 10/10] adding a drivedrip on .getcwd() return due to current paramiko behavior on Windows --- docs/changes.rst | 3 ++- sftpretty/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b34bf1f3..3073d4ed 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,5 +1,6 @@ -1.2.0 (current, released 2025-12-19) +1.2.0 (current, released 2025-12-21) ------------------------------------ + * fix for change in pathlib behavior in Python 3.13+ on Windows. * fix for parsing limitation of .stem causing issues with dots in path names. 1.1.11 (released 2025-10-22) diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 3db55127..5b313c32 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -1280,7 +1280,7 @@ def getcwd(self): :returns: (str) Remote current working directory. None, if not set. ''' with self._sftp_channel() as channel: - cwd = channel.getcwd() + cwd = drivedrop(channel.getcwd()) return cwd