FileContext: Add readdir() and recursive directory copy #131
8 changed files with 92 additions and 4 deletions
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>
commit
0c732672ed
|
|
@ -5,7 +5,7 @@ import errno
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from decimal import ROUND_FLOOR, Decimal
|
from decimal import ROUND_FLOOR, Decimal
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple, override
|
from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, override
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
@ -688,3 +688,10 @@ class ExecContext(Base):
|
||||||
['chmod', self.__mode_str(mode), path],
|
['chmod', self.__mode_str(mode), path],
|
||||||
cmd_input = InputMode.NonInteractive
|
cmd_input = InputMode.NonInteractive
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _readdir(self, path: str) -> Iterable[str]:
|
||||||
|
result = await self.run(
|
||||||
|
['ls', '-1A', path], cmd_input = InputMode.NonInteractive
|
||||||
|
)
|
||||||
|
return result.stdout_str.splitlines()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import abc
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING, Any, override
|
from typing import TYPE_CHECKING, Any, Iterable, override
|
||||||
|
|
||||||
from .log import DEBUG, ERR, log
|
from .log import DEBUG, ERR, log
|
||||||
from .ProcFilter import ProcPipeline
|
from .ProcFilter import ProcPipeline
|
||||||
|
|
@ -301,6 +301,14 @@ class FileContext(abc.ABC):
|
||||||
async def is_dir(self, path: str, follow_symlinks: bool = True) -> bool:
|
async def is_dir(self, path: str, follow_symlinks: bool = True) -> bool:
|
||||||
return await self._is_dir(self._chroot(path), follow_symlinks = follow_symlinks)
|
return await self._is_dir(self._chroot(path), follow_symlinks = follow_symlinks)
|
||||||
|
|
||||||
|
async def _readdir(self, path: str) -> Iterable[str]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f'{self.log_name}: readdir("{path}") is not implemented'
|
||||||
|
)
|
||||||
|
|
||||||
|
async def readdir(self, path: str) -> Iterable[str]:
|
||||||
|
return await self._readdir(self._chroot(path))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, uri: str | Uri, *args: Any, **kwargs: Any) -> 'FileContext':
|
def create(cls, uri: str | Uri, *args: Any, **kwargs: Any) -> 'FileContext':
|
||||||
uri = Uri.pimp(uri)
|
uri = Uri.pimp(uri)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, override
|
import errno
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Iterable, override
|
||||||
|
|
||||||
from ..FileContext import FileContext as Base
|
from ..FileContext import FileContext as Base
|
||||||
|
|
||||||
|
|
@ -48,3 +50,8 @@ class Curl(Base):
|
||||||
path = '/' + path
|
path = '/' + path
|
||||||
cmd.append(self.uri.to_string + self._chroot(path))
|
cmd.append(self.uri.to_string + self._chroot(path))
|
||||||
return await self.__ec.run(cmd, throw = throw, verbose = verbose)
|
return await self.__ec.run(cmd, throw = throw, verbose = verbose)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _readdir(self, path: str) -> Iterable[str]:
|
||||||
|
msg = f'{self.log_name}: readdir("{path}") is not implemented'
|
||||||
|
raise OSError(errno.ENOSYS, msg)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import pwd
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from functools import cache
|
from functools import cache
|
||||||
from typing import TYPE_CHECKING, override
|
from typing import TYPE_CHECKING, Iterable, override
|
||||||
|
|
||||||
from ..base import Result, StatResult
|
from ..base import Result, StatResult
|
||||||
from ..ExecContext import ExecContext as Base
|
from ..ExecContext import ExecContext as Base
|
||||||
|
|
@ -206,3 +206,7 @@ class Local(Base):
|
||||||
if (not follow_symlinks) and os.path.islink(path):
|
if (not follow_symlinks) and os.path.islink(path):
|
||||||
return False
|
return False
|
||||||
return os.path.isdir(path)
|
return os.path.isdir(path)
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def _readdir(self, path: str) -> Iterable[str]:
|
||||||
|
return os.listdir(path)
|
||||||
|
|
|
||||||
7
test/unit/python/jw/pkg/lib/ec/Curl/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/ec/Curl/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
TOPDIR = ../../../../../../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-run.mk
|
||||||
|
|
||||||
|
all:
|
||||||
|
test: run
|
||||||
15
test/unit/python/jw/pkg/lib/ec/Curl/test.py
Normal file
15
test/unit/python/jw/pkg/lib/ec/Curl/test.py
Normal 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')
|
||||||
7
test/unit/python/jw/pkg/lib/ec/Local/Makefile
Normal file
7
test/unit/python/jw/pkg/lib/ec/Local/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
TOPDIR = ../../../../../../../..
|
||||||
|
|
||||||
|
include $(TOPDIR)/make/proj.mk
|
||||||
|
include $(JWBDIR)/make/py-run.mk
|
||||||
|
|
||||||
|
all:
|
||||||
|
test: run
|
||||||
33
test/unit/python/jw/pkg/lib/ec/Local/test.py
Normal file
33
test/unit/python/jw/pkg/lib/ec/Local/test.py
Normal 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')
|
||||||
Loading…
Reference in a new issue