_util.py 1.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. from __future__ import annotations
  2. import os
  3. import stat
  4. import sys
  5. from errno import EACCES, EISDIR
  6. from pathlib import Path
  7. def raise_on_not_writable_file(filename: str) -> None:
  8. """
  9. Raise an exception if attempting to open the file for writing would fail.
  10. This is done so files that will never be writable can be separated from
  11. files that are writable but currently locked
  12. :param filename: file to check
  13. :raises OSError: as if the file was opened for writing.
  14. """
  15. try: # use stat to do exists + can write to check without race condition
  16. file_stat = os.stat(filename) # noqa: PTH116
  17. except OSError:
  18. return # swallow does not exist or other errors
  19. if file_stat.st_mtime != 0: # if os.stat returns but modification is zero that's an invalid os.stat - ignore it
  20. if not (file_stat.st_mode & stat.S_IWUSR):
  21. raise PermissionError(EACCES, "Permission denied", filename)
  22. if stat.S_ISDIR(file_stat.st_mode):
  23. if sys.platform == "win32": # pragma: win32 cover
  24. # On Windows, this is PermissionError
  25. raise PermissionError(EACCES, "Permission denied", filename)
  26. else: # pragma: win32 no cover # noqa: RET506
  27. # On linux / macOS, this is IsADirectoryError
  28. raise IsADirectoryError(EISDIR, "Is a directory", filename)
  29. def ensure_directory_exists(filename: Path | str) -> None:
  30. """
  31. Ensure the directory containing the file exists (create it if necessary)
  32. :param filename: file.
  33. """
  34. Path(filename).parent.mkdir(parents=True, exist_ok=True)
  35. __all__ = [
  36. "raise_on_not_writable_file",
  37. "ensure_directory_exists",
  38. ]