1""" 2local path implementation. 3""" 4from __future__ import with_statement 5 6from contextlib import contextmanager 7import sys, os, re, atexit, io, uuid 8import py 9from py._path import common 10from py._path.common import iswin32, fspath 11from stat import S_ISLNK, S_ISDIR, S_ISREG 12 13from os.path import abspath, normcase, normpath, isabs, exists, isdir, isfile, islink, dirname 14 15if sys.version_info > (3,0): 16 def map_as_list(func, iter): 17 return list(map(func, iter)) 18else: 19 map_as_list = map 20 21class Stat(object): 22 def __getattr__(self, name): 23 return getattr(self._osstatresult, "st_" + name) 24 25 def __init__(self, path, osstatresult): 26 self.path = path 27 self._osstatresult = osstatresult 28 29 @property 30 def owner(self): 31 if iswin32: 32 raise NotImplementedError("XXX win32") 33 import pwd 34 entry = py.error.checked_call(pwd.getpwuid, self.uid) 35 return entry[0] 36 37 @property 38 def group(self): 39 """ return group name of file. """ 40 if iswin32: 41 raise NotImplementedError("XXX win32") 42 import grp 43 entry = py.error.checked_call(grp.getgrgid, self.gid) 44 return entry[0] 45 46 def isdir(self): 47 return S_ISDIR(self._osstatresult.st_mode) 48 49 def isfile(self): 50 return S_ISREG(self._osstatresult.st_mode) 51 52 def islink(self): 53 st = self.path.lstat() 54 return S_ISLNK(self._osstatresult.st_mode) 55 56class PosixPath(common.PathBase): 57 def chown(self, user, group, rec=0): 58 """ change ownership to the given user and group. 59 user and group may be specified by a number or 60 by a name. if rec is True change ownership 61 recursively. 62 """ 63 uid = getuserid(user) 64 gid = getgroupid(group) 65 if rec: 66 for x in self.visit(rec=lambda x: x.check(link=0)): 67 if x.check(link=0): 68 py.error.checked_call(os.chown, str(x), uid, gid) 69 py.error.checked_call(os.chown, str(self), uid, gid) 70 71 def readlink(self): 72 """ return value of a symbolic link. """ 73 return py.error.checked_call(os.readlink, self.strpath) 74 75 def mklinkto(self, oldname): 76 """ posix style hard link to another name. """ 77 py.error.checked_call(os.link, str(oldname), str(self)) 78 79 def mksymlinkto(self, value, absolute=1): 80 """ create a symbolic link with the given value (pointing to another name). """ 81 if absolute: 82 py.error.checked_call(os.symlink, str(value), self.strpath) 83 else: 84 base = self.common(value) 85 # with posix local paths '/' is always a common base 86 relsource = self.__class__(value).relto(base) 87 reldest = self.relto(base) 88 n = reldest.count(self.sep) 89 target = self.sep.join(('..', )*n + (relsource, )) 90 py.error.checked_call(os.symlink, target, self.strpath) 91 92def getuserid(user): 93 import pwd 94 if not isinstance(user, int): 95 user = pwd.getpwnam(user)[2] 96 return user 97 98def getgroupid(group): 99 import grp 100 if not isinstance(group, int): 101 group = grp.getgrnam(group)[2] 102 return group 103 104FSBase = not iswin32 and PosixPath or common.PathBase 105 106class LocalPath(FSBase): 107 """ object oriented interface to os.path and other local filesystem 108 related information. 109 """ 110 class ImportMismatchError(ImportError): 111 """ raised on pyimport() if there is a mismatch of __file__'s""" 112 113 sep = os.sep 114 class Checkers(common.Checkers): 115 def _stat(self): 116 try: 117 return self._statcache 118 except AttributeError: 119 try: 120 self._statcache = self.path.stat() 121 except py.error.ELOOP: 122 self._statcache = self.path.lstat() 123 return self._statcache 124 125 def dir(self): 126 return S_ISDIR(self._stat().mode) 127 128 def file(self): 129 return S_ISREG(self._stat().mode) 130 131 def exists(self): 132 return self._stat() 133 134 def link(self): 135 st = self.path.lstat() 136 return S_ISLNK(st.mode) 137 138 def __init__(self, path=None, expanduser=False): 139 """ Initialize and return a local Path instance. 140 141 Path can be relative to the current directory. 142 If path is None it defaults to the current working directory. 143 If expanduser is True, tilde-expansion is performed. 144 Note that Path instances always carry an absolute path. 145 Note also that passing in a local path object will simply return 146 the exact same path object. Use new() to get a new copy. 147 """ 148 if path is None: 149 self.strpath = py.error.checked_call(os.getcwd) 150 else: 151 try: 152 path = fspath(path) 153 except TypeError: 154 raise ValueError("can only pass None, Path instances " 155 "or non-empty strings to LocalPath") 156 if expanduser: 157 path = os.path.expanduser(path) 158 self.strpath = abspath(path) 159 160 def __hash__(self): 161 return hash(self.strpath) 162 163 def __eq__(self, other): 164 s1 = fspath(self) 165 try: 166 s2 = fspath(other) 167 except TypeError: 168 return False 169 if iswin32: 170 s1 = s1.lower() 171 try: 172 s2 = s2.lower() 173 except AttributeError: 174 return False 175 return s1 == s2 176 177 def __ne__(self, other): 178 return not (self == other) 179 180 def __lt__(self, other): 181 return fspath(self) < fspath(other) 182 183 def __gt__(self, other): 184 return fspath(self) > fspath(other) 185 186 def samefile(self, other): 187 """ return True if 'other' references the same file as 'self'. 188 """ 189 other = fspath(other) 190 if not isabs(other): 191 other = abspath(other) 192 if self == other: 193 return True 194 if iswin32: 195 return False # there is no samefile 196 return py.error.checked_call( 197 os.path.samefile, self.strpath, other) 198 199 def remove(self, rec=1, ignore_errors=False): 200 """ remove a file or directory (or a directory tree if rec=1). 201 if ignore_errors is True, errors while removing directories will 202 be ignored. 203 """ 204 if self.check(dir=1, link=0): 205 if rec: 206 # force remove of readonly files on windows 207 if iswin32: 208 self.chmod(0o700, rec=1) 209 import shutil 210 py.error.checked_call( 211 shutil.rmtree, self.strpath, 212 ignore_errors=ignore_errors) 213 else: 214 py.error.checked_call(os.rmdir, self.strpath) 215 else: 216 if iswin32: 217 self.chmod(0o700) 218 py.error.checked_call(os.remove, self.strpath) 219 220 def computehash(self, hashtype="md5", chunksize=524288): 221 """ return hexdigest of hashvalue for this file. """ 222 try: 223 try: 224 import hashlib as mod 225 except ImportError: 226 if hashtype == "sha1": 227 hashtype = "sha" 228 mod = __import__(hashtype) 229 hash = getattr(mod, hashtype)() 230 except (AttributeError, ImportError): 231 raise ValueError("Don't know how to compute %r hash" %(hashtype,)) 232 f = self.open('rb') 233 try: 234 while 1: 235 buf = f.read(chunksize) 236 if not buf: 237 return hash.hexdigest() 238 hash.update(buf) 239 finally: 240 f.close() 241 242 def new(self, **kw): 243 """ create a modified version of this path. 244 the following keyword arguments modify various path parts:: 245 246 a:/some/path/to/a/file.ext 247 xx drive 248 xxxxxxxxxxxxxxxxx dirname 249 xxxxxxxx basename 250 xxxx purebasename 251 xxx ext 252 """ 253 obj = object.__new__(self.__class__) 254 if not kw: 255 obj.strpath = self.strpath 256 return obj 257 drive, dirname, basename, purebasename,ext = self._getbyspec( 258 "drive,dirname,basename,purebasename,ext") 259 if 'basename' in kw: 260 if 'purebasename' in kw or 'ext' in kw: 261 raise ValueError("invalid specification %r" % kw) 262 else: 263 pb = kw.setdefault('purebasename', purebasename) 264 try: 265 ext = kw['ext'] 266 except KeyError: 267 pass 268 else: 269 if ext and not ext.startswith('.'): 270 ext = '.' + ext 271 kw['basename'] = pb + ext 272 273 if ('dirname' in kw and not kw['dirname']): 274 kw['dirname'] = drive 275 else: 276 kw.setdefault('dirname', dirname) 277 kw.setdefault('sep', self.sep) 278 obj.strpath = normpath( 279 "%(dirname)s%(sep)s%(basename)s" % kw) 280 return obj 281 282 def _getbyspec(self, spec): 283 """ see new for what 'spec' can be. """ 284 res = [] 285 parts = self.strpath.split(self.sep) 286 287 args = filter(None, spec.split(',') ) 288 append = res.append 289 for name in args: 290 if name == 'drive': 291 append(parts[0]) 292 elif name == 'dirname': 293 append(self.sep.join(parts[:-1])) 294 else: 295 basename = parts[-1] 296 if name == 'basename': 297 append(basename) 298 else: 299 i = basename.rfind('.') 300 if i == -1: 301 purebasename, ext = basename, '' 302 else: 303 purebasename, ext = basename[:i], basename[i:] 304 if name == 'purebasename': 305 append(purebasename) 306 elif name == 'ext': 307 append(ext) 308 else: 309 raise ValueError("invalid part specification %r" % name) 310 return res 311 312 def dirpath(self, *args, **kwargs): 313 """ return the directory path joined with any given path arguments. """ 314 if not kwargs: 315 path = object.__new__(self.__class__) 316 path.strpath = dirname(self.strpath) 317 if args: 318 path = path.join(*args) 319 return path 320 return super(LocalPath, self).dirpath(*args, **kwargs) 321 322 def join(self, *args, **kwargs): 323 """ return a new path by appending all 'args' as path 324 components. if abs=1 is used restart from root if any 325 of the args is an absolute path. 326 """ 327 sep = self.sep 328 strargs = [fspath(arg) for arg in args] 329 strpath = self.strpath 330 if kwargs.get('abs'): 331 newargs = [] 332 for arg in reversed(strargs): 333 if isabs(arg): 334 strpath = arg 335 strargs = newargs 336 break 337 newargs.insert(0, arg) 338 # special case for when we have e.g. strpath == "/" 339 actual_sep = "" if strpath.endswith(sep) else sep 340 for arg in strargs: 341 arg = arg.strip(sep) 342 if iswin32: 343 # allow unix style paths even on windows. 344 arg = arg.strip('/') 345 arg = arg.replace('/', sep) 346 strpath = strpath + actual_sep + arg 347 actual_sep = sep 348 obj = object.__new__(self.__class__) 349 obj.strpath = normpath(strpath) 350 return obj 351 352 def open(self, mode='r', ensure=False, encoding=None): 353 """ return an opened file with the given mode. 354 355 If ensure is True, create parent directories if needed. 356 """ 357 if ensure: 358 self.dirpath().ensure(dir=1) 359 if encoding: 360 return py.error.checked_call(io.open, self.strpath, mode, encoding=encoding) 361 return py.error.checked_call(open, self.strpath, mode) 362 363 def _fastjoin(self, name): 364 child = object.__new__(self.__class__) 365 child.strpath = self.strpath + self.sep + name 366 return child 367 368 def islink(self): 369 return islink(self.strpath) 370 371 def check(self, **kw): 372 if not kw: 373 return exists(self.strpath) 374 if len(kw) == 1: 375 if "dir" in kw: 376 return not kw["dir"] ^ isdir(self.strpath) 377 if "file" in kw: 378 return not kw["file"] ^ isfile(self.strpath) 379 return super(LocalPath, self).check(**kw) 380 381 _patternchars = set("*?[" + os.path.sep) 382 def listdir(self, fil=None, sort=None): 383 """ list directory contents, possibly filter by the given fil func 384 and possibly sorted. 385 """ 386 if fil is None and sort is None: 387 names = py.error.checked_call(os.listdir, self.strpath) 388 return map_as_list(self._fastjoin, names) 389 if isinstance(fil, py.builtin._basestring): 390 if not self._patternchars.intersection(fil): 391 child = self._fastjoin(fil) 392 if exists(child.strpath): 393 return [child] 394 return [] 395 fil = common.FNMatcher(fil) 396 names = py.error.checked_call(os.listdir, self.strpath) 397 res = [] 398 for name in names: 399 child = self._fastjoin(name) 400 if fil is None or fil(child): 401 res.append(child) 402 self._sortlist(res, sort) 403 return res 404 405 def size(self): 406 """ return size of the underlying file object """ 407 return self.stat().size 408 409 def mtime(self): 410 """ return last modification time of the path. """ 411 return self.stat().mtime 412 413 def copy(self, target, mode=False, stat=False): 414 """ copy path to target. 415 416 If mode is True, will copy copy permission from path to target. 417 If stat is True, copy permission, last modification 418 time, last access time, and flags from path to target. 419 """ 420 if self.check(file=1): 421 if target.check(dir=1): 422 target = target.join(self.basename) 423 assert self!=target 424 copychunked(self, target) 425 if mode: 426 copymode(self.strpath, target.strpath) 427 if stat: 428 copystat(self, target) 429 else: 430 def rec(p): 431 return p.check(link=0) 432 for x in self.visit(rec=rec): 433 relpath = x.relto(self) 434 newx = target.join(relpath) 435 newx.dirpath().ensure(dir=1) 436 if x.check(link=1): 437 newx.mksymlinkto(x.readlink()) 438 continue 439 elif x.check(file=1): 440 copychunked(x, newx) 441 elif x.check(dir=1): 442 newx.ensure(dir=1) 443 if mode: 444 copymode(x.strpath, newx.strpath) 445 if stat: 446 copystat(x, newx) 447 448 def rename(self, target): 449 """ rename this path to target. """ 450 target = fspath(target) 451 return py.error.checked_call(os.rename, self.strpath, target) 452 453 def dump(self, obj, bin=1): 454 """ pickle object into path location""" 455 f = self.open('wb') 456 import pickle 457 try: 458 py.error.checked_call(pickle.dump, obj, f, bin) 459 finally: 460 f.close() 461 462 def mkdir(self, *args): 463 """ create & return the directory joined with args. """ 464 p = self.join(*args) 465 py.error.checked_call(os.mkdir, fspath(p)) 466 return p 467 468 def write_binary(self, data, ensure=False): 469 """ write binary data into path. If ensure is True create 470 missing parent directories. 471 """ 472 if ensure: 473 self.dirpath().ensure(dir=1) 474 with self.open('wb') as f: 475 f.write(data) 476 477 def write_text(self, data, encoding, ensure=False): 478 """ write text data into path using the specified encoding. 479 If ensure is True create missing parent directories. 480 """ 481 if ensure: 482 self.dirpath().ensure(dir=1) 483 with self.open('w', encoding=encoding) as f: 484 f.write(data) 485 486 def write(self, data, mode='w', ensure=False): 487 """ write data into path. If ensure is True create 488 missing parent directories. 489 """ 490 if ensure: 491 self.dirpath().ensure(dir=1) 492 if 'b' in mode: 493 if not py.builtin._isbytes(data): 494 raise ValueError("can only process bytes") 495 else: 496 if not py.builtin._istext(data): 497 if not py.builtin._isbytes(data): 498 data = str(data) 499 else: 500 data = py.builtin._totext(data, sys.getdefaultencoding()) 501 f = self.open(mode) 502 try: 503 f.write(data) 504 finally: 505 f.close() 506 507 def _ensuredirs(self): 508 parent = self.dirpath() 509 if parent == self: 510 return self 511 if parent.check(dir=0): 512 parent._ensuredirs() 513 if self.check(dir=0): 514 try: 515 self.mkdir() 516 except py.error.EEXIST: 517 # race condition: file/dir created by another thread/process. 518 # complain if it is not a dir 519 if self.check(dir=0): 520 raise 521 return self 522 523 def ensure(self, *args, **kwargs): 524 """ ensure that an args-joined path exists (by default as 525 a file). if you specify a keyword argument 'dir=True' 526 then the path is forced to be a directory path. 527 """ 528 p = self.join(*args) 529 if kwargs.get('dir', 0): 530 return p._ensuredirs() 531 else: 532 p.dirpath()._ensuredirs() 533 if not p.check(file=1): 534 p.open('w').close() 535 return p 536 537 def stat(self, raising=True): 538 """ Return an os.stat() tuple. """ 539 if raising == True: 540 return Stat(self, py.error.checked_call(os.stat, self.strpath)) 541 try: 542 return Stat(self, os.stat(self.strpath)) 543 except KeyboardInterrupt: 544 raise 545 except Exception: 546 return None 547 548 def lstat(self): 549 """ Return an os.lstat() tuple. """ 550 return Stat(self, py.error.checked_call(os.lstat, self.strpath)) 551 552 def setmtime(self, mtime=None): 553 """ set modification time for the given path. if 'mtime' is None 554 (the default) then the file's mtime is set to current time. 555 556 Note that the resolution for 'mtime' is platform dependent. 557 """ 558 if mtime is None: 559 return py.error.checked_call(os.utime, self.strpath, mtime) 560 try: 561 return py.error.checked_call(os.utime, self.strpath, (-1, mtime)) 562 except py.error.EINVAL: 563 return py.error.checked_call(os.utime, self.strpath, (self.atime(), mtime)) 564 565 def chdir(self): 566 """ change directory to self and return old current directory """ 567 try: 568 old = self.__class__() 569 except py.error.ENOENT: 570 old = None 571 py.error.checked_call(os.chdir, self.strpath) 572 return old 573 574 575 @contextmanager 576 def as_cwd(self): 577 """ return context manager which changes to current dir during the 578 managed "with" context. On __enter__ it returns the old dir. 579 """ 580 old = self.chdir() 581 try: 582 yield old 583 finally: 584 old.chdir() 585 586 def realpath(self): 587 """ return a new path which contains no symbolic links.""" 588 return self.__class__(os.path.realpath(self.strpath)) 589 590 def atime(self): 591 """ return last access time of the path. """ 592 return self.stat().atime 593 594 def __repr__(self): 595 return 'local(%r)' % self.strpath 596 597 def __str__(self): 598 """ return string representation of the Path. """ 599 return self.strpath 600 601 def chmod(self, mode, rec=0): 602 """ change permissions to the given mode. If mode is an 603 integer it directly encodes the os-specific modes. 604 if rec is True perform recursively. 605 """ 606 if not isinstance(mode, int): 607 raise TypeError("mode %r must be an integer" % (mode,)) 608 if rec: 609 for x in self.visit(rec=rec): 610 py.error.checked_call(os.chmod, str(x), mode) 611 py.error.checked_call(os.chmod, self.strpath, mode) 612 613 def pypkgpath(self): 614 """ return the Python package path by looking for the last 615 directory upwards which still contains an __init__.py. 616 Return None if a pkgpath can not be determined. 617 """ 618 pkgpath = None 619 for parent in self.parts(reverse=True): 620 if parent.isdir(): 621 if not parent.join('__init__.py').exists(): 622 break 623 if not isimportable(parent.basename): 624 break 625 pkgpath = parent 626 return pkgpath 627 628 def _ensuresyspath(self, ensuremode, path): 629 if ensuremode: 630 s = str(path) 631 if ensuremode == "append": 632 if s not in sys.path: 633 sys.path.append(s) 634 else: 635 if s != sys.path[0]: 636 sys.path.insert(0, s) 637 638 def pyimport(self, modname=None, ensuresyspath=True): 639 """ return path as an imported python module. 640 641 If modname is None, look for the containing package 642 and construct an according module name. 643 The module will be put/looked up in sys.modules. 644 if ensuresyspath is True then the root dir for importing 645 the file (taking __init__.py files into account) will 646 be prepended to sys.path if it isn't there already. 647 If ensuresyspath=="append" the root dir will be appended 648 if it isn't already contained in sys.path. 649 if ensuresyspath is False no modification of syspath happens. 650 """ 651 if not self.check(): 652 raise py.error.ENOENT(self) 653 654 pkgpath = None 655 if modname is None: 656 pkgpath = self.pypkgpath() 657 if pkgpath is not None: 658 pkgroot = pkgpath.dirpath() 659 names = self.new(ext="").relto(pkgroot).split(self.sep) 660 if names[-1] == "__init__": 661 names.pop() 662 modname = ".".join(names) 663 else: 664 pkgroot = self.dirpath() 665 modname = self.purebasename 666 667 self._ensuresyspath(ensuresyspath, pkgroot) 668 __import__(modname) 669 mod = sys.modules[modname] 670 if self.basename == "__init__.py": 671 return mod # we don't check anything as we might 672 # we in a namespace package ... too icky to check 673 modfile = mod.__file__ 674 if modfile[-4:] in ('.pyc', '.pyo'): 675 modfile = modfile[:-1] 676 elif modfile.endswith('$py.class'): 677 modfile = modfile[:-9] + '.py' 678 if modfile.endswith(os.path.sep + "__init__.py"): 679 if self.basename != "__init__.py": 680 modfile = modfile[:-12] 681 try: 682 issame = self.samefile(modfile) 683 except py.error.ENOENT: 684 issame = False 685 if not issame: 686 raise self.ImportMismatchError(modname, modfile, self) 687 return mod 688 else: 689 try: 690 return sys.modules[modname] 691 except KeyError: 692 # we have a custom modname, do a pseudo-import 693 import types 694 mod = types.ModuleType(modname) 695 mod.__file__ = str(self) 696 sys.modules[modname] = mod 697 try: 698 py.builtin.execfile(str(self), mod.__dict__) 699 except: 700 del sys.modules[modname] 701 raise 702 return mod 703 704 def sysexec(self, *argv, **popen_opts): 705 """ return stdout text from executing a system child process, 706 where the 'self' path points to executable. 707 The process is directly invoked and not through a system shell. 708 """ 709 from subprocess import Popen, PIPE 710 argv = map_as_list(str, argv) 711 popen_opts['stdout'] = popen_opts['stderr'] = PIPE 712 proc = Popen([str(self)] + argv, **popen_opts) 713 stdout, stderr = proc.communicate() 714 ret = proc.wait() 715 if py.builtin._isbytes(stdout): 716 stdout = py.builtin._totext(stdout, sys.getdefaultencoding()) 717 if ret != 0: 718 if py.builtin._isbytes(stderr): 719 stderr = py.builtin._totext(stderr, sys.getdefaultencoding()) 720 raise py.process.cmdexec.Error(ret, ret, str(self), 721 stdout, stderr,) 722 return stdout 723 724 def sysfind(cls, name, checker=None, paths=None): 725 """ return a path object found by looking at the systems 726 underlying PATH specification. If the checker is not None 727 it will be invoked to filter matching paths. If a binary 728 cannot be found, None is returned 729 Note: This is probably not working on plain win32 systems 730 but may work on cygwin. 731 """ 732 if isabs(name): 733 p = py.path.local(name) 734 if p.check(file=1): 735 return p 736 else: 737 if paths is None: 738 if iswin32: 739 paths = os.environ['Path'].split(';') 740 if '' not in paths and '.' not in paths: 741 paths.append('.') 742 try: 743 systemroot = os.environ['SYSTEMROOT'] 744 except KeyError: 745 pass 746 else: 747 paths = [path.replace('%SystemRoot%', systemroot) 748 for path in paths] 749 else: 750 paths = os.environ['PATH'].split(':') 751 tryadd = [] 752 if iswin32: 753 tryadd += os.environ['PATHEXT'].split(os.pathsep) 754 tryadd.append("") 755 756 for x in paths: 757 for addext in tryadd: 758 p = py.path.local(x).join(name, abs=True) + addext 759 try: 760 if p.check(file=1): 761 if checker: 762 if not checker(p): 763 continue 764 return p 765 except py.error.EACCES: 766 pass 767 return None 768 sysfind = classmethod(sysfind) 769 770 def _gethomedir(cls): 771 try: 772 x = os.environ['HOME'] 773 except KeyError: 774 try: 775 x = os.environ["HOMEDRIVE"] + os.environ['HOMEPATH'] 776 except KeyError: 777 return None 778 return cls(x) 779 _gethomedir = classmethod(_gethomedir) 780 781 # """ 782 # special class constructors for local filesystem paths 783 # """ 784 @classmethod 785 def get_temproot(cls): 786 """ return the system's temporary directory 787 (where tempfiles are usually created in) 788 """ 789 import tempfile 790 return py.path.local(tempfile.gettempdir()) 791 792 @classmethod 793 def mkdtemp(cls, rootdir=None): 794 """ return a Path object pointing to a fresh new temporary directory 795 (which we created ourself). 796 """ 797 import tempfile 798 if rootdir is None: 799 rootdir = cls.get_temproot() 800 return cls(py.error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) 801 802 def make_numbered_dir(cls, prefix='session-', rootdir=None, keep=3, 803 lock_timeout = 172800): # two days 804 """ return unique directory with a number greater than the current 805 maximum one. The number is assumed to start directly after prefix. 806 if keep is true directories with a number less than (maxnum-keep) 807 will be removed. If .lock files are used (lock_timeout non-zero), 808 algorithm is multi-process safe. 809 """ 810 if rootdir is None: 811 rootdir = cls.get_temproot() 812 813 nprefix = normcase(prefix) 814 def parse_num(path): 815 """ parse the number out of a path (if it matches the prefix) """ 816 nbasename = normcase(path.basename) 817 if nbasename.startswith(nprefix): 818 try: 819 return int(nbasename[len(nprefix):]) 820 except ValueError: 821 pass 822 823 def create_lockfile(path): 824 """ exclusively create lockfile. Throws when failed """ 825 mypid = os.getpid() 826 lockfile = path.join('.lock') 827 if hasattr(lockfile, 'mksymlinkto'): 828 lockfile.mksymlinkto(str(mypid)) 829 else: 830 fd = py.error.checked_call(os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) 831 with os.fdopen(fd, 'w') as f: 832 f.write(str(mypid)) 833 return lockfile 834 835 def atexit_remove_lockfile(lockfile): 836 """ ensure lockfile is removed at process exit """ 837 mypid = os.getpid() 838 def try_remove_lockfile(): 839 # in a fork() situation, only the last process should 840 # remove the .lock, otherwise the other processes run the 841 # risk of seeing their temporary dir disappear. For now 842 # we remove the .lock in the parent only (i.e. we assume 843 # that the children finish before the parent). 844 if os.getpid() != mypid: 845 return 846 try: 847 lockfile.remove() 848 except py.error.Error: 849 pass 850 atexit.register(try_remove_lockfile) 851 852 # compute the maximum number currently in use with the prefix 853 lastmax = None 854 while True: 855 maxnum = -1 856 for path in rootdir.listdir(): 857 num = parse_num(path) 858 if num is not None: 859 maxnum = max(maxnum, num) 860 861 # make the new directory 862 try: 863 udir = rootdir.mkdir(prefix + str(maxnum+1)) 864 if lock_timeout: 865 lockfile = create_lockfile(udir) 866 atexit_remove_lockfile(lockfile) 867 except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): 868 # race condition (1): another thread/process created the dir 869 # in the meantime - try again 870 # race condition (2): another thread/process spuriously acquired 871 # lock treating empty directory as candidate 872 # for removal - try again 873 # race condition (3): another thread/process tried to create the lock at 874 # the same time (happened in Python 3.3 on Windows) 875 # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa 876 if lastmax == maxnum: 877 raise 878 lastmax = maxnum 879 continue 880 break 881 882 def get_mtime(path): 883 """ read file modification time """ 884 try: 885 return path.lstat().mtime 886 except py.error.Error: 887 pass 888 889 garbage_prefix = prefix + 'garbage-' 890 891 def is_garbage(path): 892 """ check if path denotes directory scheduled for removal """ 893 bn = path.basename 894 return bn.startswith(garbage_prefix) 895 896 # prune old directories 897 udir_time = get_mtime(udir) 898 if keep and udir_time: 899 for path in rootdir.listdir(): 900 num = parse_num(path) 901 if num is not None and num <= (maxnum - keep): 902 try: 903 # try acquiring lock to remove directory as exclusive user 904 if lock_timeout: 905 create_lockfile(path) 906 except (py.error.EEXIST, py.error.ENOENT, py.error.EBUSY): 907 path_time = get_mtime(path) 908 if not path_time: 909 # assume directory doesn't exist now 910 continue 911 if abs(udir_time - path_time) < lock_timeout: 912 # assume directory with lockfile exists 913 # and lock timeout hasn't expired yet 914 continue 915 916 # path dir locked for exclusive use 917 # and scheduled for removal to avoid another thread/process 918 # treating it as a new directory or removal candidate 919 garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4())) 920 try: 921 path.rename(garbage_path) 922 garbage_path.remove(rec=1) 923 except KeyboardInterrupt: 924 raise 925 except: # this might be py.error.Error, WindowsError ... 926 pass 927 if is_garbage(path): 928 try: 929 path.remove(rec=1) 930 except KeyboardInterrupt: 931 raise 932 except: # this might be py.error.Error, WindowsError ... 933 pass 934 935 # make link... 936 try: 937 username = os.environ['USER'] #linux, et al 938 except KeyError: 939 try: 940 username = os.environ['USERNAME'] #windows 941 except KeyError: 942 username = 'current' 943 944 src = str(udir) 945 dest = src[:src.rfind('-')] + '-' + username 946 try: 947 os.unlink(dest) 948 except OSError: 949 pass 950 try: 951 os.symlink(src, dest) 952 except (OSError, AttributeError, NotImplementedError): 953 pass 954 955 return udir 956 make_numbered_dir = classmethod(make_numbered_dir) 957 958 959def copymode(src, dest): 960 """ copy permission from src to dst. """ 961 import shutil 962 shutil.copymode(src, dest) 963 964 965def copystat(src, dest): 966 """ copy permission, last modification time, 967 last access time, and flags from src to dst.""" 968 import shutil 969 shutil.copystat(str(src), str(dest)) 970 971 972def copychunked(src, dest): 973 chunksize = 524288 # half a meg of bytes 974 fsrc = src.open('rb') 975 try: 976 fdest = dest.open('wb') 977 try: 978 while 1: 979 buf = fsrc.read(chunksize) 980 if not buf: 981 break 982 fdest.write(buf) 983 finally: 984 fdest.close() 985 finally: 986 fsrc.close() 987 988 989def isimportable(name): 990 if name and (name[0].isalpha() or name[0] == '_'): 991 name = name.replace("_", '') 992 return not name or name.isalnum() 993