Adds CreateFileWithMode helper to create a new file with a given
authorScott Gasch <[email protected]>
Thu, 9 Feb 2023 23:34:50 +0000 (15:34 -0800)
committerScott Gasch <[email protected]>
Thu, 9 Feb 2023 23:34:50 +0000 (15:34 -0800)
mode more easily.

src/pyutils/files/file_utils.py

index 4412a41293c0b4e1567411d8bc1b02e3cce0e1ab..8c3351f1717f73046ed8ce1a917a05c6591eb36a 100644 (file)
@@ -312,7 +312,7 @@ def create_path_if_not_exist(path, on_error=None) -> None:
 
     .. warning::
 
-        Files are created with mode 0x0777 (i.e. world read/writeable).
+        Files are created with mode 0o0777 (i.e. world read/writeable).
 
     >>> import uuid
     >>> import os
@@ -1192,7 +1192,6 @@ class FileWriter(contextlib.AbstractContextManager):
             print("This is a test!", file=w)
             time.sleep(2)
             print("This is only a test...", file=w)
-
     """
 
     def __init__(self, filename: str) -> None:
@@ -1221,6 +1220,65 @@ class FileWriter(contextlib.AbstractContextManager):
         return False
 
 
+class CreateFileWithMode(contextlib.AbstractContextManager):
+    """This helper context manager can be used instead of the typical
+    pattern for creating a file if you want to ensure that the file
+    created is a particular permission mode upon creation.
+
+    Python's open doesn't support this; you need to set the os.umask
+    and then create a descriptor to open via os.open, see below.
+
+        >>> import os
+        >>> filename = f'/tmp/CreateFileWithModeTest.{os.getpid()}'
+        >>> with CreateFileWithMode(filename, mode=0o600) as wf:
+        ...     print('This is a test', file=wf)
+        >>> result = os.stat(filename)
+
+        Note: there is a high order bit set in this that is S_IFREG indicating
+        that the file is a "normal file".  Clear it with the mask.
+
+        >>> print(f'{result.st_mode & 0o7777:o}')
+        600
+        >>> with open(filename, 'r') as rf:
+        ...     contents = rf.read()
+        >>> contents
+        'This is a test\\n'
+        >>> remove(filename)
+    """
+
+    def __init__(self, filename: str, mode=0o600) -> None:
+        """
+        Args:
+            filename: path of the file to create.  It must not already
+                exist or we raise an Exception.
+            mode: the UNIX-style octal mode with which to create the
+                filename.
+        """
+        self.filename = filename
+        self.mode = mode & 0o7777
+        self.handle: Optional[TextIO] = None
+        self.old_umask = os.umask(0)
+
+    def __enter__(self) -> TextIO:
+        if does_file_exist(self.filename):
+            raise Exception(
+                f"{self.filename} already exists; it must not to use this class"
+            )
+        descriptor = os.open(
+            path=self.filename,
+            flags=(os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
+            mode=self.mode,
+        )
+        self.handle = open(descriptor, "w")
+        return self.handle
+
+    def __exit__(self, exc_type, exc_val, exc_tb) -> Literal[False]:
+        os.umask(self.old_umask)
+        if self.handle is not None:
+            self.handle.close()
+        return False
+
+
 if __name__ == "__main__":
     import doctest