backports.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. """
  2. Backports of fixes for joblib dependencies
  3. """
  4. import os
  5. import re
  6. import time
  7. from multiprocessing import util
  8. from os.path import basename
  9. class Version:
  10. """Backport from deprecated distutils
  11. We maintain this backport to avoid introducing a new dependency on
  12. `packaging`.
  13. We might rexplore this choice in the future if all major Python projects
  14. introduce a dependency on packaging anyway.
  15. """
  16. def __init__(self, vstring=None):
  17. if vstring:
  18. self.parse(vstring)
  19. def __repr__(self):
  20. return "%s ('%s')" % (self.__class__.__name__, str(self))
  21. def __eq__(self, other):
  22. c = self._cmp(other)
  23. if c is NotImplemented:
  24. return c
  25. return c == 0
  26. def __lt__(self, other):
  27. c = self._cmp(other)
  28. if c is NotImplemented:
  29. return c
  30. return c < 0
  31. def __le__(self, other):
  32. c = self._cmp(other)
  33. if c is NotImplemented:
  34. return c
  35. return c <= 0
  36. def __gt__(self, other):
  37. c = self._cmp(other)
  38. if c is NotImplemented:
  39. return c
  40. return c > 0
  41. def __ge__(self, other):
  42. c = self._cmp(other)
  43. if c is NotImplemented:
  44. return c
  45. return c >= 0
  46. class LooseVersion(Version):
  47. """Backport from deprecated distutils
  48. We maintain this backport to avoid introducing a new dependency on
  49. `packaging`.
  50. We might rexplore this choice in the future if all major Python projects
  51. introduce a dependency on packaging anyway.
  52. """
  53. component_re = re.compile(r"(\d+ | [a-z]+ | \.)", re.VERBOSE)
  54. def __init__(self, vstring=None):
  55. if vstring:
  56. self.parse(vstring)
  57. def parse(self, vstring):
  58. # I've given up on thinking I can reconstruct the version string
  59. # from the parsed tuple -- so I just store the string here for
  60. # use by __str__
  61. self.vstring = vstring
  62. components = [x for x in self.component_re.split(vstring) if x and x != "."]
  63. for i, obj in enumerate(components):
  64. try:
  65. components[i] = int(obj)
  66. except ValueError:
  67. pass
  68. self.version = components
  69. def __str__(self):
  70. return self.vstring
  71. def __repr__(self):
  72. return "LooseVersion ('%s')" % str(self)
  73. def _cmp(self, other):
  74. if isinstance(other, str):
  75. other = LooseVersion(other)
  76. elif not isinstance(other, LooseVersion):
  77. return NotImplemented
  78. if self.version == other.version:
  79. return 0
  80. if self.version < other.version:
  81. return -1
  82. if self.version > other.version:
  83. return 1
  84. try:
  85. import numpy as np
  86. def make_memmap(
  87. filename,
  88. dtype="uint8",
  89. mode="r+",
  90. offset=0,
  91. shape=None,
  92. order="C",
  93. unlink_on_gc_collect=False,
  94. ):
  95. """Custom memmap constructor compatible with numpy.memmap.
  96. This function:
  97. - is a backport the numpy memmap offset fix (See
  98. https://github.com/numpy/numpy/pull/8443 for more details.
  99. The numpy fix is available starting numpy 1.13)
  100. - adds ``unlink_on_gc_collect``, which specifies explicitly whether
  101. the process re-constructing the memmap owns a reference to the
  102. underlying file. If set to True, it adds a finalizer to the
  103. newly-created memmap that sends a maybe_unlink request for the
  104. memmaped file to resource_tracker.
  105. """
  106. util.debug(
  107. "[MEMMAP READ] creating a memmap (shape {}, filename {}, pid {})".format(
  108. shape, basename(filename), os.getpid()
  109. )
  110. )
  111. mm = np.memmap(
  112. filename, dtype=dtype, mode=mode, offset=offset, shape=shape, order=order
  113. )
  114. if LooseVersion(np.__version__) < "1.13":
  115. mm.offset = offset
  116. if unlink_on_gc_collect:
  117. from ._memmapping_reducer import add_maybe_unlink_finalizer
  118. add_maybe_unlink_finalizer(mm)
  119. return mm
  120. except ImportError:
  121. def make_memmap(
  122. filename,
  123. dtype="uint8",
  124. mode="r+",
  125. offset=0,
  126. shape=None,
  127. order="C",
  128. unlink_on_gc_collect=False,
  129. ):
  130. raise NotImplementedError(
  131. "'joblib.backports.make_memmap' should not be used "
  132. "if numpy is not installed."
  133. )
  134. if os.name == "nt":
  135. # https://github.com/joblib/joblib/issues/540
  136. access_denied_errors = (5, 13)
  137. from os import replace
  138. def concurrency_safe_rename(src, dst):
  139. """Renames ``src`` into ``dst`` overwriting ``dst`` if it exists.
  140. On Windows os.replace can yield permission errors if executed by two
  141. different processes.
  142. """
  143. max_sleep_time = 1
  144. total_sleep_time = 0
  145. sleep_time = 0.001
  146. while total_sleep_time < max_sleep_time:
  147. try:
  148. replace(src, dst)
  149. break
  150. except Exception as exc:
  151. if getattr(exc, "winerror", None) in access_denied_errors:
  152. time.sleep(sleep_time)
  153. total_sleep_time += sleep_time
  154. sleep_time *= 2
  155. else:
  156. raise
  157. else:
  158. raise
  159. else:
  160. from os import replace as concurrency_safe_rename # noqa