diff --git a/examples/hello.py b/examples/hello.py index 92695a8..b607150 100755 --- a/examples/hello.py +++ b/examples/hello.py @@ -26,13 +26,13 @@ import logging import os import stat -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from typing import cast import trio import pyfuse3 -from pyfuse3 import FileHandleT, FileInfo, InodeT +from pyfuse3 import EntryAttributes, FileHandleT, FileInfo, InodeT, ReaddirToken, RequestContext try: import faulthandler @@ -45,14 +45,14 @@ class TestFs(pyfuse3.Operations): - def __init__(self): + def __init__(self) -> None: super(TestFs, self).__init__() self.hello_name = b"message" self.hello_inode = cast(InodeT, pyfuse3.ROOT_INODE + 1) self.hello_data = b"hello world\n" - async def getattr(self, inode, ctx=None): - entry = pyfuse3.EntryAttributes() + async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes: + entry = EntryAttributes() if inode == pyfuse3.ROOT_INODE: entry.st_mode = stat.S_IFDIR | 0o755 entry.st_size = 0 @@ -72,18 +72,20 @@ async def getattr(self, inode, ctx=None): return entry - async def lookup(self, parent_inode, name, ctx=None): + async def lookup( + self, parent_inode: InodeT, name: bytes, ctx: RequestContext + ) -> EntryAttributes: if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name: raise pyfuse3.FUSEError(errno.ENOENT) - return await self.getattr(self.hello_inode) + return await self.getattr(self.hello_inode, ctx) - async def opendir(self, inode, ctx): + async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT: if inode != pyfuse3.ROOT_INODE: raise pyfuse3.FUSEError(errno.ENOENT) # For simplicity, we use the inode as file handle return FileHandleT(inode) - async def readdir(self, fh, start_id, token): + async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None: assert fh == pyfuse3.ROOT_INODE # only one entry @@ -91,7 +93,7 @@ async def readdir(self, fh, start_id, token): pyfuse3.readdir_reply(token, self.hello_name, await self.getattr(self.hello_inode), 1) return - async def open(self, inode, flags, ctx): + async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo: if inode != self.hello_inode: raise pyfuse3.FUSEError(errno.ENOENT) if flags & os.O_RDWR or flags & os.O_WRONLY: @@ -99,12 +101,12 @@ async def open(self, inode, flags, ctx): # For simplicity, we use the inode as file handle return FileInfo(fh=FileHandleT(inode)) - async def read(self, fh, off, size): + async def read(self, fh: FileHandleT, off: int, size: int) -> bytes: assert fh == self.hello_inode return self.hello_data[off : off + size] -def init_logging(debug=False): +def init_logging(debug: bool = False) -> None: formatter = logging.Formatter( '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S", @@ -121,7 +123,7 @@ def init_logging(debug=False): root_logger.addHandler(handler) -def parse_args(): +def parse_args() -> Namespace: '''Parse command line''' parser = ArgumentParser() @@ -136,7 +138,7 @@ def parse_args(): return parser.parse_args() -def main(): +def main() -> None: options = parse_args() init_logging(options.debug) diff --git a/examples/hello_asyncio.py b/examples/hello_asyncio.py index 215ef76..1021146 100755 --- a/examples/hello_asyncio.py +++ b/examples/hello_asyncio.py @@ -27,12 +27,12 @@ import logging import os import stat -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from typing import cast import pyfuse3 import pyfuse3.asyncio -from pyfuse3 import FileHandleT, FileInfo, InodeT +from pyfuse3 import EntryAttributes, FileHandleT, FileInfo, InodeT, ReaddirToken, RequestContext try: import faulthandler @@ -46,14 +46,14 @@ class TestFs(pyfuse3.Operations): - def __init__(self): + def __init__(self) -> None: super(TestFs, self).__init__() self.hello_name = b"message" self.hello_inode = cast(InodeT, pyfuse3.ROOT_INODE + 1) self.hello_data = b"hello world\n" - async def getattr(self, inode, ctx=None): - entry = pyfuse3.EntryAttributes() + async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes: + entry = EntryAttributes() if inode == pyfuse3.ROOT_INODE: entry.st_mode = stat.S_IFDIR | 0o755 entry.st_size = 0 @@ -73,18 +73,20 @@ async def getattr(self, inode, ctx=None): return entry - async def lookup(self, parent_inode, name, ctx=None): + async def lookup( + self, parent_inode: InodeT, name: bytes, ctx: RequestContext + ) -> EntryAttributes: if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name: raise pyfuse3.FUSEError(errno.ENOENT) - return await self.getattr(self.hello_inode) + return await self.getattr(self.hello_inode, ctx) - async def opendir(self, inode, ctx): + async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT: if inode != pyfuse3.ROOT_INODE: raise pyfuse3.FUSEError(errno.ENOENT) # For simplicity, we use the inode as file handle return FileHandleT(inode) - async def readdir(self, fh, start_id, token): + async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None: assert fh == pyfuse3.ROOT_INODE # only one entry @@ -92,7 +94,7 @@ async def readdir(self, fh, start_id, token): pyfuse3.readdir_reply(token, self.hello_name, await self.getattr(self.hello_inode), 1) return - async def setxattr(self, inode, name, value, ctx): + async def setxattr(self, inode: InodeT, name: bytes, value: bytes, ctx: RequestContext) -> None: if inode != pyfuse3.ROOT_INODE or name != b'command': raise pyfuse3.FUSEError(errno.ENOTSUP) @@ -101,7 +103,7 @@ async def setxattr(self, inode, name, value, ctx): else: raise pyfuse3.FUSEError(errno.EINVAL) - async def open(self, inode, flags, ctx): + async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo: if inode != self.hello_inode: raise pyfuse3.FUSEError(errno.ENOENT) if flags & os.O_RDWR or flags & os.O_WRONLY: @@ -109,12 +111,12 @@ async def open(self, inode, flags, ctx): # For simplicity, we use the inode as file handle return FileInfo(fh=FileHandleT(inode)) - async def read(self, fh, off, size): + async def read(self, fh: FileHandleT, off: int, size: int) -> bytes: assert fh == self.hello_inode return self.hello_data[off : off + size] -def init_logging(debug=False): +def init_logging(debug: bool = False) -> None: formatter = logging.Formatter( '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S", @@ -131,7 +133,7 @@ def init_logging(debug=False): root_logger.addHandler(handler) -def parse_args(): +def parse_args() -> Namespace: '''Parse command line''' parser = ArgumentParser() @@ -146,7 +148,7 @@ def parse_args(): return parser.parse_args() -def main(): +def main() -> None: options = parse_args() init_logging(options.debug) diff --git a/examples/passthroughfs.py b/examples/passthroughfs.py index 3a1718c..2f754bc 100755 --- a/examples/passthroughfs.py +++ b/examples/passthroughfs.py @@ -45,15 +45,26 @@ import os import stat as stat_m import sys -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from collections import defaultdict +from collections.abc import Sequence from os import fsdecode, fsencode from typing import cast import trio import pyfuse3 -from pyfuse3 import FileHandleT, FUSEError, InodeT +from pyfuse3 import ( + EntryAttributes, + FileHandleT, + FileInfo, + FUSEError, + InodeT, + ReaddirToken, + RequestContext, + SetattrFields, + StatvfsData, +) faulthandler.enable() @@ -63,15 +74,15 @@ class Operations(pyfuse3.Operations): enable_writeback_cache = True - def __init__(self, source): + def __init__(self, source: str) -> None: super().__init__() - self._inode_path_map = {pyfuse3.ROOT_INODE: source} - self._lookup_cnt = defaultdict(lambda: 0) - self._fd_inode_map = dict() - self._inode_fd_map = dict() - self._fd_open_count = dict() + self._inode_path_map: dict[InodeT, str | set[str]] = {pyfuse3.ROOT_INODE: source} + self._lookup_cnt: defaultdict[InodeT, int] = defaultdict(lambda: 0) + self._fd_inode_map: dict[int, InodeT] = dict() + self._inode_fd_map: dict[InodeT, int] = dict() + self._fd_open_count: dict[int, int] = dict() - def _inode_to_path(self, inode): + def _inode_to_path(self, inode: InodeT) -> str: try: val = self._inode_path_map[inode] except KeyError: @@ -82,7 +93,7 @@ def _inode_to_path(self, inode): val = next(iter(val)) return val - def _add_path(self, inode, path): + def _add_path(self, inode: InodeT, path: str) -> None: log.debug('_add_path for %d, %s', inode, path) self._lookup_cnt[inode] += 1 @@ -97,7 +108,7 @@ def _add_path(self, inode, path): elif val != path: self._inode_path_map[inode] = {path, val} - async def forget(self, inode_list): + async def forget(self, inode_list: Sequence[tuple[InodeT, int]]) -> None: for inode, nlookup in inode_list: if self._lookup_cnt[inode] > nlookup: self._lookup_cnt[inode] -= nlookup @@ -110,22 +121,24 @@ async def forget(self, inode_list): except KeyError: # may have been deleted pass - async def lookup(self, parent_inode, name, ctx=None): - name = fsdecode(name) - log.debug('lookup for %s in %d', name, parent_inode) - path = os.path.join(self._inode_to_path(parent_inode), name) + async def lookup( + self, parent_inode: InodeT, name: bytes, ctx: RequestContext + ) -> EntryAttributes: + name_str = fsdecode(name) + log.debug('lookup for %s in %d', name_str, parent_inode) + path = os.path.join(self._inode_to_path(parent_inode), name_str) attr = self._getattr(path=path) - if name != '.' and name != '..': - self._add_path(attr.st_ino, path) + if name_str != '.' and name_str != '..': + self._add_path(InodeT(attr.st_ino), path) return attr - async def getattr(self, inode, ctx=None): + async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes: if inode in self._inode_fd_map: return self._getattr(fd=self._inode_fd_map[inode]) else: return self._getattr(path=self._inode_to_path(inode)) - def _getattr(self, path=None, fd=None): + def _getattr(self, path: str | None = None, fd: int | None = None) -> EntryAttributes: assert fd is None or path is None assert not (fd is None and path is None) try: @@ -138,7 +151,7 @@ def _getattr(self, path=None, fd=None): assert exc.errno is not None raise FUSEError(exc.errno) - entry = pyfuse3.EntryAttributes() + entry = EntryAttributes() for attr in ( 'st_ino', 'st_mode', @@ -160,7 +173,7 @@ def _getattr(self, path=None, fd=None): return entry - async def readlink(self, inode, ctx): + async def readlink(self, inode: InodeT, ctx: RequestContext) -> bytes: path = self._inode_to_path(inode) try: target = os.readlink(path) @@ -169,19 +182,19 @@ async def readlink(self, inode, ctx): raise FUSEError(exc.errno) return fsencode(target) - async def opendir(self, inode, ctx): + async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT: # For simplicity, we use the inode as file handle return FileHandleT(inode) - async def readdir(self, fh, start_id, token): - path = self._inode_to_path(fh) + async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None: + path = self._inode_to_path(InodeT(fh)) log.debug('reading %s', path) - entries = [] + entries: list[tuple[InodeT, str, EntryAttributes]] = [] for name in os.listdir(path): if name == '.' or name == '..': continue attr = self._getattr(path=os.path.join(path, name)) - entries.append((attr.st_ino, name, attr)) + entries.append((InodeT(attr.st_ino), name, attr)) log.debug('read %d entries, starting at %d', len(entries), start_id) @@ -198,10 +211,10 @@ async def readdir(self, fh, start_id, token): break self._add_path(attr.st_ino, os.path.join(path, name)) - async def unlink(self, parent_inode, name, ctx): - name = fsdecode(name) + async def unlink(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None: + name_str = fsdecode(name) parent = self._inode_to_path(parent_inode) - path = os.path.join(parent, name) + path = os.path.join(parent, name_str) try: inode = os.lstat(path).st_ino os.unlink(path) @@ -209,12 +222,12 @@ async def unlink(self, parent_inode, name, ctx): assert exc.errno is not None raise FUSEError(exc.errno) if inode in self._lookup_cnt: - self._forget_path(inode, path) + self._forget_path(InodeT(inode), path) - async def rmdir(self, parent_inode, name, ctx): - name = fsdecode(name) + async def rmdir(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None: + name_str = fsdecode(name) parent = self._inode_to_path(parent_inode) - path = os.path.join(parent, name) + path = os.path.join(parent, name_str) try: inode = os.lstat(path).st_ino os.rmdir(path) @@ -222,9 +235,9 @@ async def rmdir(self, parent_inode, name, ctx): assert exc.errno is not None raise FUSEError(exc.errno) if inode in self._lookup_cnt: - self._forget_path(inode, path) + self._forget_path(InodeT(inode), path) - def _forget_path(self, inode, path): + def _forget_path(self, inode: InodeT, path: str) -> None: log.debug('forget %s for %d', path, inode) val = self._inode_path_map[inode] if isinstance(val, set): @@ -234,31 +247,41 @@ def _forget_path(self, inode, path): else: del self._inode_path_map[inode] - async def symlink(self, parent_inode, name, target, ctx): - name = fsdecode(name) - target = fsdecode(target) + async def symlink( + self, parent_inode: InodeT, name: bytes, target: bytes, ctx: RequestContext + ) -> EntryAttributes: + name_str = fsdecode(name) + target_str = fsdecode(target) parent = self._inode_to_path(parent_inode) - path = os.path.join(parent, name) + path = os.path.join(parent, name_str) try: - os.symlink(target, path) + os.symlink(target_str, path) os.lchown(path, ctx.uid, ctx.gid) except OSError as exc: assert exc.errno is not None raise FUSEError(exc.errno) - stat = os.lstat(path) - self._add_path(stat.st_ino, path) - return await self.getattr(InodeT(stat.st_ino)) - - async def rename(self, parent_inode_old, name_old, parent_inode_new, name_new, flags, ctx): + inode = InodeT(os.lstat(path).st_ino) + self._add_path(inode, path) + return await self.getattr(inode, ctx) + + async def rename( + self, + parent_inode_old: InodeT, + name_old: bytes, + parent_inode_new: InodeT, + name_new: bytes, + flags: int, + ctx: RequestContext, + ) -> None: if flags != 0: raise FUSEError(errno.EINVAL) - name_old = fsdecode(name_old) - name_new = fsdecode(name_new) + name_old_str = fsdecode(name_old) + name_new_str = fsdecode(name_new) parent_old = self._inode_to_path(parent_inode_old) parent_new = self._inode_to_path(parent_inode_new) - path_old = os.path.join(parent_old, name_old) - path_new = os.path.join(parent_new, name_new) + path_old = os.path.join(parent_old, name_old_str) + path_new = os.path.join(parent_new, name_new_str) try: os.rename(path_old, path_new) inode = cast(InodeT, os.lstat(path_new).st_ino) @@ -277,19 +300,28 @@ async def rename(self, parent_inode_old, name_old, parent_inode_new, name_new, f assert val == path_old self._inode_path_map[inode] = path_new - async def link(self, inode, new_parent_inode, new_name, ctx): - new_name = fsdecode(new_name) + async def link( + self, inode: InodeT, new_parent_inode: InodeT, new_name: bytes, ctx: RequestContext + ) -> EntryAttributes: + new_name_str = fsdecode(new_name) parent = self._inode_to_path(new_parent_inode) - path = os.path.join(parent, new_name) + path = os.path.join(parent, new_name_str) try: os.link(self._inode_to_path(inode), path, follow_symlinks=False) except OSError as exc: assert exc.errno is not None raise FUSEError(exc.errno) self._add_path(inode, path) - return await self.getattr(inode) - - async def setattr(self, inode, attr, fields, fh, ctx): + return await self.getattr(inode, ctx) + + async def setattr( + self, + inode: InodeT, + attr: EntryAttributes, + fields: SetattrFields, + fh: FileHandleT | None, + ctx: RequestContext, + ) -> EntryAttributes: try: if fields.update_size: if fh is None: @@ -363,9 +395,11 @@ async def setattr(self, inode, attr, fields, fh, ctx): assert exc.errno is not None raise FUSEError(exc.errno) - return await self.getattr(inode) + return await self.getattr(inode, ctx) - async def mknod(self, parent_inode, name, mode, rdev, ctx): + async def mknod( + self, parent_inode: InodeT, name: bytes, mode: int, rdev: int, ctx: RequestContext + ) -> EntryAttributes: path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name)) try: os.mknod(path, mode=(mode & ~ctx.umask), device=rdev) @@ -377,7 +411,9 @@ async def mknod(self, parent_inode, name, mode, rdev, ctx): self._add_path(attr.st_ino, path) return attr - async def mkdir(self, parent_inode, name, mode, ctx): + async def mkdir( + self, parent_inode: InodeT, name: bytes, mode: int, ctx: RequestContext + ) -> EntryAttributes: path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name)) try: os.mkdir(path, mode=(mode & ~ctx.umask)) @@ -389,9 +425,10 @@ async def mkdir(self, parent_inode, name, mode, ctx): self._add_path(attr.st_ino, path) return attr - async def statfs(self, ctx): + async def statfs(self, ctx: RequestContext) -> StatvfsData: root = self._inode_path_map[pyfuse3.ROOT_INODE] - stat_ = pyfuse3.StatvfsData() + assert isinstance(root, str) + stat_ = StatvfsData() try: statfs = os.statvfs(root) except OSError as exc: @@ -411,11 +448,11 @@ async def statfs(self, ctx): stat_.f_namemax = statfs.f_namemax - (len(root) + 1) return stat_ - async def open(self, inode, flags, ctx): + async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo: if inode in self._inode_fd_map: fd = self._inode_fd_map[inode] self._fd_open_count[fd] += 1 - return pyfuse3.FileInfo(fh=fd) + return FileInfo(fh=FileHandleT(fd)) assert flags & os.O_CREAT == 0 try: fd = os.open(self._inode_to_path(inode), flags) @@ -425,9 +462,11 @@ async def open(self, inode, flags, ctx): self._inode_fd_map[inode] = fd self._fd_inode_map[fd] = inode self._fd_open_count[fd] = 1 - return pyfuse3.FileInfo(fh=cast(FileHandleT, fd)) + return FileInfo(fh=cast(FileHandleT, fd)) - async def create(self, parent_inode, name, mode, flags, ctx): + async def create( + self, parent_inode: InodeT, name: bytes, mode: int, flags: int, ctx: RequestContext + ) -> tuple[FileInfo, EntryAttributes]: path = os.path.join(self._inode_to_path(parent_inode), fsdecode(name)) try: fd = os.open(path, flags | os.O_CREAT | os.O_TRUNC) @@ -439,17 +478,17 @@ async def create(self, parent_inode, name, mode, flags, ctx): self._inode_fd_map[attr.st_ino] = fd self._fd_inode_map[fd] = attr.st_ino self._fd_open_count[fd] = 1 - return (pyfuse3.FileInfo(fh=cast(FileHandleT, fd)), attr) + return (FileInfo(fh=cast(FileHandleT, fd)), attr) - async def read(self, fh, off, size): + async def read(self, fh: FileHandleT, off: int, size: int) -> bytes: os.lseek(fh, off, os.SEEK_SET) return os.read(fh, size) - async def write(self, fh, off, buf): + async def write(self, fh: FileHandleT, off: int, buf: bytes) -> int: os.lseek(fh, off, os.SEEK_SET) return os.write(fh, buf) - async def release(self, fh): + async def release(self, fh: FileHandleT) -> None: if self._fd_open_count[fh] > 1: self._fd_open_count[fh] -= 1 return @@ -465,7 +504,7 @@ async def release(self, fh): raise FUSEError(exc.errno) -def init_logging(debug=False): +def init_logging(debug: bool = False) -> None: formatter = logging.Formatter( '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S", @@ -482,7 +521,7 @@ def init_logging(debug=False): root_logger.addHandler(handler) -def parse_args(args): +def parse_args(args: list[str]) -> Namespace: '''Parse command line''' parser = ArgumentParser() @@ -499,7 +538,7 @@ def parse_args(args): return parser.parse_args(args) -def main(): +def main() -> None: options = parse_args(sys.argv[1:]) init_logging(options.debug) operations = Operations(options.source) diff --git a/examples/tmpfs.py b/examples/tmpfs.py index 2d16574..5295a0e 100755 --- a/examples/tmpfs.py +++ b/examples/tmpfs.py @@ -26,15 +26,25 @@ import os import sqlite3 import stat -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from collections import defaultdict from time import time -from typing import cast +from typing import Any, cast import trio import pyfuse3 -from pyfuse3 import FileHandleT, FileInfo, FUSEError, InodeT +from pyfuse3 import ( + EntryAttributes, + FileHandleT, + FileInfo, + FUSEError, + InodeT, + ReaddirToken, + RequestContext, + SetattrFields, + StatvfsData, +) try: import faulthandler @@ -61,16 +71,16 @@ class Operations(pyfuse3.Operations): enable_writeback_cache = True - def __init__(self): + def __init__(self) -> None: super(Operations, self).__init__() - self.db = sqlite3.connect(':memory:') + self.db: sqlite3.Connection = sqlite3.connect(':memory:') self.db.text_factory = str self.db.row_factory = sqlite3.Row - self.cursor = self.db.cursor() - self.inode_open_count = defaultdict(int) + self.cursor: sqlite3.Cursor = self.db.cursor() + self.inode_open_count: defaultdict[InodeT, int] = defaultdict(int) self.init_tables() - def init_tables(self): + def init_tables(self) -> None: '''Initialize file system tables''' self.cursor.execute(""" @@ -126,7 +136,7 @@ def init_tables(self): (b'..', pyfuse3.ROOT_INODE, pyfuse3.ROOT_INODE), ) - def get_row(self, *a, **kw): + def get_row(self, *a: Any, **kw: Any) -> sqlite3.Row: self.cursor.execute(*a, **kw) try: row = next(self.cursor) @@ -141,7 +151,9 @@ def get_row(self, *a, **kw): return row - async def lookup(self, parent_inode, name, ctx=None): + async def lookup( + self, parent_inode: InodeT, name: bytes, ctx: RequestContext + ) -> EntryAttributes: if name == b'.': inode = parent_inode elif name == b'..': @@ -158,13 +170,13 @@ async def lookup(self, parent_inode, name, ctx=None): return await self.getattr(InodeT(inode), ctx) - async def getattr(self, inode, ctx=None): + async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes: try: row = self.get_row("SELECT * FROM inodes WHERE id=?", (inode,)) except NoSuchRowError: raise (pyfuse3.FUSEError(errno.ENOENT)) - entry = pyfuse3.EntryAttributes() + entry = EntryAttributes() entry.st_ino = inode entry.generation = 0 entry.entry_timeout = 300 @@ -186,14 +198,14 @@ async def getattr(self, inode, ctx=None): return entry - async def readlink(self, inode, ctx): + async def readlink(self, inode: InodeT, ctx: RequestContext) -> bytes: return self.get_row('SELECT * FROM inodes WHERE id=?', (inode,))['target'] - async def opendir(self, inode, ctx): + async def opendir(self, inode: InodeT, ctx: RequestContext) -> FileHandleT: # For simplicity, we use the inode as file handle return FileHandleT(inode) - async def readdir(self, fh, start_id, token): + async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None: if start_id == 0: off = -1 else: @@ -209,23 +221,23 @@ async def readdir(self, fh, start_id, token): token, row['name'], await self.getattr(InodeT(row['inode'])), row['rowid'] ) - async def unlink(self, parent_inode, name, ctx): - entry = await self.lookup(parent_inode, name) + async def unlink(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None: + entry = await self.lookup(parent_inode, name, ctx) if stat.S_ISDIR(entry.st_mode): raise pyfuse3.FUSEError(errno.EISDIR) self._remove(parent_inode, name, entry) - async def rmdir(self, parent_inode, name, ctx): - entry = await self.lookup(parent_inode, name) + async def rmdir(self, parent_inode: InodeT, name: bytes, ctx: RequestContext) -> None: + entry = await self.lookup(parent_inode, name, ctx) if not stat.S_ISDIR(entry.st_mode): raise pyfuse3.FUSEError(errno.ENOTDIR) self._remove(parent_inode, name, entry) - def _remove(self, parent_inode, name, entry): + def _remove(self, parent_inode: InodeT, name: bytes, entry: EntryAttributes) -> None: if ( self.get_row("SELECT COUNT(inode) FROM contents WHERE parent_inode=?", (entry.st_ino,))[ 0 @@ -241,7 +253,9 @@ def _remove(self, parent_inode, name, entry): if entry.st_nlink == 1 and entry.st_ino not in self.inode_open_count: self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry.st_ino,)) - async def symlink(self, parent_inode, name, target, ctx): + async def symlink( + self, parent_inode: InodeT, name: bytes, target: bytes, ctx: RequestContext + ) -> EntryAttributes: mode = ( stat.S_IFLNK | stat.S_IRUSR @@ -256,16 +270,26 @@ async def symlink(self, parent_inode, name, target, ctx): ) return await self._create(parent_inode, name, mode, ctx, target=target) - async def rename(self, parent_inode_old, name_old, parent_inode_new, name_new, flags, ctx): + async def rename( + self, + parent_inode_old: InodeT, + name_old: bytes, + parent_inode_new: InodeT, + name_new: bytes, + flags: int, + ctx: RequestContext, + ) -> None: if flags != 0: raise FUSEError(errno.EINVAL) - entry_old = await self.lookup(parent_inode_old, name_old) + entry_old = await self.lookup(parent_inode_old, name_old, ctx) entry_new = None try: entry_new = await self.lookup( - parent_inode_new, name_new if isinstance(name_new, bytes) else name_new.encode() + parent_inode_new, + name_new if isinstance(name_new, bytes) else name_new.encode(), + ctx, ) except pyfuse3.FUSEError as exc: if exc.errno != errno.ENOENT: @@ -282,8 +306,14 @@ async def rename(self, parent_inode_old, name_old, parent_inode_new, name_new, f ) def _replace( - self, parent_inode_old, name_old, parent_inode_new, name_new, entry_old, entry_new - ): + self, + parent_inode_old: InodeT, + name_old: bytes, + parent_inode_new: InodeT, + name_new: bytes, + entry_old: EntryAttributes, + entry_new: EntryAttributes, + ) -> None: if ( self.get_row( "SELECT COUNT(inode) FROM contents WHERE parent_inode=?", (entry_new.st_ino,) @@ -303,8 +333,10 @@ def _replace( if entry_new.st_nlink == 1 and entry_new.st_ino not in self.inode_open_count: self.cursor.execute("DELETE FROM inodes WHERE id=?", (entry_new.st_ino,)) - async def link(self, inode, new_parent_inode, new_name, ctx): - entry_p = await self.getattr(new_parent_inode) + async def link( + self, inode: InodeT, new_parent_inode: InodeT, new_name: bytes, ctx: RequestContext + ) -> EntryAttributes: + entry_p = await self.getattr(new_parent_inode, ctx) if entry_p.st_nlink == 0: log.warning( 'Attempted to create entry %s with unlinked parent %d', new_name, new_parent_inode @@ -316,9 +348,16 @@ async def link(self, inode, new_parent_inode, new_name, ctx): (new_name, inode, new_parent_inode), ) - return await self.getattr(inode) + return await self.getattr(inode, ctx) - async def setattr(self, inode, attr, fields, fh, ctx): + async def setattr( + self, + inode: InodeT, + attr: EntryAttributes, + fields: SetattrFields, + fh: FileHandleT | None, + ctx: RequestContext, + ) -> EntryAttributes: if fields.update_size: data = self.get_row('SELECT data FROM inodes WHERE id=?', (inode,))[0] if data is None: @@ -359,16 +398,20 @@ async def setattr(self, inode, attr, fields, fh, ctx): 'UPDATE inodes SET ctime_ns=? WHERE id=?', (int(time() * 1e9), inode) ) - return await self.getattr(inode) + return await self.getattr(inode, ctx) - async def mknod(self, parent_inode, name, mode, rdev, ctx): + async def mknod( + self, parent_inode: InodeT, name: bytes, mode: int, rdev: int, ctx: RequestContext + ) -> EntryAttributes: return await self._create(parent_inode, name, mode, ctx, rdev=rdev) - async def mkdir(self, parent_inode, name, mode, ctx): + async def mkdir( + self, parent_inode: InodeT, name: bytes, mode: int, ctx: RequestContext + ) -> EntryAttributes: return await self._create(parent_inode, name, mode, ctx) - async def statfs(self, ctx): - stat_ = pyfuse3.StatvfsData() + async def statfs(self, ctx: RequestContext) -> StatvfsData: + stat_ = StatvfsData() stat_.f_bsize = 512 stat_.f_frsize = 512 @@ -385,26 +428,36 @@ async def statfs(self, ctx): return stat_ - async def open(self, inode, flags, ctx): + async def open(self, inode: InodeT, flags: int, ctx: RequestContext) -> FileInfo: self.inode_open_count[inode] += 1 # For simplicity, we use the inode as file handle return FileInfo(fh=FileHandleT(inode)) - async def access(self, inode, mode, ctx): + async def access(self, inode: InodeT, mode: int, ctx: RequestContext) -> bool: # Yeah, could be a function and has unused arguments # pylint: disable=R0201,W0613 return True - async def create(self, parent_inode, name, mode, flags, ctx): + async def create( + self, parent_inode: InodeT, name: bytes, mode: int, flags: int, ctx: RequestContext + ) -> tuple[FileInfo, EntryAttributes]: # pylint: disable=W0612 entry = await self._create(parent_inode, name, mode, ctx) self.inode_open_count[entry.st_ino] += 1 # For simplicity, we use the inode as file handle return (FileInfo(fh=FileHandleT(entry.st_ino)), entry) - async def _create(self, parent_inode, name, mode, ctx, rdev=0, target=None): - if (await self.getattr(parent_inode)).st_nlink == 0: + async def _create( + self, + parent_inode: InodeT, + name: bytes, + mode: int, + ctx: RequestContext, + rdev: int = 0, + target: bytes | None = None, + ) -> EntryAttributes: + if (await self.getattr(parent_inode, ctx)).st_nlink == 0: log.warning('Attempted to create entry %s with unlinked parent %d', name, parent_inode) raise FUSEError(errno.EINVAL) @@ -420,15 +473,15 @@ async def _create(self, parent_inode, name, mode, ctx, rdev=0, target=None): "INSERT INTO contents(name, inode, parent_inode) VALUES(?,?,?)", (name, inode, parent_inode), ) - return await self.getattr(inode) + return await self.getattr(inode, ctx) - async def read(self, fh, off, size): + async def read(self, fh: FileHandleT, off: int, size: int) -> bytes: data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0] if data is None: data = b'' return data[off : off + size] - async def write(self, fh, off, buf): + async def write(self, fh: FileHandleT, off: int, buf: bytes) -> int: data = self.get_row('SELECT data FROM inodes WHERE id=?', (fh,))[0] if data is None: data = b'' @@ -439,7 +492,7 @@ async def write(self, fh, off, buf): ) return len(buf) - async def release(self, fh): + async def release(self, fh: FileHandleT) -> None: inode = cast(InodeT, fh) self.inode_open_count[inode] -= 1 @@ -450,16 +503,16 @@ async def release(self, fh): class NoUniqueValueError(Exception): - def __str__(self): + def __str__(self) -> str: return 'Query generated more than 1 result row' class NoSuchRowError(Exception): - def __str__(self): + def __str__(self) -> str: return 'Query produced 0 result rows' -def init_logging(debug=False): +def init_logging(debug: bool = False) -> None: formatter = logging.Formatter( '%(asctime)s.%(msecs)03d %(threadName)s: [%(name)s] %(message)s', datefmt="%Y-%m-%d %H:%M:%S", @@ -476,7 +529,7 @@ def init_logging(debug=False): root_logger.addHandler(handler) -def parse_args(): +def parse_args() -> Namespace: '''Parse command line''' parser = ArgumentParser() diff --git a/test/pytest_checklogs.py b/test/pytest_checklogs.py index 095ba86..ef3f4aa 100644 --- a/test/pytest_checklogs.py +++ b/test/pytest_checklogs.py @@ -16,22 +16,25 @@ import functools import logging import re +from collections.abc import Generator from contextlib import contextmanager import pytest class CountMessagesHandler(logging.Handler): - def __init__(self, level=logging.NOTSET): + def __init__(self, level: int = logging.NOTSET) -> None: super().__init__(level) - self.count = 0 + self.count: int = 0 - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: self.count += 1 @contextmanager -def assert_logs(pattern, level=logging.WARNING, count=None): +def assert_logs( + pattern: str, level: int = logging.WARNING, count: int | None = None +) -> Generator[None, None, None]: '''Assert that suite emits specified log message *pattern* is matched against the *unformatted* log message, i.e. before any @@ -44,7 +47,7 @@ def assert_logs(pattern, level=logging.WARNING, count=None): does not generate exceptions for them (no matter their severity). ''' - def filter(record): + def filter(record: logging.LogRecord) -> bool: if record.levelno == level and re.search(pattern, record.msg): record.checklogs_ignore = True return True @@ -102,7 +105,7 @@ def check_test_output(capfd, item): pytest.fail('Suspicious output to stdout (matched "%s")' % hit.group(0)) -def register_output(item, pattern, count=1, flags=re.MULTILINE): +def register_output(item, pattern: str, count: int = 1, flags: int = re.MULTILINE) -> None: '''Register *pattern* as false positive for output checking This prevents the test from failing because the output otherwise diff --git a/test/test_api.py b/test/test_api.py index 2348dde..6a9eca9 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -25,6 +25,7 @@ import pytest import pyfuse3 +from pyfuse3 import EntryAttributes, RequestContext, SetattrFields, StatvfsData def test_listdir(): @@ -68,7 +69,7 @@ def _getxattr_helper(path, name): def test_entry_res(): - a = pyfuse3.EntryAttributes() + a = EntryAttributes() val = 1000.2735 a.st_atime_ns = int(val * 1e9) assert a.st_atime_ns / 1e9 == val @@ -99,10 +100,10 @@ def test_xattr(): def test_copy(): - for obj in (pyfuse3.SetattrFields(), pyfuse3.RequestContext()): + for obj in (SetattrFields(), RequestContext()): pytest.raises(PicklingError, copy, obj) - for inst, attr in ((pyfuse3.EntryAttributes(), 'st_mode'), (pyfuse3.StatvfsData(), 'f_files')): + for inst, attr in ((EntryAttributes(), 'st_mode'), (StatvfsData(), 'f_files')): setattr(inst, attr, 42) inst_copy = copy(inst) assert getattr(inst, attr) == getattr(inst_copy, attr) diff --git a/test/test_fs.py b/test/test_fs.py index 309c557..ed1bbe0 100755 --- a/test/test_fs.py +++ b/test/test_fs.py @@ -28,7 +28,15 @@ import trio import pyfuse3 -from pyfuse3 import FileHandleT, FileInfo, FUSEError +from pyfuse3 import ( + EntryAttributes, + FileHandleT, + FileInfo, + FUSEError, + InodeT, + ReaddirToken, + RequestContext, +) from util import cleanup, fuse_test_marker, umount, wait_for_mount pytestmark = fuse_test_marker() @@ -165,7 +173,7 @@ class Fs(pyfuse3.Operations): def __init__(self, cross_process): super(Fs, self).__init__() self.hello_name = b"message" - self.hello_inode = cast(pyfuse3.InodeT, pyfuse3.ROOT_INODE + 1) + self.hello_inode = cast(InodeT, pyfuse3.ROOT_INODE + 1) self.hello_data = b"hello world\n" self.status = cross_process self.lookup_cnt = 0 @@ -175,8 +183,8 @@ def __init__(self, cross_process): self.status.entry_timeout = 99999 self.status.attr_timeout = 99999 - async def getattr(self, inode, ctx=None): - entry = pyfuse3.EntryAttributes() + async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> EntryAttributes: + entry = EntryAttributes() if inode == pyfuse3.ROOT_INODE: entry.st_mode = stat.S_IFDIR | 0o755 entry.st_size = 0 @@ -207,12 +215,14 @@ async def forget(self, inode_list): else: assert inode == pyfuse3.ROOT_INODE - async def lookup(self, parent_inode, name, ctx=None): + async def lookup( + self, parent_inode: InodeT, name: bytes, ctx: RequestContext + ) -> EntryAttributes: if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name: raise pyfuse3.FUSEError(errno.ENOENT) self.lookup_cnt += 1 self.status.lookup_called = True - return await self.getattr(self.hello_inode) + return await self.getattr(self.hello_inode, ctx) async def opendir(self, inode, ctx): if inode != pyfuse3.ROOT_INODE: @@ -220,7 +230,7 @@ async def opendir(self, inode, ctx): # For simplicity, we use the inode as file handle return FileHandleT(inode) - async def readdir(self, fh, start_id, token): + async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None: assert fh == pyfuse3.ROOT_INODE if start_id == 0: pyfuse3.readdir_reply(token, self.hello_name, await self.getattr(self.hello_inode), 1) diff --git a/test/test_rounding.py b/test/test_rounding.py index c8e7909..0d74468 100755 --- a/test/test_rounding.py +++ b/test/test_rounding.py @@ -16,14 +16,13 @@ sys.exit(pytest.main([__file__] + sys.argv[1:])) -import pyfuse3 -from pyfuse3 import _NANOS_PER_SEC +from pyfuse3 import _NANOS_PER_SEC, EntryAttributes def test_rounding(): # Incorrect division previously resulted in rounding errors for # all dates. - entry = pyfuse3.EntryAttributes() + entry = EntryAttributes() # Approximately 67 years, ending in 999. # Note: 67 years were chosen to avoid y2038 issues (1970 + 67 = 2037). diff --git a/test/util.py b/test/util.py index 6e1c2e9..70b8d87 100644 --- a/test/util.py +++ b/test/util.py @@ -9,15 +9,21 @@ the terms of the GNU LGPL. ''' +import multiprocessing import os import platform import shutil import stat import subprocess import time +from collections.abc import Callable +from multiprocessing.context import ForkProcess +from typing import TypeVar import pytest +Process = subprocess.Popen | multiprocessing.Process | ForkProcess + def fuse_test_marker(): '''Return a pytest.marker that indicates FUSE availability @@ -56,7 +62,7 @@ def fuse_test_marker(): return pytest.mark.uses_fuse() -def exitcode(process): +def exitcode(process: Process) -> int | None: if isinstance(process, subprocess.Popen): return process.poll() else: @@ -66,13 +72,16 @@ def exitcode(process): return process.exitcode -def wait_for(callable, timeout=10, interval=0.1): +T = TypeVar('T') + + +def wait_for(callable: Callable[[], T], timeout: float = 10, interval: float = 0.1) -> T | None: '''Wait until *callable* returns something True and return it If *timeout* expires, return None ''' - waited = 0 + waited = 0.0 while True: ret = callable() if ret: @@ -83,7 +92,7 @@ def wait_for(callable, timeout=10, interval=0.1): time.sleep(interval) -def wait_for_mount(mount_process, mnt_dir): +def wait_for_mount(mount_process: Process, mnt_dir: str) -> bool: elapsed = 0.0 while elapsed < 30: if os.path.ismount(mnt_dir): @@ -95,7 +104,7 @@ def wait_for_mount(mount_process, mnt_dir): pytest.fail("mountpoint failed to come up") -def cleanup(mount_process, mnt_dir): +def cleanup(mount_process: Process, mnt_dir: str) -> None: if platform.system() == 'Darwin': subprocess.call( ['umount', '-l', mnt_dir], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT @@ -117,7 +126,7 @@ def cleanup(mount_process, mnt_dir): mount_process.kill() -def umount(mount_process, mnt_dir): +def umount(mount_process: Process, mnt_dir: str) -> None: if platform.system() == 'Darwin': subprocess.check_call(['umount', '-l', mnt_dir]) else: @@ -138,13 +147,12 @@ def umount(mount_process, mnt_dir): mount_process.kill() else: mount_process.join(5) - code = mount_process.exitcode - if code == 0: + if mount_process.exitcode == 0: return - elif code is None: + elif mount_process.exitcode is None: mount_process.terminate() mount_process.join(1) else: - pytest.fail('file system process terminated with code %s' % (code,)) + pytest.fail('file system process terminated with code %s' % (mount_process.exitcode,)) pytest.fail('mount process did not terminate')