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)
|
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:
|
class VirtualFileSystem:
|
||||||
"""Represent a file system, with common file system operations."""
|
"""Represent a file system, with common file system operations."""
|
||||||
|
|
||||||
@@ -127,7 +240,7 @@ class VirtualFileSystem:
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
msg = f"failed to read '{path}' metadata"
|
msg = f"failed to read '{path}' metadata"
|
||||||
raise FsError(msg) from err
|
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]:
|
def iter_dir(self, path: AnyBsvPath) -> Iterator[FileMetadata]:
|
||||||
"""Return the metadata of all items in the directory `path`."""
|
"""Return the metadata of all items in the directory `path`."""
|
||||||
@@ -136,7 +249,7 @@ class VirtualFileSystem:
|
|||||||
try:
|
try:
|
||||||
for entry in os.scandir(real_path):
|
for entry in os.scandir(real_path):
|
||||||
yield FileMetadata.from_stat(
|
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:
|
except OSError as err:
|
||||||
msg = f"failed to read directory {path}"
|
msg = f"failed to read directory {path}"
|
||||||
@@ -233,122 +346,3 @@ class VirtualFileSystem:
|
|||||||
|
|
||||||
def _real_path(self, path: PurePosixPath) -> Path:
|
def _real_path(self, path: PurePosixPath) -> Path:
|
||||||
return self.path / path.relative_to("/")
|
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
|
import pytest
|
||||||
|
|
||||||
from bsv.vfs import FsError, Permissions, VirtualFileSystem
|
from bsv.vfs import FileMetadata, FsError, Permissions, VirtualFileSystem
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -53,6 +53,158 @@ def test_permissions():
|
|||||||
assert str(perm1) == "rwxr-x-w-"
|
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
|
# mkdir
|
||||||
|
|
||||||
@@ -175,13 +327,9 @@ def test_metadata(fs: VirtualFileSystem):
|
|||||||
assert md.path == PurePosixPath("/test_file")
|
assert md.path == PurePosixPath("/test_file")
|
||||||
assert md.permissions == file_permissions
|
assert md.permissions == file_permissions
|
||||||
assert md.type == "file"
|
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.modification_time == file_time
|
||||||
assert md.byte_size == len(file_content)
|
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.metadata("/test_file") == md
|
||||||
assert fs.is_file("/test_file")
|
assert fs.is_file("/test_file")
|
||||||
assert not fs.is_dir("/test_file")
|
assert not fs.is_dir("/test_file")
|
||||||
@@ -194,11 +342,7 @@ def test_metadata(fs: VirtualFileSystem):
|
|||||||
fs.mkdir("/.test_dir")
|
fs.mkdir("/.test_dir")
|
||||||
md = fs.metadata("/.test_dir")
|
md = fs.metadata("/.test_dir")
|
||||||
assert md.type == "dir"
|
assert md.type == "dir"
|
||||||
assert not md.is_file
|
assert fs.metadata("/.test_dir").is_hidden
|
||||||
assert md.is_dir
|
|
||||||
assert not md.is_symlink
|
|
||||||
assert not md.is_other
|
|
||||||
assert fs.metadata("/.test_dir").is_hidden_files
|
|
||||||
assert not fs.is_file("/.test_dir")
|
assert not fs.is_file("/.test_dir")
|
||||||
assert fs.is_dir("/.test_dir")
|
assert fs.is_dir("/.test_dir")
|
||||||
assert not fs.is_symlink("/.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")
|
fs.make_link("/test_link", "/link_target")
|
||||||
md = fs.metadata("/test_link")
|
md = fs.metadata("/test_link")
|
||||||
assert md.type == "symlink"
|
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_file("/test_link")
|
||||||
assert not fs.is_dir("/test_link")
|
assert not fs.is_dir("/test_link")
|
||||||
assert fs.is_symlink("/test_link")
|
assert fs.is_symlink("/test_link")
|
||||||
|
|||||||
Reference in New Issue
Block a user