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