Skip to content

fix: preserve absolute root in _strip_protocol so walk("/") works - #69

Merged
shcheklein merged 5 commits into
fsspec:mainfrom
mixilchenko:fix/walk-root-strip-protocol
Aug 5, 2026
Merged

fix: preserve absolute root in _strip_protocol so walk("/") works#69
shcheklein merged 5 commits into
fsspec:mainfrom
mixilchenko:fix/walk-root-strip-protocol

Conversation

@mixilchenko

@mixilchenko mixilchenko commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

SSHFileSystem._strip_protocol("/") collapsed the explicit root down to "" (the inherited empty root_marker). Most SFTP servers, OpenSSH included, reject an empty path with ENOENT for everything except REALPATH, so walk("/") silently yielded nothing and exists("/") / isdir("/") returned False. Servers implementing the SFTP draft's "empty path = default directory" rule resolved it to the home directory instead.

Fix

_strip_protocol now restores / only when the caller explicitly supplied the absolute root:

  • explicit absolute root (/, ssh://host/) → /
  • empty and relative inputs ("", ssh://host, foo/bar) → unchanged, still resolving against the home directory

Setting root_marker = "/" instead would have remapped "" and bare ssh://host from home to root and shifted _parent() for relative paths, so the narrower fix was chosen.

Behavior change

"/" now refers to the actual root in every operation that goes through fsspec's path stripping: exists, isdir, info, walk, find, glob, du, get, put, rm on "/" act on the root tree instead of failing or resolving to $HOME. Methods that never stripped paths (ls, mkdir, mv, ...) are unaffected.

Tests

walk coverage over a nested tree, a stat-based root-resolution test (deterministic on any host and any supported fsspec release), and _strip_protocol unit tests pinning root/empty/relative/URL behavior.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes SSHFileSystem.walk("/") returning incorrect paths by ensuring protocol-stripping preserves the root path. It aligns SSHFileSystem’s root handling with expected absolute-path semantics so directory traversal from / yields paths that exists() recognizes.

Changes:

  • Set SSHFileSystem.root_marker = "/" to prevent _strip_protocol("/") collapsing / into an empty string.
  • Add walk() test coverage for a nested directory tree under a test-specific remote path.
  • Add a regression test ensuring walk("/") yields root == "/" and absolute info["name"] entries that exist.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
tests/test_sshfs.py Adds coverage for walk() on both nested paths and / regression case to ensure returned entries are absolute and valid.
sshfs/spec.py Sets root_marker to preserve / during protocol stripping, fixing root-walk behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mixilchenko

Copy link
Copy Markdown
Contributor Author

@shcheklein I'd like it to be merged and released. What is left?

Comment thread sshfs/spec.py Outdated
Comment thread tests/test_sshfs.py Outdated

@shcheklein shcheklein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR has (unintentionally) higher blast radius. Probably we need some redesign, at least don't set root_marker. Preserve / only when the caller explicitly supplied an absolute root in strip_protocol or something like this. + we need more tests to ensure that the current behavior is not affected.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread sshfs/spec.py
@mixilchenko mixilchenko changed the title fix: preserve root path in _strip_protocol so walk('/') works fix: preserve absolute root in _strip_protocol so walk("/") works Jul 26, 2026
@mixilchenko

Copy link
Copy Markdown
Contributor Author

@shcheklein updated the pr name and description

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@shcheklein

Copy link
Copy Markdown
Collaborator

There is one small test tweak I'd like to include before merging: 20e9924 (branch pr-69-walk-root-test). It makes test_walk_root deterministic: the mock server exposes the host's real filesystem, so assertions about the contents of / depend on machine state — a single dangling symlink at the host root fails the whole mockssh listing, and walk(on_error="omit") then silently skips the name/exists assertions. Asserting only the yielded root distinguishes the fix (/) from the regression ("") on every machine.

Could you either pull it into your branch:

git pull https://github.com/fsspec/sshfs pr-69-walk-root-test

or tick "Allow edits by maintainers" on this PR so I can push it directly? Thanks!

@mixilchenko

Copy link
Copy Markdown
Contributor Author

@shcheklein did both, pull and allowed maintainers edits. Thanks!

@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.60%. Comparing base (15aac74) to head (94161fe).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
- Coverage   94.30%   93.60%   -0.70%     
==========================================
  Files          13       12       -1     
  Lines         738      798      +60     
==========================================
+ Hits          696      747      +51     
- Misses         42       51       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

shcheklein and others added 2 commits August 4, 2026 13:20
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>
@shcheklein
shcheklein merged commit df852af into fsspec:main Aug 5, 2026
13 checks passed
@mixilchenko

Copy link
Copy Markdown
Contributor Author

Thanks for the merge. Could you please make the release so I could bump the dependencies in my project?

@shcheklein

Copy link
Copy Markdown
Collaborator

@mixilchenko I've published it, it should be available soon. There some other changes. Please test it and let me know if you hit any issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants