Skip to content

Commit df852af

Browse files
mixilchenkoshchekleinclaude
authored
fix: preserve absolute root in _strip_protocol so walk("/") works (#69)
* fix: preserve root path in _strip_protocol so walk('/') works SSHFileSystem inherited the default root_marker of "", so _strip_protocol("/") collapsed the root to an empty string. walk("/") then listed the home directory and produced relative paths that were not considered to exist. Set root_marker = "/" to keep the root, and add walk tests covering a nested tree and the root case. * Address review * tests: make test_walk_root deterministic The mock server exposes the host's real filesystem, so assertions about the contents of "/" depend on machine state: mock-ssh-server stats every entry of a listing, so a single dangling symlink at the host root fails the whole readdir, and fsspec's walk(on_error="omit") swallows the error, silently turning the name/exists assertions into a no-op. Assert only the yielded root, which distinguishes the fix (root "/") from the regression (root "") on every machine. Also correct the failure-mode note: most servers (e.g. OpenSSH) reject an empty path with ENOENT outside REALPATH; servers implementing the SFTP draft's empty-path rule resolve it to the default directory instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * tests: replace walk-based root test with stat-based assertions test_walk_root listed the host's real root through the mock server: mock-ssh-server stats every entry of a listing, so a single dangling symlink at "/" fails the whole readdir. On older fsspec releases (e.g. 2025.10.0, which Python 3.9 resolves) walk() yields nothing when the listing fails, so next() raised StopIteration -- the macos-latest/3.9 CI failure. Assert root resolution via stat instead, which depends neither on the contents of "/" nor on the fsspec version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Ivan Shcheklein <shcheklein@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 29d3f6c commit df852af

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

sshfs/spec.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ def __init__(
9797
def _strip_protocol(cls, path):
9898
# Remove components such as host and username from path.
9999
inferred_path = infer_storage_options(path)["path"]
100-
return super()._strip_protocol(inferred_path)
100+
stripped = super()._strip_protocol(inferred_path)
101+
# super() collapses a bare "/" to the (empty) root_marker. Restore it
102+
# only when the caller explicitly supplied the absolute root, so that
103+
# walk("/") lists from the root while relative paths (and "") still
104+
# resolve against the home directory.
105+
if not stripped and inferred_path.startswith("/"):
106+
return "/"
107+
return stripped
101108

102109
@staticmethod
103110
def _get_kwargs_from_urls(urlpath):

tests/test_sshfs.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,45 @@ def test_ls(fs, remote_dir):
256256
assert dirs == expected
257257

258258

259+
def test_walk(fs, remote_dir):
260+
fs.mkdir(remote_dir + "/a")
261+
fs.mkdir(remote_dir + "/a/b")
262+
fs.touch(remote_dir + "/a/f1")
263+
fs.touch(remote_dir + "/a/b/f2")
264+
265+
result = {
266+
root: (sorted(dirs), sorted(files))
267+
for root, dirs, files in fs.walk(remote_dir + "/a")
268+
}
269+
assert result == {
270+
remote_dir + "/a": (["b"], ["f1"]),
271+
remote_dir + "/a/b": ([], ["f2"]),
272+
}
273+
274+
275+
def test_root(fs):
276+
# An explicit "/" must resolve to the filesystem root, not the
277+
# user's home directory.
278+
assert fs.exists("/")
279+
assert fs.isdir("/")
280+
assert fs.info("/")["name"] == "/"
281+
282+
283+
def test_strip_protocol():
284+
strip = SSHFileSystem._strip_protocol
285+
# An explicit absolute root is preserved so walk("/") lists from the root.
286+
assert strip("/") == "/"
287+
assert strip("ssh://host/") == "/"
288+
# Empty and relative paths still resolve against the home directory
289+
# instead of being redirected to the root.
290+
assert strip("") == ""
291+
assert strip("ssh://host") == ""
292+
assert strip("foo/bar") == "foo/bar"
293+
# Regular absolute paths are unaffected.
294+
assert strip("/foo/bar") == "/foo/bar"
295+
assert strip("ssh://host/foo/bar") == "/foo/bar"
296+
297+
259298
def test_mkdir(fs, remote_dir):
260299
fs.mkdir(remote_dir + "dir/")
261300
assert fs.isdir(remote_dir + "dir/")

0 commit comments

Comments
 (0)