From 2b961baa5b733f810994337ef8450663ac6579b2 Mon Sep 17 00:00:00 2001 From: Draklaw Date: Sat, 12 Jul 2025 20:40:35 +0200 Subject: [PATCH] Improve FileMetadata & tests. --- src/bsv/vfs.py | 236 ++++++++++++++++++------------------- tests/test_bsv/test_vfs.py | 170 +++++++++++++++++++++++--- 2 files changed, 270 insertions(+), 136 deletions(-) diff --git a/src/bsv/vfs.py b/src/bsv/vfs.py index 883f007..361b51c 100644 --- a/src/bsv/vfs.py +++ b/src/bsv/vfs.py @@ -79,6 +79,119 @@ DEFAULT_DIR_PERMS = Permissions(0o770) DEFAULT_FILE_PERMS = Permissions(0o640) +FileType = Literal["dir", "file", "symlink", "other"] + +_IFMT_MAP: dict[int, FileType] = { + S_IFDIR: "dir", + S_IFREG: "file", + S_IFLNK: "symlink", +} + + +@total_ordering +class FileMetadata: + """Metadata associated with vfs files: file type, permissions, etc.""" + + path: PurePosixPath + type: FileType + permissions: Permissions + modification_time: datetime + byte_size: int + _stat: stat_result + + def __init__( + self, + path: PurePosixPath, + *, + type: FileType, + permissions: Permissions, + modification_time: datetime, + byte_size: int, + ): + """Create a `FileMetadata`.""" + self.path = path + self.type = type + self.permissions = permissions + self.modification_time = modification_time + self.byte_size = byte_size + + @classmethod + def from_stat( + cls, + path: PurePosixPath, + stat: stat_result, + ) -> Self: + """Create a `FileMetadata` from a `stat_result`.""" + return cls( + path, + type=_IFMT_MAP.get(S_IFMT(stat.st_mode), "other"), + permissions=Permissions(S_IMODE(stat.st_mode)), + modification_time=datetime.fromtimestamp(stat.st_mtime, UTC), + byte_size=stat.st_size, + ) + + @property + def unix_mode(self) -> str: + """Return unix-like mode in the form '-rwxrwxrwx'.""" + return UNIX_MODE_FILE_TYPE[self.type] + str(self.permissions) + + @property + def is_hidden(self) -> bool: + """Return true if the file starts with a '.'.""" + return self.path.name.startswith(".") + + @property + def is_file(self) -> bool: + """Test if this is a file.""" + return self.type == "file" + + @property + def is_dir(self) -> bool: + """Test if this is a directory.""" + return self.type == "dir" + + @property + def is_symlink(self) -> bool: + """Test if this is a symbolic link.""" + return self.type == "symlink" + + @property + def is_other(self) -> bool: + """Test if this is a symbolic link.""" + return self.type == "other" + + def _as_tuple( + self, + ) -> tuple[PurePosixPath, FileType, Permissions, datetime, int]: + return ( + self.path, + self.type, + self.permissions, + self.modification_time, + self.byte_size, + ) + + def __eq__(self, rhs: Any) -> bool: + """Test if two `Metadata` are the same.""" + return ( + self._as_tuple() == rhs._as_tuple() + if isinstance(rhs, FileMetadata) + else NotImplemented + ) + + def __lt__(self, rhs: Any) -> bool: + """Compare `rhs.path` with `self.path`.""" + return self.path < rhs.path if isinstance(rhs, FileMetadata) else NotImplemented + + +UNIX_MODE_FILE_TYPE = { + "dir": "d", + "file": "-", + "other": "o", + "link": "l", +} + + class VirtualFileSystem: """Represent a file system, with common file system operations.""" @@ -127,7 +240,7 @@ class VirtualFileSystem: except OSError as err: msg = f"failed to read '{path}' metadata" raise FsError(msg) from err - return FileMetadata.from_stat(self, path, stat) + return FileMetadata.from_stat(path, stat) def iter_dir(self, path: AnyBsvPath) -> Iterator[FileMetadata]: """Return the metadata of all items in the directory `path`.""" @@ -136,7 +249,7 @@ class VirtualFileSystem: try: for entry in os.scandir(real_path): yield FileMetadata.from_stat( - self, path / entry.name, entry.stat(follow_symlinks=False) + path / entry.name, entry.stat(follow_symlinks=False) ) except OSError as err: msg = f"failed to read directory {path}" @@ -233,122 +346,3 @@ class VirtualFileSystem: def _real_path(self, path: PurePosixPath) -> Path: return self.path / path.relative_to("/") - - -FileType = Literal["dir", "file", "symlink", "other"] - -_IFMT_MAP: dict[int, FileType] = { - S_IFDIR: "dir", - S_IFREG: "file", - S_IFLNK: "symlink", -} - - -@total_ordering -class FileMetadata: - """Metadata associated with vfs files: file type, permissions, etc.""" - - vfs: VirtualFileSystem - path: PurePosixPath - type: FileType - permissions: Permissions - modification_time: datetime - byte_size: int - _stat: stat_result - - def __init__( - self, - vfs: VirtualFileSystem, - path: PurePosixPath, - *, - type: FileType, - permissions: Permissions, - modification_time: datetime, - byte_size: int, - ): - """Create a `FileMetadata`.""" - self.vfs = vfs - self.path = path - self.type = type - self.permissions = permissions - self.modification_time = modification_time - self.byte_size = byte_size - - @classmethod - def from_stat( - cls, - vfs: VirtualFileSystem, - path: PurePosixPath, - stat: stat_result, - ) -> Self: - """Create a `FileMetadata` from a `stat_result`.""" - return cls( - vfs, - path, - type=_IFMT_MAP.get(S_IFMT(stat.st_mode), "other"), - permissions=Permissions(S_IMODE(stat.st_mode)), - modification_time=datetime.fromtimestamp(stat.st_mtime, UTC), - byte_size=stat.st_size, - ) - - @property - def unix_mode(self) -> str: - """Return unix-like mode in the form '-rwxrwxrwx'.""" - return UNIX_MODE_FILE_TYPE[self.type] + str(self.permissions) - - @property - def is_hidden_files(self) -> bool: - """Return true if the file starts with a '.'.""" - return self.path.name.startswith(".") - - @property - def is_file(self) -> bool: - """Test if this is a file.""" - return self.type == "file" - - @property - def is_dir(self) -> bool: - """Test if this is a directory.""" - return self.type == "dir" - - @property - def is_symlink(self) -> bool: - """Test if this is a symbolic link.""" - return self.type == "symlink" - - @property - def is_other(self) -> bool: - """Test if this is a symbolic link.""" - return self.type == "other" - - def _as_tuple( - self, - ) -> tuple[VirtualFileSystem, PurePosixPath, FileType, Permissions, datetime, int]: - return ( - self.vfs, - self.path, - self.type, - self.permissions, - self.modification_time, - self.byte_size, - ) - - def __eq__(self, rhs: Any) -> bool: - """Test if two `Metadata` are the same.""" - return ( - self._as_tuple() == rhs._as_tuple() - if isinstance(rhs, FileMetadata) - else NotImplemented - ) - - def __lt__(self, rhs: Any) -> bool: - """Compare `rhs.path` with `self.path`.""" - return self.path < rhs.path if isinstance(rhs, FileMetadata) else NotImplemented - - -UNIX_MODE_FILE_TYPE = { - "dir": "d", - "file": "-", - "other": "o", - "link": "l", -} diff --git a/tests/test_bsv/test_vfs.py b/tests/test_bsv/test_vfs.py index 8667471..45d1e16 100644 --- a/tests/test_bsv/test_vfs.py +++ b/tests/test_bsv/test_vfs.py @@ -23,7 +23,7 @@ from pathlib import Path, PurePosixPath import pytest -from bsv.vfs import FsError, Permissions, VirtualFileSystem +from bsv.vfs import FileMetadata, FsError, Permissions, VirtualFileSystem @pytest.fixture @@ -53,6 +53,158 @@ def test_permissions(): assert str(perm1) == "rwxr-x-w-" +######################################################################################## +# FileMetadata + + +def test_file_metadata(): + path = PurePosixPath("/some_dir/some_file") + permissions = Permissions(0o1234) + mod_time = datetime(2025, 7, 12, 12, 34, 56, tzinfo=UTC) + + file_md = FileMetadata( + path, + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + + assert file_md.path == path + assert file_md.type == "file" + assert file_md.permissions == permissions + assert file_md.modification_time == mod_time + assert file_md.byte_size == 123 + assert file_md.unix_mode == "--w--wxr-T" + assert not file_md.is_hidden + assert file_md.is_file + assert not file_md.is_dir + assert not file_md.is_symlink + assert not file_md.is_other + + dir_md = FileMetadata( + path, + type="dir", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + assert dir_md.type == "dir" + assert not dir_md.is_file + assert dir_md.is_dir + assert not dir_md.is_symlink + assert not dir_md.is_other + + symlink_md = FileMetadata( + path, + type="symlink", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + assert symlink_md.type == "symlink" + assert not symlink_md.is_file + assert not symlink_md.is_dir + assert symlink_md.is_symlink + assert not symlink_md.is_other + + other_md = FileMetadata( + path, + type="other", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + assert other_md.type == "other" + assert not other_md.is_file + assert not other_md.is_dir + assert not other_md.is_symlink + assert other_md.is_other + + assert FileMetadata( + PurePosixPath("/some_dir/.some_file"), + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ).is_hidden + + +def test_file_metadata_eq(): + path = PurePosixPath("/some_dir/some_file") + permissions = Permissions(0o1234) + mod_time = datetime(2025, 7, 12, 12, 34, 56, tzinfo=UTC) + + md = FileMetadata( + path, + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + + assert ( + FileMetadata( + path, + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + == md + ) + assert ( + FileMetadata( + PurePosixPath("/some_dir/some_other_file"), + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + != md + ) + assert ( + FileMetadata( + path, + type="dir", + permissions=permissions, + modification_time=mod_time, + byte_size=123, + ) + != md + ) + assert ( + FileMetadata( + path, + type="file", + permissions=Permissions(0o0752), + modification_time=mod_time, + byte_size=123, + ) + != md + ) + assert ( + FileMetadata( + path, + type="file", + permissions=permissions, + modification_time=datetime(2025, 1, 2, 3, 4, 5), + byte_size=123, + ) + != md + ) + assert ( + FileMetadata( + path, + type="file", + permissions=permissions, + modification_time=mod_time, + byte_size=124, + ) + != md + ) + + ######################################################################################## # mkdir @@ -175,13 +327,9 @@ def test_metadata(fs: VirtualFileSystem): assert md.path == PurePosixPath("/test_file") assert md.permissions == file_permissions assert md.type == "file" - assert md.is_file - assert not md.is_dir - assert not md.is_symlink - assert not md.is_other assert md.modification_time == file_time assert md.byte_size == len(file_content) - assert not md.is_hidden_files + assert not md.is_hidden assert fs.metadata("/test_file") == md assert fs.is_file("/test_file") assert not fs.is_dir("/test_file") @@ -194,11 +342,7 @@ def test_metadata(fs: VirtualFileSystem): fs.mkdir("/.test_dir") md = fs.metadata("/.test_dir") assert md.type == "dir" - assert not md.is_file - assert md.is_dir - assert not md.is_symlink - assert not md.is_other - assert fs.metadata("/.test_dir").is_hidden_files + assert fs.metadata("/.test_dir").is_hidden assert not fs.is_file("/.test_dir") assert fs.is_dir("/.test_dir") assert not fs.is_symlink("/.test_dir") @@ -207,10 +351,6 @@ def test_metadata(fs: VirtualFileSystem): fs.make_link("/test_link", "/link_target") md = fs.metadata("/test_link") assert md.type == "symlink" - assert not md.is_file - assert not md.is_dir - assert md.is_symlink - assert not md.is_other assert not fs.is_file("/test_link") assert not fs.is_dir("/test_link") assert fs.is_symlink("/test_link")