Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Contributors of code, tests and documentation to the project who have agreed to

Acknowledgment
--------------
* PR #84 by miraz12
* PR #80 & #81 by shashfrankenstien
* PR #46 by ofiliojo
* PR #19 by coreyhartley
* PR #15 by RobertCochran
Expand Down
11 changes: 10 additions & 1 deletion docs/changes.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
1.2.2 (current, released 2026-5-10)
1.2.3 (current, released 2026-9-08)
-----------------------------------
* adding new tests for connections, hash, drivepath and read-only servers.
* reworking tests to incorporate self-cleaning of all artifacts.
* fix for error handling catches in _set_authentication function.
* fix for github workflow so all tests can run from fork PRs.
* fix for hash function return that was breaking host key verification.
* use paramiko's compression algorithm list by default instead of None.

1.2.2 (released 2026-5-10)
--------------------------
* adding new test for _sftp_channel exception handling.
* adding curve25519-sha256@libssh.org to kex list.
* fix for UnboundLocalError on a certain exception in _sftp_channel.
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@
# built documents.
#
# The short X.Y version.
version = '1.2.2'
version = '1.2.3'
# The full version, including alpha/beta/rc tags.
release = '1.2.2'
release = '1.2.3'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ keywords = [
name = 'sftpretty'
readme = 'README.rst'
requires-python = '>=3.6'
version = '1.2.2'
version = '1.2.3'

[project.scripts]
sftpretty = 'sftpretty:Connection'
Expand Down
8 changes: 4 additions & 4 deletions sftpretty/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,16 @@ def hash(filename, algorithm=sha3_512(), blocksize=65536):
with open(filename, 'rb') as filestream:
for chunk in iter(lambda: filestream.read(blocksize), b''):
buffer.update(chunk)
except FileNotFoundError:
except OSError:
buffer.update(bytes(filename.encode('utf-8')))
elif isinstance(filename, BytesIO):
for chunk in iter(lambda: filestream.read1(blocksize), b''):
for chunk in iter(lambda: filename.read1(blocksize), b''):
buffer.update(chunk)
elif isinstance(filename, IOBase):
for chunk in iter(lambda: filestream.read(blocksize), b''):
for chunk in iter(lambda: filename.read(blocksize), b''):
buffer.update(chunk)

return algorithm.hexdigest()
return buffer.hexdigest()


def localtree(container, localdir, remotedir, recurse=True):
Expand Down
20 changes: 1 addition & 19 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import pytest

from contextlib import contextmanager
from os import close, environ
from os import environ
from pathlib import Path
from sftpretty import CnOpts
from stat import S_ISDIR
from tempfile import mkstemp


PASS = 'tEst@!357'
Expand Down Expand Up @@ -81,20 +80,3 @@ def rmdir(dir):
else:
item.unlink()
dir.rmdir()


@contextmanager
def tempfile_containing(contents=STARS8192, suffix=''):
'''create a temporary file, with optional suffix and return the filename,
cleanup when finished'''

fd, temp_path = mkstemp(suffix=suffix)
close(fd)

with open(temp_path, 'wb') as fh:
fh.write(contents.encode('utf-8'))

try:
yield Path(temp_path).as_posix()
finally:
Path(temp_path).unlink()
23 changes: 20 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import pytest

from common import LOCAL, remote_rmdir, STARS8192, USER_HOME
from os import close
from paramiko.hostkeys import HostKeys
from pathlib import Path
from uuid import uuid4

from common import LOCAL, remote_rmdir, USER_HOME
from sftpretty import CnOpts, Connection
from tempfile import mkstemp
from uuid import uuid4


@pytest.fixture(scope='session')
Expand Down Expand Up @@ -50,3 +51,19 @@ def remote_tmpdir(lsftp):
yield lsftp.normalize(remotedir.as_posix())
finally:
remote_rmdir(lsftp, remotedir.as_posix())


@pytest.fixture
def tempfile_containing(tmp_path):
'''create a temporary file, with optional suffix, holding content and
return the filename'''
def contentfile(contents=STARS8192, suffix=''):
fd, temp_path = mkstemp(dir=tmp_path, suffix=suffix)
close(fd)

with open(temp_path, 'wb') as tempfile:
tempfile.write(contents.encode('utf-8'))

return Path(temp_path).as_posix()

return contentfile
6 changes: 3 additions & 3 deletions tests/test_cd.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


def test_cd_none(sftpserver):
'''test sftpretty.cd with None'''
'''test cd with None'''
pubpath = PurePosixPath(drivepath(VFS_HOME)).joinpath('pub')
with sftpserver.serve_content(VFS):
with Connection(**conn(sftpserver)) as sftp:
Expand All @@ -21,7 +21,7 @@ def test_cd_none(sftpserver):


def test_cd_path(sftpserver):
'''test sftpretty.cd with a path'''
'''test cd with a path'''
pubpath = PurePosixPath(drivepath(VFS_HOME)).joinpath('pub')
with sftpserver.serve_content(VFS):
with Connection(**conn(sftpserver)) as sftp:
Expand All @@ -46,7 +46,7 @@ def test_cd_nested(sftpserver):


def test_cd_bad_path(sftpserver):
'''test sftpretty.cd with a bad path'''
'''test cd with a bad path'''
with sftpserver.serve_content(VFS):
with Connection(**conn(sftpserver)) as sftp:
home = sftp.pwd
Expand Down
24 changes: 13 additions & 11 deletions tests/test_chmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from common import conn, SKIP_IF_ROOT, SKIP_IF_WIN, tempfile_containing, VFS
from common import conn, SKIP_IF_ROOT, SKIP_IF_WIN, VFS
from pathlib import Path
from sftpretty import Connection
from sftpretty.helpers import st_mode_to_int
Expand All @@ -18,13 +18,14 @@ def test_chmod_not_exist(sftpserver):

@SKIP_IF_ROOT
@SKIP_IF_WIN # Win32-OpenSSH doesn't translate mode bits into ACLs
def test_chmod_ro(lsftp, remote_tmpdir):
def test_chmod_ro(lsftp, remote_tmpdir, tempfile_containing):
'''test chmod against read-only path'''
content = 'You answer me, although I never ask you questions, what am I?'
parent = Path(remote_tmpdir).joinpath('readonly')
rfile = parent.joinpath('readme.txt')
lsftp.mkdir_p(parent.as_posix())
with tempfile_containing() as fname:
lsftp.put(fname, rfile.as_posix())
localfile = tempfile_containing(contents=content)
lsftp.put(localfile, rfile.as_posix())
lsftp.chmod(parent.as_posix(), 400) # no search bit, 500 fails
try:
with pytest.raises(PermissionError):
Expand All @@ -34,15 +35,16 @@ def test_chmod_ro(lsftp, remote_tmpdir):


@SKIP_IF_WIN # Win32-OpenSSH doesn't translate mode bits into ACLs
def test_chmod_simple(lsftp):
def test_chmod_simple(lsftp, tempfile_containing):
'''test basic chmod with octal mode represented by an int'''
content = 'Which three letters can frighten a thief away?'
new_mode = 711
with tempfile_containing(contents='') as fname:
base_fname = Path(fname).name
org_attrs = lsftp.put(fname)
lsftp.chmod(base_fname, new_mode)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)
localfile = tempfile_containing(contents=content)
base_fname = Path(localfile).name
org_attrs = lsftp.put(localfile)
lsftp.chmod(base_fname, new_mode)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)

