diff --git a/LICENSE b/LICENSE index df010701..bbf3c269 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2021, byteskeptical +Copyright (c) 2026, byteskeptical All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/changes.rst b/docs/changes.rst index 3073d4ed..4feeff51 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,7 +1,13 @@ -1.2.0 (current, released 2025-12-21) ------------------------------------- +1.2.1 (current, released 2026-2-11) +----------------------------------- + * adding boolean to rename for switch between posix and standard behavior. + * change in default path behavior where cwd is set when default path is not. + * update drivedrop to better handle all non-UNC Windows path possibilities. + +1.2.0 (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. + * fix for parsing limitation of .stem causing issue with dots in path names. 1.1.11 (released 2025-10-22) ---------------------------- diff --git a/docs/conf.py b/docs/conf.py index 1ec0b7fd..459455df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,16 +47,16 @@ # General information about the project. project = u'sftpretty' -copyright = u'2025, byteskeptical' +copyright = u'2026, 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.2.0' +version = '1.2.1' # The full version, including alpha/beta/rc tags. -release = '1.2.0' +release = '1.2.1' # 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 18381556..5f917831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ keywords = [ name = 'sftpretty' readme = 'README.rst' requires-python = '>=3.6' -version = '1.2.0' +version = '1.2.1' [project.scripts] sftpretty = 'sftpretty:Connection' diff --git a/sftpretty/__init__.py b/sftpretty/__init__.py index 5b313c32..c566b5c6 100644 --- a/sftpretty/__init__.py +++ b/sftpretty/__init__.py @@ -328,10 +328,10 @@ def _sftp_channel(self): meta.settimeout(self._timeout) self._cache.__dict__.setdefault('cwd', self._default_path) - if self._cache.cwd: - channel.chdir(drivedrop(self._cache.cwd)) - else: - self._cache.cwd = '/' + if self._cache.cwd is None: + self._cache.cwd = drivedrop(channel.normalize('.')) + + channel.chdir(drivedrop(self._cache.cwd)) log.info(f'Current Working Directory: [{self._cache.cwd}]') yield channel @@ -643,7 +643,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() + remotedir = self.normalize(remotedir) filelist = self.listdir_attr(remotedir) if not Path(localdir).is_dir(): @@ -747,11 +747,8 @@ def get_r(self, remotedir, localdir, callback=None, :raises: Any exception raised by operations will be passed through. ''' - with self._sftp_channel(): - remotedir = Path(self._cache.cwd).joinpath(remotedir).as_posix() - lwd = Path(localdir).absolute().as_posix() - rwd = remotedir + rwd = self.normalize(remotedir) tree = {} tree[rwd] = [(rwd, lwd)] @@ -960,6 +957,7 @@ def put_d(self, localdir, remotedir, callback=None, confirm=True, :raises OSError: if localdir doesn't exist ''' localdir = Path(localdir) + remotedir = self.normalize(remotedir) self.mkdir_p(Path(remotedir).joinpath(localdir.parts[-1]).as_posix()) @@ -1430,9 +1428,9 @@ def normalize(self, remotepath): :raises: IOError, if remotepath can't be resolved ''' with self._sftp_channel() as channel: - expanded_path = channel.normalize(drivedrop(remotepath)) + absolute = channel.normalize(drivedrop(remotepath)) - return drivedrop(expanded_path) + return drivedrop(absolute) def open(self, remotefile, bufsize=-1, mode='r'): '''Open a file on the remote server. @@ -1511,19 +1509,22 @@ def remove(self, remotefile): with self._sftp_channel() as channel: channel.remove(drivedrop(remotefile)) - def rename(self, remotepath, newpath): + def rename(self, remotepath, newpath, posix=True): '''Rename a path on the remote host. :param str remotepath: Remote path to rename. - :param str newpath: New name for remote path. + :param bool posix: *Default: True* - If set uses posix rename + extension behavior from OpenSSH otherwise fallback to standard + SFTP rename behavior. :returns: None :raises: IOError ''' with self._sftp_channel() as channel: - channel.posix_rename(drivedrop(remotepath), drivedrop(newpath)) + renamer = channel.posix_rename if posix else channel.rename + renamer(drivedrop(remotepath), drivedrop(newpath)) def rmdir(self, remotedir): '''Delete remote directory. diff --git a/sftpretty/helpers.py b/sftpretty/helpers.py index 4ed65795..8233c246 100644 --- a/sftpretty/helpers.py +++ b/sftpretty/helpers.py @@ -18,9 +18,13 @@ def _callback(filename, bytes_so_far, bytes_total, logger=None): def drivedrop(filepath): if filepath: - if PureWindowsPath(filepath).drive: + if PureWindowsPath(filepath).drive and not filepath.startswith('//'): filepath = PurePosixPath('/').joinpath( - *PurePosixPath(filepath).parts[1:]).as_posix() + *PureWindowsPath(filepath).parts[1:]).as_posix() + filepath = filepath.encode('unicode_escape').decode() + filepath = filepath.replace('\\', '/').replace('//', '/') + elif filepath.startswith('//'): + filepath = PurePosixPath(filepath.replace('//', '/')).as_posix() return filepath diff --git a/tests/test_getcwd.py b/tests/test_getcwd.py index 9caaadf6..4e922e69 100644 --- a/tests/test_getcwd.py +++ b/tests/test_getcwd.py @@ -11,7 +11,7 @@ def test_getcwd_none(sftpserver): cnn = conn(sftpserver) cnn['default_path'] = None with Connection(**cnn) as sftp: - assert sftp.getcwd() is None + assert sftp.getcwd() == '/' def test_getcwd_default_path(sftpserver): diff --git a/tests/test_issue_65.py b/tests/test_issue_65.py index 46c82fb0..99d86654 100644 --- a/tests/test_issue_65.py +++ b/tests/test_issue_65.py @@ -14,7 +14,7 @@ def test_issue_65(sftpserver): cnn = conn(sftpserver) cnn['default_path'] = None with Connection(**cnn) as sftp: - assert sftp.getcwd() is None + assert sftp.getcwd() == '/' with sftp.cd(pubpath.as_posix()): pass diff --git a/tests/test_rename.py b/tests/test_rename.py index e10d5960..f559a5f1 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -13,10 +13,14 @@ def test_rename(lsftp): lsftp.remove(base_fname) assert base_fname not in lsftp.listdir() lsftp.put(fname) - lsftp.rename(base_fname, 'bob') + lsftp.rename(base_fname, 'alice') rdirs = lsftp.listdir() - assert 'bob' in rdirs + assert 'alice' in rdirs assert base_fname not in rdirs + lsftp.rename('alice', 'bob', posix=False) + rdirs = lsftp.listdir() + assert 'alice' not in rdirs + assert 'bob' in rdirs lsftp.remove('bob')