Improve FileMetadata & tests.
This commit is contained in:
236
src/bsv/vfs.py
236
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",
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user