assert st_mode_to_int(new_attrs.st_mode) == new_mode
assert new_attrs.st_mode != org_attrs.st_mode
73 changes: 40 additions & 33 deletions tests/test_chown.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,52 @@

import pytest

from common import SKIP_IF_ROOT, SKIP_IF_WIN, tempfile_containing
from common import SKIP_IF_ROOT, SKIP_IF_WIN
from pathlib import Path


@SKIP_IF_WIN # uid comes through as 0, lacks support
def test_chown_uid(lsftp):
def test_chown_uid(lsftp, tempfile_containing):
'''test changing just the uid'''
with tempfile_containing() as fname:
base_fname = Path(fname).name
org_attrs = lsftp.put(fname)
uid = org_attrs.st_uid
lsftp.chown(base_fname, uid=uid)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)
content = 'What is the end of everything?'
localfile = tempfile_containing(contents=content)
base_fname = Path(localfile).name
org_attrs = lsftp.put(localfile)
uid = org_attrs.st_uid
lsftp.chown(base_fname, uid=uid)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)

assert new_attrs.st_gid == org_attrs.st_gid
assert new_attrs.st_uid == uid


@SKIP_IF_WIN # gid comes through as 0, lacks support
def test_chown_gid(lsftp):
def test_chown_gid(lsftp, tempfile_containing):
'''test changing just the gid'''
with tempfile_containing() as fname:
base_fname = Path(fname).name
org_attrs = lsftp.put(fname)
gid = org_attrs.st_gid
lsftp.chown(base_fname, gid=gid)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)
content = 'I am wet when drying. What am I?'
localfile = tempfile_containing(contents=content)
base_fname = Path(localfile).name
org_attrs = lsftp.put(localfile)
gid = org_attrs.st_gid
lsftp.chown(base_fname, gid=gid)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)

assert new_attrs.st_gid == gid
assert new_attrs.st_uid == org_attrs.st_uid


def test_chown_none(lsftp):
def test_chown_none(lsftp, tempfile_containing):
'''call chown with no gid or uid specified'''
with tempfile_containing() as fname:
base_fname = Path(fname).name
org_attrs = lsftp.put(fname)
lsftp.chown(base_fname)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)
content = 'What color is the wind?'
localfile = tempfile_containing(contents=content)
base_fname = Path(localfile).name
org_attrs = lsftp.put(localfile)
lsftp.chown(base_fname)
new_attrs = lsftp.stat(base_fname)
lsftp.remove(base_fname)

assert new_attrs.st_gid == org_attrs.st_gid
assert new_attrs.st_uid == org_attrs.st_uid

Expand All @@ -54,13 +60,14 @@ def test_chown_not_exist(lsftp):

@SKIP_IF_ROOT
@SKIP_IF_WIN # ownership ids are synthetic, cannot be set
def test_chown_ro(lsftp):
def test_chown_ro(lsftp, tempfile_containing):
'''call chown against path on read-only server'''
with tempfile_containing() as fname:
base_fname = Path(fname).name
lsftp.put(fname)
try:
with pytest.raises(PermissionError):
lsftp.chown(base_fname, gid=0, uid=0)
finally:
lsftp.remove(base_fname)
content = 'What only works the first time you use it?'
localfile = tempfile_containing(contents=content)
base_fname = Path(localfile).name
lsftp.put(localfile)
try:
with pytest.raises(PermissionError):
lsftp.chown(base_fname, gid=0, uid=0)
finally:
lsftp.remove(base_fname)
Loading