• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Licensed under the Apache License, Version 2.0 (the "License");
2# you may not use this file except in compliance with the License.
3# You may obtain a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13"""A fake implementation for pathlib working with FakeFilesystem.
14New in pyfakefs 3.0.
15
16Usage:
17
18* With fake_filesystem_unittest:
19  If using fake_filesystem_unittest.TestCase, pathlib gets replaced
20  by fake_pathlib together with other file system related modules.
21
22* Stand-alone with FakeFilesystem:
23  `filesystem = fake_filesystem.FakeFilesystem()`
24  `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)`
25  `path = fake_pathlib_module.Path('/foo/bar')`
26
27Note: as the implementation is based on FakeFilesystem, all faked classes
28(including PurePosixPath, PosixPath, PureWindowsPath and WindowsPath)
29get the properties of the underlying fake filesystem.
30"""
31import fnmatch
32import os
33import re
34
35try:
36    from urllib.parse import quote_from_bytes as urlquote_from_bytes
37except ImportError:
38    from urllib import quote as urlquote_from_bytes
39
40import sys
41
42import functools
43
44import errno
45
46from pyfakefs import fake_scandir
47from pyfakefs.extra_packages import use_scandir, pathlib, pathlib2
48from pyfakefs.fake_filesystem import FakeFileOpen, FakeFilesystem
49
50
51def init_module(filesystem):
52    """Initializes the fake module with the fake file system."""
53    # pylint: disable=protected-access
54    FakePath.filesystem = filesystem
55    FakePathlibModule.PureWindowsPath._flavour = _FakeWindowsFlavour(
56        filesystem)
57    FakePathlibModule.PurePosixPath._flavour = _FakePosixFlavour(filesystem)
58
59
60def _wrap_strfunc(strfunc):
61    @functools.wraps(strfunc)
62    def _wrapped(pathobj, *args):
63        return strfunc(pathobj.filesystem, str(pathobj), *args)
64
65    return staticmethod(_wrapped)
66
67
68def _wrap_binary_strfunc(strfunc):
69    @functools.wraps(strfunc)
70    def _wrapped(pathobj1, pathobj2, *args):
71        return strfunc(
72            pathobj1.filesystem, str(pathobj1), str(pathobj2), *args)
73
74    return staticmethod(_wrapped)
75
76
77def _wrap_binary_strfunc_reverse(strfunc):
78    @functools.wraps(strfunc)
79    def _wrapped(pathobj1, pathobj2, *args):
80        return strfunc(
81            pathobj2.filesystem, str(pathobj2), str(pathobj1), *args)
82
83    return staticmethod(_wrapped)
84
85
86try:
87    accessor = pathlib._Accessor
88except AttributeError:
89    accessor = object
90
91
92class _FakeAccessor(accessor):
93    """Accessor which forwards some of the functions to FakeFilesystem methods.
94    """
95
96    stat = _wrap_strfunc(FakeFilesystem.stat)
97
98    lstat = _wrap_strfunc(
99        lambda fs, path: FakeFilesystem.stat(fs, path, follow_symlinks=False))
100
101    listdir = _wrap_strfunc(FakeFilesystem.listdir)
102
103    chmod = _wrap_strfunc(FakeFilesystem.chmod)
104
105    if use_scandir:
106        scandir = _wrap_strfunc(fake_scandir.scandir)
107
108    if hasattr(os, "lchmod"):
109        lchmod = _wrap_strfunc(lambda fs, path, mode: FakeFilesystem.chmod(
110            fs, path, mode, follow_symlinks=False))
111    else:
112        def lchmod(self, pathobj, mode):
113            """Raises not implemented for Windows systems."""
114            raise NotImplementedError("lchmod() not available on this system")
115
116    mkdir = _wrap_strfunc(FakeFilesystem.makedir)
117
118    unlink = _wrap_strfunc(FakeFilesystem.remove)
119
120    rmdir = _wrap_strfunc(FakeFilesystem.rmdir)
121
122    rename = _wrap_binary_strfunc(FakeFilesystem.rename)
123
124    replace = _wrap_binary_strfunc(
125        lambda fs, old_path, new_path: FakeFilesystem.rename(
126            fs, old_path, new_path, force_replace=True))
127
128    symlink = _wrap_binary_strfunc_reverse(
129        lambda fs, file_path, link_target, target_is_directory:
130        FakeFilesystem.create_symlink(fs, file_path, link_target,
131                                      create_missing_dirs=False))
132
133    utime = _wrap_strfunc(FakeFilesystem.utime)
134
135
136_fake_accessor = _FakeAccessor()
137
138flavour = pathlib._Flavour if pathlib else object
139
140
141class _FakeFlavour(flavour):
142    """Fake Flavour implementation used by PurePath and _Flavour"""
143
144    filesystem = None
145    sep = '/'
146    altsep = None
147    has_drv = False
148
149    ext_namespace_prefix = '\\\\?\\'
150
151    drive_letters = (
152            set(chr(x) for x in range(ord('a'), ord('z') + 1)) |
153            set(chr(x) for x in range(ord('A'), ord('Z') + 1))
154    )
155
156    def __init__(self, filesystem):
157        self.filesystem = filesystem
158        self.sep = filesystem.path_separator
159        self.altsep = filesystem.alternative_path_separator
160        self.has_drv = filesystem.is_windows_fs
161        super(_FakeFlavour, self).__init__()
162
163    @staticmethod
164    def _split_extended_path(path, ext_prefix=ext_namespace_prefix):
165        prefix = ''
166        if path.startswith(ext_prefix):
167            prefix = path[:4]
168            path = path[4:]
169            if path.startswith('UNC\\'):
170                prefix += path[:3]
171                path = '\\' + path[3:]
172        return prefix, path
173
174    def _splitroot_with_drive(self, path, sep):
175        first = path[0:1]
176        second = path[1:2]
177        if second == sep and first == sep:
178            # extended paths should also disable the collapsing of "."
179            # components (according to MSDN docs).
180            prefix, path = self._split_extended_path(path)
181            first = path[0:1]
182            second = path[1:2]
183        else:
184            prefix = ''
185        third = path[2:3]
186        if second == sep and first == sep and third != sep:
187            # is a UNC path:
188            # vvvvvvvvvvvvvvvvvvvvv root
189            # \\machine\mountpoint\directory\etc\...
190            #            directory ^^^^^^^^^^^^^^
191            index = path.find(sep, 2)
192            if index != -1:
193                index2 = path.find(sep, index + 1)
194                # a UNC path can't have two slashes in a row
195                # (after the initial two)
196                if index2 != index + 1:
197                    if index2 == -1:
198                        index2 = len(path)
199                    if prefix:
200                        return prefix + path[1:index2], sep, path[index2 + 1:]
201                    return path[:index2], sep, path[index2 + 1:]
202        drv = root = ''
203        if second == ':' and first in self.drive_letters:
204            drv = path[:2]
205            path = path[2:]
206            first = third
207        if first == sep:
208            root = first
209            path = path.lstrip(sep)
210        return prefix + drv, root, path
211
212    @staticmethod
213    def _splitroot_posix(path, sep):
214        if path and path[0] == sep:
215            stripped_part = path.lstrip(sep)
216            if len(path) - len(stripped_part) == 2:
217                return '', sep * 2, stripped_part
218            return '', sep, stripped_part
219        else:
220            return '', '', path
221
222    def splitroot(self, path, sep=None):
223        """Split path into drive, root and rest."""
224        if sep is None:
225            sep = self.filesystem.path_separator
226        if self.filesystem.is_windows_fs:
227            return self._splitroot_with_drive(path, sep)
228        return self._splitroot_posix(path, sep)
229
230    def casefold(self, path):
231        """Return the lower-case version of s for a Windows filesystem."""
232        if self.filesystem.is_windows_fs:
233            return path.lower()
234        return path
235
236    def casefold_parts(self, parts):
237        """Return the lower-case version of parts for a Windows filesystem."""
238        if self.filesystem.is_windows_fs:
239            return [p.lower() for p in parts]
240        return parts
241
242    def _resolve_posix(self, path, strict):
243        sep = self.sep
244        seen = {}
245
246        def _resolve(path, rest):
247            if rest.startswith(sep):
248                path = ''
249
250            for name in rest.split(sep):
251                if not name or name == '.':
252                    # current dir
253                    continue
254                if name == '..':
255                    # parent dir
256                    path, _, _ = path.rpartition(sep)
257                    continue
258                newpath = path + sep + name
259                if newpath in seen:
260                    # Already seen this path
261                    path = seen[newpath]
262                    if path is not None:
263                        # use cached value
264                        continue
265                    # The symlink is not resolved, so we must have
266                    # a symlink loop.
267                    raise RuntimeError("Symlink loop from %r" % newpath)
268                # Resolve the symbolic link
269                try:
270                    target = self.filesystem.readlink(newpath)
271                except OSError as e:
272                    if e.errno != errno.EINVAL and strict:
273                        raise
274                    # Not a symlink, or non-strict mode. We just leave the path
275                    # untouched.
276                    path = newpath
277                else:
278                    seen[newpath] = None  # not resolved symlink
279                    path = _resolve(path, target)
280                    seen[newpath] = path  # resolved symlink
281
282            return path
283
284        # NOTE: according to POSIX, getcwd() cannot contain path components
285        # which are symlinks.
286        base = '' if path.is_absolute() else self.filesystem.cwd
287        return _resolve(base, str(path)) or sep
288
289    def _resolve_windows(self, path, strict):
290        path = str(path)
291        if not path:
292            return os.getcwd()
293        previous_s = None
294        if strict:
295            if not self.filesystem.exists(path):
296                self.filesystem.raise_os_error(errno.ENOENT, path)
297            return self.filesystem.resolve_path(path)
298        else:
299            while True:
300                try:
301                    path = self.filesystem.resolve_path(path)
302                except OSError:
303                    previous_s = path
304                    path = self.filesystem.splitpath(path)[0]
305                else:
306                    if previous_s is None:
307                        return path
308                    return self.filesystem.joinpaths(
309                        path, os.path.basename(previous_s))
310
311    def resolve(self, path, strict):
312        """Make the path absolute, resolving any symlinks."""
313        if self.filesystem.is_windows_fs:
314            return self._resolve_windows(path, strict)
315        return self._resolve_posix(path, strict)
316
317    def gethomedir(self, username):
318        """Return the home directory of the current user."""
319        if not username:
320            try:
321                return os.environ['HOME']
322            except KeyError:
323                import pwd
324                return pwd.getpwuid(os.getuid()).pw_dir
325        else:
326            import pwd
327            try:
328                return pwd.getpwnam(username).pw_dir
329            except KeyError:
330                raise RuntimeError("Can't determine home directory "
331                                   "for %r" % username)
332
333
334class _FakeWindowsFlavour(_FakeFlavour):
335    """Flavour used by PureWindowsPath with some Windows specific
336    implementations independent of FakeFilesystem properties.
337    """
338    reserved_names = (
339            {'CON', 'PRN', 'AUX', 'NUL'} |
340            {'COM%d' % i for i in range(1, 10)} |
341            {'LPT%d' % i for i in range(1, 10)}
342    )
343
344    def is_reserved(self, parts):
345        """Return True if the path is considered reserved under Windows."""
346
347        # NOTE: the rules for reserved names seem somewhat complicated
348        # (e.g. r"..\NUL" is reserved but not r"foo\NUL").
349        # We err on the side of caution and return True for paths which are
350        # not considered reserved by Windows.
351        if not parts:
352            return False
353        if self.filesystem.is_windows_fs and parts[0].startswith('\\\\'):
354            # UNC paths are never reserved
355            return False
356        return parts[-1].partition('.')[0].upper() in self.reserved_names
357
358    def make_uri(self, path):
359        """Return a file URI for the given path"""
360
361        # Under Windows, file URIs use the UTF-8 encoding.
362        # original version, not faked
363        drive = path.drive
364        if len(drive) == 2 and drive[1] == ':':
365            # It's a path on a local drive => 'file:///c:/a/b'
366            rest = path.as_posix()[2:].lstrip('/')
367            return 'file:///%s/%s' % (
368                drive, urlquote_from_bytes(rest.encode('utf-8')))
369        else:
370            # It's a path on a network drive => 'file://host/share/a/b'
371            return ('file:' +
372                    urlquote_from_bytes(path.as_posix().encode('utf-8')))
373
374    def gethomedir(self, username):
375        """Return the home directory of the current user."""
376
377        # original version, not faked
378        if 'HOME' in os.environ:
379            userhome = os.environ['HOME']
380        elif 'USERPROFILE' in os.environ:
381            userhome = os.environ['USERPROFILE']
382        elif 'HOMEPATH' in os.environ:
383            try:
384                drv = os.environ['HOMEDRIVE']
385            except KeyError:
386                drv = ''
387            userhome = drv + os.environ['HOMEPATH']
388        else:
389            raise RuntimeError("Can't determine home directory")
390
391        if username:
392            # Try to guess user home directory.  By default all users
393            # directories are located in the same place and are named by
394            # corresponding usernames.  If current user home directory points
395            # to nonstandard place, this guess is likely wrong.
396            if os.environ['USERNAME'] != username:
397                drv, root, parts = self.parse_parts((userhome,))
398                if parts[-1] != os.environ['USERNAME']:
399                    raise RuntimeError("Can't determine home directory "
400                                       "for %r" % username)
401                parts[-1] = username
402                if drv or root:
403                    userhome = drv + root + self.join(parts[1:])
404                else:
405                    userhome = self.join(parts)
406        return userhome
407
408    def compile_pattern(self, pattern):
409        return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
410
411
412class _FakePosixFlavour(_FakeFlavour):
413    """Flavour used by PurePosixPath with some Unix specific implementations
414    independent of FakeFilesystem properties.
415    """
416
417    def is_reserved(self, parts):
418        return False
419
420    def make_uri(self, path):
421        # We represent the path using the local filesystem encoding,
422        # for portability to other applications.
423        bpath = bytes(path)
424        return 'file://' + urlquote_from_bytes(bpath)
425
426    def gethomedir(self, username):
427        # original version, not faked
428        if not username:
429            try:
430                return os.environ['HOME']
431            except KeyError:
432                import pwd
433                return pwd.getpwuid(os.getuid()).pw_dir
434        else:
435            import pwd
436            try:
437                return pwd.getpwnam(username).pw_dir
438            except KeyError:
439                raise RuntimeError("Can't determine home directory "
440                                   "for %r" % username)
441
442    def compile_pattern(self, pattern):
443        return re.compile(fnmatch.translate(pattern)).fullmatch
444
445
446path_module = pathlib.Path if pathlib else object
447
448
449class FakePath(path_module):
450    """Replacement for pathlib.Path. Reimplement some methods to use
451    fake filesystem. The rest of the methods work as they are, as they will
452    use the fake accessor.
453    New in pyfakefs 3.0.
454    """
455
456    # the underlying fake filesystem
457    filesystem = None
458
459    def __new__(cls, *args, **kwargs):
460        """Creates the correct subclass based on OS."""
461        if cls is FakePathlibModule.Path:
462            cls = (FakePathlibModule.WindowsPath if os.name == 'nt'
463                   else FakePathlibModule.PosixPath)
464        self = cls._from_parts(args, init=True)
465        return self
466
467    def _path(self):
468        """Returns the underlying path string as used by the fake filesystem.
469        """
470        return str(self)
471
472    def _init(self, template=None):
473        """Initializer called from base class."""
474        self._accessor = _fake_accessor
475        self._closed = False
476
477    @classmethod
478    def cwd(cls):
479        """Return a new path pointing to the current working directory
480        (as returned by os.getcwd()).
481        """
482        return cls(cls.filesystem.cwd)
483
484    def resolve(self, strict=None):
485        """Make the path absolute, resolving all symlinks on the way and also
486        normalizing it (for example turning slashes into backslashes
487        under Windows).
488
489        Args:
490            strict: If False (default) no exception is raised if the path
491                does not exist.
492                New in Python 3.6.
493
494        Raises:
495            OSError: if the path doesn't exist (strict=True or Python < 3.6)
496        """
497        if sys.version_info >= (3, 6) or pathlib2:
498            if strict is None:
499                strict = False
500        else:
501            if strict is not None:
502                raise TypeError(
503                    "resolve() got an unexpected keyword argument 'strict'")
504            strict = True
505        if self._closed:
506            self._raise_closed()
507        path = self._flavour.resolve(self, strict=strict)
508        if path is None:
509            self.stat()
510            path = str(self.absolute())
511        path = self.filesystem.absnormpath(path)
512        return FakePath(path)
513
514    def open(self, mode='r', buffering=-1, encoding=None,
515             errors=None, newline=None):
516        """Open the file pointed by this path and return a fake file object.
517
518        Raises:
519            OSError: if the target object is a directory, the path is invalid
520                or permission is denied.
521        """
522        if self._closed:
523            self._raise_closed()
524        return FakeFileOpen(self.filesystem)(
525            self._path(), mode, buffering, encoding, errors, newline)
526
527    def read_bytes(self):
528        """Open the fake file in bytes mode, read it, and close the file.
529
530        Raises:
531            OSError: if the target object is a directory, the path is
532                invalid or permission is denied.
533        """
534        with FakeFileOpen(self.filesystem)(self._path(), mode='rb') as f:
535            return f.read()
536
537    def read_text(self, encoding=None, errors=None):
538        """
539        Open the fake file in text mode, read it, and close the file.
540        """
541        with FakeFileOpen(self.filesystem)(self._path(), mode='r',
542                                           encoding=encoding,
543                                           errors=errors) as f:
544            return f.read()
545
546    def write_bytes(self, data):
547        """Open the fake file in bytes mode, write to it, and close the file.
548        Args:
549            data: the bytes to be written
550        Raises:
551            OSError: if the target object is a directory, the path is
552                invalid or permission is denied.
553        """
554        # type-check for the buffer interface before truncating the file
555        view = memoryview(data)
556        with FakeFileOpen(self.filesystem)(self._path(), mode='wb') as f:
557            return f.write(view)
558
559    def write_text(self, data, encoding=None, errors=None):
560        """Open the fake file in text mode, write to it, and close
561        the file.
562
563        Args:
564            data: the string to be written
565            encoding: the encoding used for the string; if not given, the
566                default locale encoding is used
567            errors: ignored
568        Raises:
569            TypeError: if data is not of type 'str'.
570            OSError: if the target object is a directory, the path is
571                invalid or permission is denied.
572        """
573        if not isinstance(data, str):
574            raise TypeError('data must be str, not %s' %
575                            data.__class__.__name__)
576        with FakeFileOpen(self.filesystem)(self._path(),
577                                           mode='w',
578                                           encoding=encoding,
579                                           errors=errors) as f:
580            return f.write(data)
581
582    @classmethod
583    def home(cls):
584        """Return a new path pointing to the user's home directory (as
585        returned by os.path.expanduser('~')).
586        """
587        return cls(cls()._flavour.gethomedir(None).
588                   replace(os.sep, cls.filesystem.path_separator))
589
590    def samefile(self, other_path):
591        """Return whether other_path is the same or not as this file
592        (as returned by os.path.samefile()).
593
594        Args:
595            other_path: A path object or string of the file object
596            to be compared with
597
598        Raises:
599            OSError: if the filesystem object doesn't exist.
600        """
601        st = self.stat()
602        try:
603            other_st = other_path.stat()
604        except AttributeError:
605            other_st = self.filesystem.stat(other_path)
606        return (st.st_ino == other_st.st_ino and
607                st.st_dev == other_st.st_dev)
608
609    def expanduser(self):
610        """ Return a new path with expanded ~ and ~user constructs
611        (as returned by os.path.expanduser)
612        """
613        return FakePath(os.path.expanduser(self._path())
614                        .replace(os.path.sep,
615                                 self.filesystem.path_separator))
616
617    def touch(self, mode=0o666, exist_ok=True):
618        """Create a fake file for the path with the given access mode,
619        if it doesn't exist.
620
621        Args:
622            mode: the file mode for the file if it does not exist
623            exist_ok: if the file already exists and this is True, nothing
624                happens, otherwise FileExistError is raised
625
626        Raises:
627            FileExistsError: if the file exists and exits_ok is False.
628        """
629        if self._closed:
630            self._raise_closed()
631        if self.exists():
632            if exist_ok:
633                self.filesystem.utime(self._path(), times=None)
634            else:
635                self.filesystem.raise_os_error(errno.EEXIST, self._path())
636        else:
637            fake_file = self.open('w')
638            fake_file.close()
639            self.chmod(mode)
640
641
642class FakePathlibModule:
643    """Uses FakeFilesystem to provide a fake pathlib module replacement.
644    Can be used to replace both the standard `pathlib` module and the
645    `pathlib2` package available on PyPi.
646
647    You need a fake_filesystem to use this:
648    `filesystem = fake_filesystem.FakeFilesystem()`
649    `fake_pathlib_module = fake_filesystem.FakePathlibModule(filesystem)`
650    """
651
652    PurePath = pathlib.PurePath if pathlib else object
653
654    def __init__(self, filesystem):
655        """
656        Initializes the module with the given filesystem.
657
658        Args:
659            filesystem: FakeFilesystem used to provide file system information
660        """
661        init_module(filesystem)
662        self._pathlib_module = pathlib
663
664    class PurePosixPath(PurePath):
665        """A subclass of PurePath, that represents non-Windows filesystem
666        paths"""
667        __slots__ = ()
668
669    class PureWindowsPath(PurePath):
670        """A subclass of PurePath, that represents Windows filesystem paths"""
671        __slots__ = ()
672
673    if sys.platform == 'win32':
674        class WindowsPath(FakePath, PureWindowsPath):
675            """A subclass of Path and PureWindowsPath that represents
676            concrete Windows filesystem paths.
677            """
678            __slots__ = ()
679    else:
680        class PosixPath(FakePath, PurePosixPath):
681            """A subclass of Path and PurePosixPath that represents
682            concrete non-Windows filesystem paths.
683            """
684            __slots__ = ()
685
686    Path = FakePath
687
688    def __getattr__(self, name):
689        """Forwards any unfaked calls to the standard pathlib module."""
690        return getattr(self._pathlib_module, name)
691
692
693class FakePathlibPathModule:
694    """Patches `pathlib.Path` by passing all calls to FakePathlibModule."""
695    fake_pathlib = None
696
697    def __init__(self, filesystem):
698        if self.fake_pathlib is None:
699            self.__class__.fake_pathlib = FakePathlibModule(filesystem)
700
701    def __call__(self, *args, **kwargs):
702        return self.fake_pathlib.Path(*args, **kwargs)
703
704    def __getattr__(self, name):
705        return getattr(self.fake_pathlib.Path, name)
706