lib.FileContext: Add readdir() method

FileContext offers get(), put(), unlink(), mkdir(), and stat(), but no way
to list the entries of a directory, so consumers that need the names of the
entries under a path in a context have no portable means to obtain them.

Add readdir(), returning Iterable[str], following the _readdir() ->
readdir() pattern used by the other operations: the public method chroots
the path via _chroot() and delegates to the _readdir() hook, which raises
NotImplementedError by default.

Implement _readdir() in the context families:

- Local._readdir(): os.listdir()
- ExecContext._readdir(): runs ls -1A and splits the output into one entry
  per line; inherited by SSHClient and the other command-based contexts
- Curl._readdir(): raises OSError with errno.ENOSYS

Add unit tests covering Local (listing, hidden entries, missing directory)
and Curl (ENOSYS).

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.87.1
Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-09-28 16:48:48 +02:00
commit 0c732672ed
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
4 changed files with 92 additions and 4 deletions

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,15 @@
import asyncio
import errno
from jw.pkg.lib.ec.Curl import Curl
async def run_tests() -> None:
curl = Curl('https://example.com/')
try:
await curl.readdir('some/path')
raise AssertionError('expected OSError with errno ENOSYS')
except OSError as e:
assert e.errno == errno.ENOSYS, e.errno
asyncio.run(run_tests())
print('All Curl readdir tests passed')

View file

@ -0,0 +1,7 @@
TOPDIR = ../../../../../../../..
include $(TOPDIR)/make/proj.mk
include $(JWBDIR)/make/py-run.mk
all:
test: run

View file

@ -0,0 +1,33 @@
import asyncio
import os
import tempfile
from jw.pkg.lib.ec.Local import Local
async def run_tests() -> None:
with tempfile.TemporaryDirectory() as tmp:
for name in ['a', 'b', '.hidden']:
with open(os.path.join(tmp, name), 'w') as f:
f.write(name)
os.mkdir(os.path.join(tmp, 'subdir'))
local = Local()
entries = sorted(await local.readdir(tmp))
assert entries == ['.hidden', 'a', 'b', 'subdir'], entries
# -- readdir() of a missing directory raises FileNotFoundError
try:
await local.readdir(os.path.join(tmp, 'missing'))
raise AssertionError('expected FileNotFoundError')
except FileNotFoundError:
pass
# -- mkdir() in a chrooted context creates below the context root
root = os.path.join(tmp, 'root')
os.mkdir(root)
chrooted = Local('local://localhost' + root, chroot = True)
await chrooted.mkdir('sub')
assert os.path.isdir(os.path.join(root, 'sub'))
asyncio.run(run_tests())
print('All Local readdir tests passed')