utils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. """Notebook related utilities"""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import print_function
  5. import ctypes
  6. import errno
  7. import os
  8. import stat
  9. import sys
  10. from distutils.version import LooseVersion
  11. try:
  12. from inspect import isawaitable
  13. except ImportError:
  14. def isawaitable(f):
  15. """If isawaitable is undefined, nothing is awaitable"""
  16. return False
  17. try:
  18. from concurrent.futures import Future as ConcurrentFuture
  19. except ImportError:
  20. class ConcurrentFuture:
  21. """If concurrent.futures isn't importable, nothing will be a c.f.Future"""
  22. pass
  23. try:
  24. from urllib.parse import quote, unquote, urlparse, urljoin
  25. from urllib.request import pathname2url
  26. except ImportError:
  27. from urllib import quote, unquote, pathname2url
  28. from urlparse import urlparse, urljoin
  29. # tornado.concurrent.Future is asyncio.Future
  30. # in tornado >=5 with Python 3
  31. from tornado.concurrent import Future as TornadoFuture
  32. from tornado import gen
  33. from ipython_genutils import py3compat
  34. # UF_HIDDEN is a stat flag not defined in the stat module.
  35. # It is used by BSD to indicate hidden files.
  36. UF_HIDDEN = getattr(stat, 'UF_HIDDEN', 32768)
  37. def exists(path):
  38. """Replacement for `os.path.exists` which works for host mapped volumes
  39. on Windows containers
  40. """
  41. try:
  42. os.lstat(path)
  43. except OSError:
  44. return False
  45. return True
  46. def url_path_join(*pieces):
  47. """Join components of url into a relative url
  48. Use to prevent double slash when joining subpath. This will leave the
  49. initial and final / in place
  50. """
  51. initial = pieces[0].startswith('/')
  52. final = pieces[-1].endswith('/')
  53. stripped = [s.strip('/') for s in pieces]
  54. result = '/'.join(s for s in stripped if s)
  55. if initial: result = '/' + result
  56. if final: result = result + '/'
  57. if result == '//': result = '/'
  58. return result
  59. def url_is_absolute(url):
  60. """Determine whether a given URL is absolute"""
  61. return urlparse(url).path.startswith("/")
  62. def path2url(path):
  63. """Convert a local file path to a URL"""
  64. pieces = [ quote(p) for p in path.split(os.sep) ]
  65. # preserve trailing /
  66. if pieces[-1] == '':
  67. pieces[-1] = '/'
  68. url = url_path_join(*pieces)
  69. return url
  70. def url2path(url):
  71. """Convert a URL to a local file path"""
  72. pieces = [ unquote(p) for p in url.split('/') ]
  73. path = os.path.join(*pieces)
  74. return path
  75. def url_escape(path):
  76. """Escape special characters in a URL path
  77. Turns '/foo bar/' into '/foo%20bar/'
  78. """
  79. parts = py3compat.unicode_to_str(path, encoding='utf8').split('/')
  80. return u'/'.join([quote(p) for p in parts])
  81. def url_unescape(path):
  82. """Unescape special characters in a URL path
  83. Turns '/foo%20bar/' into '/foo bar/'
  84. """
  85. return u'/'.join([
  86. py3compat.str_to_unicode(unquote(p), encoding='utf8')
  87. for p in py3compat.unicode_to_str(path, encoding='utf8').split('/')
  88. ])
  89. def is_file_hidden_win(abs_path, stat_res=None):
  90. """Is a file hidden?
  91. This only checks the file itself; it should be called in combination with
  92. checking the directory containing the file.
  93. Use is_hidden() instead to check the file and its parent directories.
  94. Parameters
  95. ----------
  96. abs_path : unicode
  97. The absolute path to check.
  98. stat_res : os.stat_result, optional
  99. Ignored on Windows, exists for compatibility with POSIX version of the
  100. function.
  101. """
  102. if os.path.basename(abs_path).startswith('.'):
  103. return True
  104. win32_FILE_ATTRIBUTE_HIDDEN = 0x02
  105. try:
  106. attrs = ctypes.windll.kernel32.GetFileAttributesW(
  107. py3compat.cast_unicode(abs_path)
  108. )
  109. except AttributeError:
  110. pass
  111. else:
  112. if attrs > 0 and attrs & win32_FILE_ATTRIBUTE_HIDDEN:
  113. return True
  114. return False
  115. def is_file_hidden_posix(abs_path, stat_res=None):
  116. """Is a file hidden?
  117. This only checks the file itself; it should be called in combination with
  118. checking the directory containing the file.
  119. Use is_hidden() instead to check the file and its parent directories.
  120. Parameters
  121. ----------
  122. abs_path : unicode
  123. The absolute path to check.
  124. stat_res : os.stat_result, optional
  125. The result of calling stat() on abs_path. If not passed, this function
  126. will call stat() internally.
  127. """
  128. if os.path.basename(abs_path).startswith('.'):
  129. return True
  130. if stat_res is None or stat.S_ISLNK(stat_res.st_mode):
  131. try:
  132. stat_res = os.stat(abs_path)
  133. except OSError as e:
  134. if e.errno == errno.ENOENT:
  135. return False
  136. raise
  137. # check that dirs can be listed
  138. if stat.S_ISDIR(stat_res.st_mode):
  139. # use x-access, not actual listing, in case of slow/large listings
  140. if not os.access(abs_path, os.X_OK | os.R_OK):
  141. return True
  142. # check UF_HIDDEN
  143. if getattr(stat_res, 'st_flags', 0) & UF_HIDDEN:
  144. return True
  145. return False
  146. if sys.platform == 'win32':
  147. is_file_hidden = is_file_hidden_win
  148. else:
  149. is_file_hidden = is_file_hidden_posix
  150. def is_hidden(abs_path, abs_root=''):
  151. """Is a file hidden or contained in a hidden directory?
  152. This will start with the rightmost path element and work backwards to the
  153. given root to see if a path is hidden or in a hidden directory. Hidden is
  154. determined by either name starting with '.' or the UF_HIDDEN flag as
  155. reported by stat.
  156. If abs_path is the same directory as abs_root, it will be visible even if
  157. that is a hidden folder. This only checks the visibility of files
  158. and directories *within* abs_root.
  159. Parameters
  160. ----------
  161. abs_path : unicode
  162. The absolute path to check for hidden directories.
  163. abs_root : unicode
  164. The absolute path of the root directory in which hidden directories
  165. should be checked for.
  166. """
  167. if os.path.normpath(abs_path) == os.path.normpath(abs_root):
  168. return False
  169. if is_file_hidden(abs_path):
  170. return True
  171. if not abs_root:
  172. abs_root = abs_path.split(os.sep, 1)[0] + os.sep
  173. inside_root = abs_path[len(abs_root):]
  174. if any(part.startswith('.') for part in inside_root.split(os.sep)):
  175. return True
  176. # check UF_HIDDEN on any location up to root.
  177. # is_file_hidden() already checked the file, so start from its parent dir
  178. path = os.path.dirname(abs_path)
  179. while path and path.startswith(abs_root) and path != abs_root:
  180. if not exists(path):
  181. path = os.path.dirname(path)
  182. continue
  183. try:
  184. # may fail on Windows junctions
  185. st = os.lstat(path)
  186. except OSError:
  187. return True
  188. if getattr(st, 'st_flags', 0) & UF_HIDDEN:
  189. return True
  190. path = os.path.dirname(path)
  191. return False
  192. def samefile_simple(path, other_path):
  193. """
  194. Fill in for os.path.samefile when it is unavailable (Windows+py2).
  195. Do a case-insensitive string comparison in this case
  196. plus comparing the full stat result (including times)
  197. because Windows + py2 doesn't support the stat fields
  198. needed for identifying if it's the same file (st_ino, st_dev).
  199. Only to be used if os.path.samefile is not available.
  200. Parameters
  201. -----------
  202. path: String representing a path to a file
  203. other_path: String representing a path to another file
  204. Returns
  205. -----------
  206. same: Boolean that is True if both path and other path are the same
  207. """
  208. path_stat = os.stat(path)
  209. other_path_stat = os.stat(other_path)
  210. return (path.lower() == other_path.lower()
  211. and path_stat == other_path_stat)
  212. def to_os_path(path, root=''):
  213. """Convert an API path to a filesystem path
  214. If given, root will be prepended to the path.
  215. root must be a filesystem path already.
  216. """
  217. parts = path.strip('/').split('/')
  218. parts = [p for p in parts if p != ''] # remove duplicate splits
  219. path = os.path.join(root, *parts)
  220. return path
  221. def to_api_path(os_path, root=''):
  222. """Convert a filesystem path to an API path
  223. If given, root will be removed from the path.
  224. root must be a filesystem path already.
  225. """
  226. if os_path.startswith(root):
  227. os_path = os_path[len(root):]
  228. parts = os_path.strip(os.path.sep).split(os.path.sep)
  229. parts = [p for p in parts if p != ''] # remove duplicate splits
  230. path = '/'.join(parts)
  231. return path
  232. def check_version(v, check):
  233. """check version string v >= check
  234. If dev/prerelease tags result in TypeError for string-number comparison,
  235. it is assumed that the dependency is satisfied.
  236. Users on dev branches are responsible for keeping their own packages up to date.
  237. """
  238. try:
  239. return LooseVersion(v) >= LooseVersion(check)
  240. except TypeError:
  241. return True
  242. # Copy of IPython.utils.process.check_pid:
  243. def _check_pid_win32(pid):
  244. import ctypes
  245. # OpenProcess returns 0 if no such process (of ours) exists
  246. # positive int otherwise
  247. return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
  248. def _check_pid_posix(pid):
  249. """Copy of IPython.utils.process.check_pid"""
  250. try:
  251. os.kill(pid, 0)
  252. except OSError as err:
  253. if err.errno == errno.ESRCH:
  254. return False
  255. elif err.errno == errno.EPERM:
  256. # Don't have permission to signal the process - probably means it exists
  257. return True
  258. raise
  259. else:
  260. return True
  261. if sys.platform == 'win32':
  262. check_pid = _check_pid_win32
  263. else:
  264. check_pid = _check_pid_posix
  265. def maybe_future(obj):
  266. """Like tornado's gen.maybe_future
  267. but more compatible with asyncio for recent versions
  268. of tornado
  269. """
  270. if isinstance(obj, TornadoFuture):
  271. return obj
  272. elif isawaitable(obj):
  273. return asyncio.ensure_future(obj)
  274. elif isinstance(obj, ConcurrentFuture):
  275. return asyncio.wrap_future(obj)
  276. else:
  277. # not awaitable, wrap scalar in future
  278. f = TornadoFuture()
  279. f.set_result(obj)
  280. return f
  281. # monkeypatch tornado gen.maybe_future
  282. # on Python 3
  283. # TODO: remove monkeypatch after backporting smaller fix to 5.x
  284. try:
  285. import asyncio
  286. except ImportError:
  287. pass
  288. else:
  289. import tornado.gen
  290. tornado.gen.maybe_future = maybe_future