From 885f64b827687d42ca2845d79735b99aa8d66c80 Mon Sep 17 00:00:00 2001 From: Mikhail Ilchenko Date: Thu, 9 Jul 2026 19:25:29 +0400 Subject: [PATCH 1/2] fix: replace deprecated datetime.utcfromtimestamp datetime.utcfromtimestamp is deprecated and slated for removal. Route both timestamps through a _naive_utc helper that uses datetime.fromtimestamp(ts, timezone.utc) and strips tzinfo, preserving the existing naive-UTC values returned in info dicts. --- sshfs/spec.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sshfs/spec.py b/sshfs/spec.py index 5c1fc73..bb34f99 100644 --- a/sshfs/spec.py +++ b/sshfs/spec.py @@ -4,7 +4,7 @@ import stat import weakref from contextlib import AsyncExitStack, suppress -from datetime import datetime +from datetime import datetime, timezone import asyncssh from asyncssh.sftp import SFTPOpUnsupported @@ -34,6 +34,11 @@ _DEFAULT_MAX_SESSIONS = 10 +def _naive_utc(timestamp): + # Naive UTC datetime, matching the historical utcfromtimestamp() output. + return datetime.fromtimestamp(timestamp, timezone.utc).replace(tzinfo=None) + + class SSHFileSystem(AsyncFileSystem): def __init__( self, @@ -151,8 +156,8 @@ def _decode_attributes(self, attributes): "type": kind, "gid": attributes.gid, "uid": attributes.uid, - "time": datetime.utcfromtimestamp(attributes.atime), - "mtime": datetime.utcfromtimestamp(attributes.mtime), + "time": _naive_utc(attributes.atime), + "mtime": _naive_utc(attributes.mtime), "permissions": attributes.permissions, } From cfa621b9265af84b7600540825115172d7159720 Mon Sep 17 00:00:00 2001 From: Mikhail Ilchenko Date: Fri, 24 Jul 2026 12:36:34 +0400 Subject: [PATCH 2/2] add tests, remove helper --- sshfs/spec.py | 17 ++++++++++------- tests/test_sshfs.py | 20 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/sshfs/spec.py b/sshfs/spec.py index bb34f99..77a1e11 100644 --- a/sshfs/spec.py +++ b/sshfs/spec.py @@ -34,11 +34,6 @@ _DEFAULT_MAX_SESSIONS = 10 -def _naive_utc(timestamp): - # Naive UTC datetime, matching the historical utcfromtimestamp() output. - return datetime.fromtimestamp(timestamp, timezone.utc).replace(tzinfo=None) - - class SSHFileSystem(AsyncFileSystem): def __init__( self, @@ -156,8 +151,16 @@ def _decode_attributes(self, attributes): "type": kind, "gid": attributes.gid, "uid": attributes.uid, - "time": _naive_utc(attributes.atime), - "mtime": _naive_utc(attributes.mtime), + "time": ( + datetime.fromtimestamp(attributes.atime, tz=timezone.utc) + if attributes.atime is not None + else None + ), + "mtime": ( + datetime.fromtimestamp(attributes.mtime, tz=timezone.utc) + if attributes.mtime is not None + else None + ), "permissions": attributes.permissions, } diff --git a/tests/test_sshfs.py b/tests/test_sshfs.py index e55639c..ebe351b 100644 --- a/tests/test_sshfs.py +++ b/tests/test_sshfs.py @@ -4,12 +4,12 @@ import tempfile import warnings from concurrent import futures -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path import fsspec import pytest -from asyncssh.sftp import SFTPFailure +from asyncssh.sftp import SFTPAttrs, SFTPFailure from importlib_metadata import entry_points from sshfs import SSHFileSystem @@ -116,6 +116,22 @@ def test_info(fs, remote_dir): assert details["name"] == remote_dir + "/dir/" +def test_info_timestamps_are_tz_aware_utc(fs, remote_dir): + fs.touch(remote_dir + "/a.txt") + details = fs.info(remote_dir + "/a.txt") + for key in ["time", "mtime"]: + assert details[key].tzinfo == timezone.utc + + +def test_decode_attributes_missing_timestamps(fs): + attrs = SFTPAttrs( + permissions=0o100644, size=0, uid=0, gid=0, atime=None, mtime=None + ) + details = fs._decode_attributes(attrs) + assert details["time"] is None + assert details["mtime"] is None + + def test_move(fs, remote_dir): fs.touch(remote_dir + "/a.txt") initial_info = fs.info(remote_dir + "/a.txt")