common.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. """
  2. """
  3. import warnings
  4. import os
  5. import sys
  6. import posixpath
  7. import fnmatch
  8. import py
  9. # Moved from local.py.
  10. iswin32 = sys.platform == "win32" or (getattr(os, '_name', False) == 'nt')
  11. try:
  12. from os import fspath
  13. except ImportError:
  14. def fspath(path):
  15. """
  16. Return the string representation of the path.
  17. If str or bytes is passed in, it is returned unchanged.
  18. This code comes from PEP 519, modified to support earlier versions of
  19. python.
  20. This is required for python < 3.6.
  21. """
  22. if isinstance(path, (py.builtin.text, py.builtin.bytes)):
  23. return path
  24. # Work from the object's type to match method resolution of other magic
  25. # methods.
  26. path_type = type(path)
  27. try:
  28. return path_type.__fspath__(path)
  29. except AttributeError:
  30. if hasattr(path_type, '__fspath__'):
  31. raise
  32. try:
  33. import pathlib
  34. except ImportError:
  35. pass
  36. else:
  37. if isinstance(path, pathlib.PurePath):
  38. return py.builtin.text(path)
  39. raise TypeError("expected str, bytes or os.PathLike object, not "
  40. + path_type.__name__)
  41. class Checkers:
  42. _depend_on_existence = 'exists', 'link', 'dir', 'file'
  43. def __init__(self, path):
  44. self.path = path
  45. def dir(self):
  46. raise NotImplementedError
  47. def file(self):
  48. raise NotImplementedError
  49. def dotfile(self):
  50. return self.path.basename.startswith('.')
  51. def ext(self, arg):
  52. if not arg.startswith('.'):
  53. arg = '.' + arg
  54. return self.path.ext == arg
  55. def exists(self):
  56. raise NotImplementedError
  57. def basename(self, arg):
  58. return self.path.basename == arg
  59. def basestarts(self, arg):
  60. return self.path.basename.startswith(arg)
  61. def relto(self, arg):
  62. return self.path.relto(arg)
  63. def fnmatch(self, arg):
  64. return self.path.fnmatch(arg)
  65. def endswith(self, arg):
  66. return str(self.path).endswith(arg)
  67. def _evaluate(self, kw):
  68. for name, value in kw.items():
  69. invert = False
  70. meth = None
  71. try:
  72. meth = getattr(self, name)
  73. except AttributeError:
  74. if name[:3] == 'not':
  75. invert = True
  76. try:
  77. meth = getattr(self, name[3:])
  78. except AttributeError:
  79. pass
  80. if meth is None:
  81. raise TypeError(
  82. "no %r checker available for %r" % (name, self.path))
  83. try:
  84. if py.code.getrawcode(meth).co_argcount > 1:
  85. if (not meth(value)) ^ invert:
  86. return False
  87. else:
  88. if bool(value) ^ bool(meth()) ^ invert:
  89. return False
  90. except (py.error.ENOENT, py.error.ENOTDIR, py.error.EBUSY):
  91. # EBUSY feels not entirely correct,
  92. # but its kind of necessary since ENOMEDIUM
  93. # is not accessible in python
  94. for name in self._depend_on_existence:
  95. if name in kw:
  96. if kw.get(name):
  97. return False
  98. name = 'not' + name
  99. if name in kw:
  100. if not kw.get(name):
  101. return False
  102. return True
  103. class NeverRaised(Exception):
  104. pass
  105. class PathBase(object):
  106. """ shared implementation for filesystem path objects."""
  107. Checkers = Checkers
  108. def __div__(self, other):
  109. return self.join(fspath(other))
  110. __truediv__ = __div__ # py3k
  111. def basename(self):
  112. """ basename part of path. """
  113. return self._getbyspec('basename')[0]
  114. basename = property(basename, None, None, basename.__doc__)
  115. def dirname(self):
  116. """ dirname part of path. """
  117. return self._getbyspec('dirname')[0]
  118. dirname = property(dirname, None, None, dirname.__doc__)
  119. def purebasename(self):
  120. """ pure base name of the path."""
  121. return self._getbyspec('purebasename')[0]
  122. purebasename = property(purebasename, None, None, purebasename.__doc__)
  123. def ext(self):
  124. """ extension of the path (including the '.')."""
  125. return self._getbyspec('ext')[0]
  126. ext = property(ext, None, None, ext.__doc__)
  127. def dirpath(self, *args, **kwargs):
  128. """ return the directory path joined with any given path arguments. """
  129. return self.new(basename='').join(*args, **kwargs)
  130. def read_binary(self):
  131. """ read and return a bytestring from reading the path. """
  132. with self.open('rb') as f:
  133. return f.read()
  134. def read_text(self, encoding):
  135. """ read and return a Unicode string from reading the path. """
  136. with self.open("r", encoding=encoding) as f:
  137. return f.read()
  138. def read(self, mode='r'):
  139. """ read and return a bytestring from reading the path. """
  140. with self.open(mode) as f:
  141. return f.read()
  142. def readlines(self, cr=1):
  143. """ read and return a list of lines from the path. if cr is False, the
  144. newline will be removed from the end of each line. """
  145. if sys.version_info < (3, ):
  146. mode = 'rU'
  147. else: # python 3 deprecates mode "U" in favor of "newline" option
  148. mode = 'r'
  149. if not cr:
  150. content = self.read(mode)
  151. return content.split('\n')
  152. else:
  153. f = self.open(mode)
  154. try:
  155. return f.readlines()
  156. finally:
  157. f.close()
  158. def load(self):
  159. """ (deprecated) return object unpickled from self.read() """
  160. f = self.open('rb')
  161. try:
  162. import pickle
  163. return py.error.checked_call(pickle.load, f)
  164. finally:
  165. f.close()
  166. def move(self, target):
  167. """ move this path to target. """
  168. if target.relto(self):
  169. raise py.error.EINVAL(
  170. target,
  171. "cannot move path into a subdirectory of itself")
  172. try:
  173. self.rename(target)
  174. except py.error.EXDEV: # invalid cross-device link
  175. self.copy(target)
  176. self.remove()
  177. def __repr__(self):
  178. """ return a string representation of this path. """
  179. return repr(str(self))
  180. def check(self, **kw):
  181. """ check a path for existence and properties.
  182. Without arguments, return True if the path exists, otherwise False.
  183. valid checkers::
  184. file=1 # is a file
  185. file=0 # is not a file (may not even exist)
  186. dir=1 # is a dir
  187. link=1 # is a link
  188. exists=1 # exists
  189. You can specify multiple checker definitions, for example::
  190. path.check(file=1, link=1) # a link pointing to a file
  191. """
  192. if not kw:
  193. kw = {'exists': 1}
  194. return self.Checkers(self)._evaluate(kw)
  195. def fnmatch(self, pattern):
  196. """return true if the basename/fullname matches the glob-'pattern'.
  197. valid pattern characters::
  198. * matches everything
  199. ? matches any single character
  200. [seq] matches any character in seq
  201. [!seq] matches any char not in seq
  202. If the pattern contains a path-separator then the full path
  203. is used for pattern matching and a '*' is prepended to the
  204. pattern.
  205. if the pattern doesn't contain a path-separator the pattern
  206. is only matched against the basename.
  207. """
  208. return FNMatcher(pattern)(self)
  209. def relto(self, relpath):
  210. """ return a string which is the relative part of the path
  211. to the given 'relpath'.
  212. """
  213. if not isinstance(relpath, (str, PathBase)):
  214. raise TypeError("%r: not a string or path object" %(relpath,))
  215. strrelpath = str(relpath)
  216. if strrelpath and strrelpath[-1] != self.sep:
  217. strrelpath += self.sep
  218. #assert strrelpath[-1] == self.sep
  219. #assert strrelpath[-2] != self.sep
  220. strself = self.strpath
  221. if sys.platform == "win32" or getattr(os, '_name', None) == 'nt':
  222. if os.path.normcase(strself).startswith(
  223. os.path.normcase(strrelpath)):
  224. return strself[len(strrelpath):]
  225. elif strself.startswith(strrelpath):
  226. return strself[len(strrelpath):]
  227. return ""
  228. def ensure_dir(self, *args):
  229. """ ensure the path joined with args is a directory. """
  230. return self.ensure(*args, **{"dir": True})
  231. def bestrelpath(self, dest):
  232. """ return a string which is a relative path from self
  233. (assumed to be a directory) to dest such that
  234. self.join(bestrelpath) == dest and if not such
  235. path can be determined return dest.
  236. """
  237. try:
  238. if self == dest:
  239. return os.curdir
  240. base = self.common(dest)
  241. if not base: # can be the case on windows
  242. return str(dest)
  243. self2base = self.relto(base)
  244. reldest = dest.relto(base)
  245. if self2base:
  246. n = self2base.count(self.sep) + 1
  247. else:
  248. n = 0
  249. l = [os.pardir] * n
  250. if reldest:
  251. l.append(reldest)
  252. target = dest.sep.join(l)
  253. return target
  254. except AttributeError:
  255. return str(dest)
  256. def exists(self):
  257. return self.check()
  258. def isdir(self):
  259. return self.check(dir=1)
  260. def isfile(self):
  261. return self.check(file=1)
  262. def parts(self, reverse=False):
  263. """ return a root-first list of all ancestor directories
  264. plus the path itself.
  265. """
  266. current = self
  267. l = [self]
  268. while 1:
  269. last = current
  270. current = current.dirpath()
  271. if last == current:
  272. break
  273. l.append(current)
  274. if not reverse:
  275. l.reverse()
  276. return l
  277. def common(self, other):
  278. """ return the common part shared with the other path
  279. or None if there is no common part.
  280. """
  281. last = None
  282. for x, y in zip(self.parts(), other.parts()):
  283. if x != y:
  284. return last
  285. last = x
  286. return last
  287. def __add__(self, other):
  288. """ return new path object with 'other' added to the basename"""
  289. return self.new(basename=self.basename+str(other))
  290. def __cmp__(self, other):
  291. """ return sort value (-1, 0, +1). """
  292. try:
  293. return cmp(self.strpath, other.strpath)
  294. except AttributeError:
  295. return cmp(str(self), str(other)) # self.path, other.path)
  296. def __lt__(self, other):
  297. try:
  298. return self.strpath < other.strpath
  299. except AttributeError:
  300. return str(self) < str(other)
  301. def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
  302. """ yields all paths below the current one
  303. fil is a filter (glob pattern or callable), if not matching the
  304. path will not be yielded, defaulting to None (everything is
  305. returned)
  306. rec is a filter (glob pattern or callable) that controls whether
  307. a node is descended, defaulting to None
  308. ignore is an Exception class that is ignoredwhen calling dirlist()
  309. on any of the paths (by default, all exceptions are reported)
  310. bf if True will cause a breadthfirst search instead of the
  311. default depthfirst. Default: False
  312. sort if True will sort entries within each directory level.
  313. """
  314. for x in Visitor(fil, rec, ignore, bf, sort).gen(self):
  315. yield x
  316. def _sortlist(self, res, sort):
  317. if sort:
  318. if hasattr(sort, '__call__'):
  319. warnings.warn(DeprecationWarning(
  320. "listdir(sort=callable) is deprecated and breaks on python3"
  321. ), stacklevel=3)
  322. res.sort(sort)
  323. else:
  324. res.sort()
  325. def samefile(self, other):
  326. """ return True if other refers to the same stat object as self. """
  327. return self.strpath == str(other)
  328. def __fspath__(self):
  329. return self.strpath
  330. class Visitor:
  331. def __init__(self, fil, rec, ignore, bf, sort):
  332. if isinstance(fil, py.builtin._basestring):
  333. fil = FNMatcher(fil)
  334. if isinstance(rec, py.builtin._basestring):
  335. self.rec = FNMatcher(rec)
  336. elif not hasattr(rec, '__call__') and rec:
  337. self.rec = lambda path: True
  338. else:
  339. self.rec = rec
  340. self.fil = fil
  341. self.ignore = ignore
  342. self.breadthfirst = bf
  343. self.optsort = sort and sorted or (lambda x: x)
  344. def gen(self, path):
  345. try:
  346. entries = path.listdir()
  347. except self.ignore:
  348. return
  349. rec = self.rec
  350. dirs = self.optsort([p for p in entries
  351. if p.check(dir=1) and (rec is None or rec(p))])
  352. if not self.breadthfirst:
  353. for subdir in dirs:
  354. for p in self.gen(subdir):
  355. yield p
  356. for p in self.optsort(entries):
  357. if self.fil is None or self.fil(p):
  358. yield p
  359. if self.breadthfirst:
  360. for subdir in dirs:
  361. for p in self.gen(subdir):
  362. yield p
  363. class FNMatcher:
  364. def __init__(self, pattern):
  365. self.pattern = pattern
  366. def __call__(self, path):
  367. pattern = self.pattern
  368. if (pattern.find(path.sep) == -1 and
  369. iswin32 and
  370. pattern.find(posixpath.sep) != -1):
  371. # Running on Windows, the pattern has no Windows path separators,
  372. # and the pattern has one or more Posix path separators. Replace
  373. # the Posix path separators with the Windows path separator.
  374. pattern = pattern.replace(posixpath.sep, path.sep)
  375. if pattern.find(path.sep) == -1:
  376. name = path.basename
  377. else:
  378. name = str(path) # path.strpath # XXX svn?
  379. if not os.path.isabs(pattern):
  380. pattern = '*' + path.sep + pattern
  381. return fnmatch.fnmatch(name, pattern)