_fixes.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import contextlib
  2. import os
  3. import shutil
  4. import stat
  5. import tempfile
  6. import time
  7. from collections.abc import Callable, Generator
  8. from functools import partial
  9. from pathlib import Path
  10. import yaml
  11. from filelock import BaseFileLock, FileLock, SoftFileLock, Timeout
  12. from .. import constants
  13. from . import logging
  14. logger = logging.get_logger(__name__)
  15. # Wrap `yaml.dump` to set `allow_unicode=True` by default.
  16. #
  17. # Example:
  18. # ```py
  19. # >>> yaml.dump({"emoji": "👀", "some unicode": "日本か"})
  20. # 'emoji: "\\U0001F440"\nsome unicode: "\\u65E5\\u672C\\u304B"\n'
  21. #
  22. # >>> yaml_dump({"emoji": "👀", "some unicode": "日本か"})
  23. # 'emoji: "👀"\nsome unicode: "日本か"\n'
  24. # ```
  25. yaml_dump: Callable[..., str] = partial(yaml.dump, stream=None, allow_unicode=True) # type: ignore
  26. @contextlib.contextmanager
  27. def SoftTemporaryDirectory(
  28. suffix: str | None = None,
  29. prefix: str | None = None,
  30. dir: Path | str | None = None,
  31. **kwargs,
  32. ) -> Generator[Path, None, None]:
  33. """
  34. Context manager to create a temporary directory and safely delete it.
  35. If tmp directory cannot be deleted normally, we set the WRITE permission and retry.
  36. If cleanup still fails, we give up but don't raise an exception. This is equivalent
  37. to `tempfile.TemporaryDirectory(..., ignore_cleanup_errors=True)` introduced in
  38. Python 3.10.
  39. See https://www.scivision.dev/python-tempfile-permission-error-windows/.
  40. """
  41. tmpdir = tempfile.TemporaryDirectory(prefix=prefix, suffix=suffix, dir=dir, **kwargs)
  42. yield Path(tmpdir.name).resolve()
  43. try:
  44. # First once with normal cleanup
  45. shutil.rmtree(tmpdir.name)
  46. except Exception:
  47. # If failed, try to set write permission and retry
  48. try:
  49. shutil.rmtree(tmpdir.name, onerror=_set_write_permission_and_retry)
  50. except Exception:
  51. pass
  52. # And finally, cleanup the tmpdir.
  53. # If it fails again, give up but do not throw error
  54. try:
  55. tmpdir.cleanup()
  56. except Exception:
  57. pass
  58. def _set_write_permission_and_retry(func, path, excinfo):
  59. os.chmod(path, stat.S_IWRITE)
  60. func(path)
  61. @contextlib.contextmanager
  62. def WeakFileLock(lock_file: str | Path, *, timeout: float | None = None) -> Generator[BaseFileLock, None, None]:
  63. """A filelock with some custom logic.
  64. This filelock is weaker than the default filelock in that:
  65. 1. It won't raise an exception if release fails.
  66. 2. It will default to a SoftFileLock if the filesystem does not support flock.
  67. 3. Lock files are created with mode 0o664 (group-writable) instead of the default 0o644.
  68. This allows multiple users sharing a cache directory to wait for locks.
  69. An INFO log message is emitted every 10 seconds if the lock is not acquired immediately.
  70. If a timeout is provided, a `filelock.Timeout` exception is raised if the lock is not acquired within the timeout.
  71. """
  72. log_interval = constants.FILELOCK_LOG_EVERY_SECONDS
  73. lock = FileLock(lock_file, timeout=log_interval, mode=0o664)
  74. start_time = time.time()
  75. while True:
  76. elapsed_time = time.time() - start_time
  77. if timeout is not None and elapsed_time >= timeout:
  78. raise Timeout(str(lock_file))
  79. try:
  80. lock.acquire(timeout=min(log_interval, timeout - elapsed_time) if timeout else log_interval)
  81. except Timeout:
  82. logger.info(
  83. f"Still waiting to acquire lock on {lock_file} (elapsed: {time.time() - start_time:.1f} seconds)"
  84. )
  85. except NotImplementedError as e:
  86. if "use SoftFileLock instead" in str(e):
  87. logger.warning(
  88. "FileSystem does not appear to support flock. Falling back to SoftFileLock for %s", lock_file
  89. )
  90. lock = SoftFileLock(lock_file, timeout=log_interval)
  91. continue
  92. else:
  93. break
  94. try:
  95. yield lock
  96. finally:
  97. try:
  98. lock.release()
  99. except OSError:
  100. try:
  101. Path(lock_file).unlink()
  102. except OSError:
  103. pass