From c5c9fe325782a790d563a0a8b1cf62a855a50d81 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Sat, 25 May 2024 10:22:21 +0200 Subject: add a FileDescriptor type It serves two main purposes. For one thing, it allows telling bare integers and file descriptors apart on a typing level similar to a NewType. For another it adds common methods to a file descriptor and enables closing it via a context manager. --- linuxnamespaces/filedescriptor.py | 75 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 linuxnamespaces/filedescriptor.py (limited to 'linuxnamespaces/filedescriptor.py') diff --git a/linuxnamespaces/filedescriptor.py b/linuxnamespaces/filedescriptor.py new file mode 100644 index 0000000..4395a54 --- /dev/null +++ b/linuxnamespaces/filedescriptor.py @@ -0,0 +1,75 @@ +# Copyright 2024 Helmut Grohne +# SPDX-License-Identifier: GPL-3 + +"""A type tag for integers that represent file descriptors.""" + +import fcntl +import os +import typing + + +class FileDescriptor(int): + """Type tag for integers that represent file descriptors. It also provides + a few very generic file descriptor methods. + """ + + def __enter__(self) -> "FileDescriptor": + """When used as a context manager, close the file descriptor on scope + exit. + """ + return self + + def __exit__(self, *args: typing.Any) -> None: + """When used as a context manager, close the file descriptor on scope + exit. + """ + self.close() + + def close(self) -> None: + """Close the file descriptor. Since int is immutable, the caller is + responsibe for not closing twice. + """ + os.close(self) + + def dup(self, inheritable: bool = True) -> "FileDescriptor": + """Return a duplicate of the file descriptor.""" + if inheritable: + return FileDescriptor(os.dup(self)) + return FileDescriptor(fcntl.fcntl(self, fcntl.F_DUPFD_CLOEXEC, 0)) + + def dup2(self, fd2: int, inheritable: bool = True) -> "FileDescriptor": + """Duplicate the file to the given file descriptor number.""" + return FileDescriptor(os.dup2(self, fd2, inheritable)) + + def fileno(self) -> int: + """Return self such that it satisfies the HasFileno protocol.""" + return self + + def get_blocking(self) -> bool: + """Get the blocking mode of the file descriptor.""" + return os.get_blocking(self) + + def get_inheritable(self) -> bool: + """Get the close-on-exec flag of the file descriptor.""" + return os.get_inheritable(self) + + @classmethod + def pipe( + cls, blocking: bool = True, inheritable: bool = True + ) -> tuple["FileDescriptor", "FileDescriptor"]: + """Create a pipe with flags set atomically. This actually corresponds + to the pipe2 syscall, but skipping flags is equivalent to calling pipe. + """ + rfd, wfd = os.pipe2( + (0 if blocking else os.O_NONBLOCK) + | (0 if inheritable else os.O_CLOEXEC), + ) + return (cls(rfd), cls(wfd)) + + def set_blocking(self, blocking: bool) -> None: + """Set the blocking mode of the file descriptor.""" + os.set_blocking(self, blocking) + + def set_inheritable(self, inheritable: bool) -> None: + """Set the close-on-exec flag of the file descriptor.""" + os.set_inheritable(self, inheritable) -- cgit v1.2.3