• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Faked ``os.path`` module replacement. See ``fake_filesystem`` for usage."""
16
17import errno
18import functools
19import inspect
20import os
21import sys
22from stat import (
23    S_IFDIR,
24    S_IFMT,
25)
26from types import ModuleType
27from typing import (
28    Callable,
29    List,
30    Optional,
31    Union,
32    Any,
33    Dict,
34    Tuple,
35    AnyStr,
36    overload,
37    ClassVar,
38    TYPE_CHECKING,
39)
40
41from pyfakefs.helpers import (
42    is_called_from_skipped_module,
43    make_string_path,
44    to_string,
45    matching_string,
46    to_bytes,
47)
48
49if TYPE_CHECKING:
50    from pyfakefs.fake_filesystem import FakeFilesystem
51    from pyfakefs.fake_os import FakeOsModule
52
53
54def _copy_module(old: ModuleType) -> ModuleType:
55    """Recompiles and creates new module object."""
56    saved = sys.modules.pop(old.__name__, None)
57    new = __import__(old.__name__)
58    if saved is not None:
59        sys.modules[old.__name__] = saved
60    return new
61
62
63class FakePathModule:
64    """Faked os.path module replacement.
65
66    FakePathModule should *only* be instantiated by FakeOsModule.  See the
67    FakeOsModule docstring for details.
68    """
69
70    _OS_PATH_COPY: Any = _copy_module(os.path)
71
72    devnull: ClassVar[str] = ""
73    sep: ClassVar[str] = ""
74    altsep: ClassVar[Optional[str]] = None
75    linesep: ClassVar[str] = ""
76    pathsep: ClassVar[str] = ""
77
78    @staticmethod
79    def dir() -> List[str]:
80        """Return the list of patched function names. Used for patching
81        functions imported from the module.
82        """
83        dir_list = [
84            "abspath",
85            "dirname",
86            "exists",
87            "expanduser",
88            "getatime",
89            "getctime",
90            "getmtime",
91            "getsize",
92            "isabs",
93            "isdir",
94            "isfile",
95            "islink",
96            "ismount",
97            "join",
98            "lexists",
99            "normcase",
100            "normpath",
101            "realpath",
102            "relpath",
103            "split",
104            "splitdrive",
105            "samefile",
106        ]
107        if sys.version_info >= (3, 12):
108            dir_list += ["isjunction", "splitroot"]
109        return dir_list
110
111    def __init__(self, filesystem: "FakeFilesystem", os_module: "FakeOsModule"):
112        """Init.
113
114        Args:
115            filesystem: FakeFilesystem used to provide file system information
116        """
117        self.filesystem = filesystem
118        self._os_path = self._OS_PATH_COPY
119        self._os_path.os = self.os = os_module  # type: ignore[attr-defined]
120        self.reset(filesystem)
121
122    @classmethod
123    def reset(cls, filesystem: "FakeFilesystem") -> None:
124        cls.sep = filesystem.path_separator
125        cls.altsep = filesystem.alternative_path_separator
126        cls.linesep = filesystem.line_separator
127        cls.devnull = filesystem.devnull
128        cls.pathsep = filesystem.pathsep
129
130    def exists(self, path: AnyStr) -> bool:
131        """Determine whether the file object exists within the fake filesystem.
132
133        Args:
134            path: The path to the file object.
135
136        Returns:
137            (bool) `True` if the file exists.
138        """
139        return self.filesystem.exists(path)
140
141    def lexists(self, path: AnyStr) -> bool:
142        """Test whether a path exists.  Returns True for broken symbolic links.
143
144        Args:
145          path:  path to the symlink object.
146
147        Returns:
148          bool (if file exists).
149        """
150        return self.filesystem.exists(path, check_link=True)
151
152    def getsize(self, path: AnyStr):
153        """Return the file object size in bytes.
154
155        Args:
156          path:  path to the file object.
157
158        Returns:
159          file size in bytes.
160        """
161        file_obj = self.filesystem.resolve(path)
162        if (
163            self.filesystem.ends_with_path_separator(path)
164            and S_IFMT(file_obj.st_mode) != S_IFDIR
165        ):
166            error_nr = errno.EINVAL if self.filesystem.is_windows_fs else errno.ENOTDIR
167            self.filesystem.raise_os_error(error_nr, path)
168        return file_obj.st_size
169
170    def isabs(self, path: AnyStr) -> bool:
171        """Return True if path is an absolute pathname."""
172        empty = matching_string(path, "")
173        if self.filesystem.is_windows_fs:
174            drive, path = self.splitdrive(path)
175        else:
176            drive = empty
177        path = make_string_path(path)
178        if not self.filesystem.starts_with_sep(path):
179            return False
180        if self.filesystem.is_windows_fs and sys.version_info >= (3, 13):
181            # from Python 3.13 on, a path under Windows starting with a single separator
182            # (e.g. not a drive and not an UNC path) is no more considered absolute
183            return drive != empty
184        return True
185
186    def isdir(self, path: AnyStr) -> bool:
187        """Determine if path identifies a directory."""
188        return self.filesystem.isdir(path)
189
190    def isfile(self, path: AnyStr) -> bool:
191        """Determine if path identifies a regular file."""
192        return self.filesystem.isfile(path)
193
194    def islink(self, path: AnyStr) -> bool:
195        """Determine if path identifies a symbolic link.
196
197        Args:
198            path: Path to filesystem object.
199
200        Returns:
201            `True` if path points to a symbolic link.
202
203        Raises:
204            TypeError: if path is None.
205        """
206        return self.filesystem.islink(path)
207
208    if sys.version_info >= (3, 12):
209
210        def isjunction(self, path: AnyStr) -> bool:
211            """Returns False. Junctions are never faked."""
212            return self.filesystem.isjunction(path)
213
214        def splitroot(self, path: AnyStr):
215            """Split a pathname into drive, root and tail.
216            Implementation taken from ntpath and posixpath.
217            """
218            return self.filesystem.splitroot(path)
219
220    if sys.version_info >= (3, 13):
221
222        def isreserved(self, path):
223            if not self.filesystem.is_windows_fs:
224                raise AttributeError("module 'os' has no attribute 'isreserved'")
225
226            return self.filesystem.isreserved(path)
227
228    def getmtime(self, path: AnyStr) -> float:
229        """Returns the modification time of the fake file.
230
231        Args:
232            path: the path to fake file.
233
234        Returns:
235            (int, float) the modification time of the fake file
236                         in number of seconds since the epoch.
237
238        Raises:
239            OSError: if the file does not exist.
240        """
241        try:
242            file_obj = self.filesystem.resolve(path)
243            return file_obj.st_mtime
244        except OSError:
245            self.filesystem.raise_os_error(
246                errno.ENOENT, winerror=3
247            )  # pytype: disable=bad-return-type
248
249    def getatime(self, path: AnyStr) -> float:
250        """Returns the last access time of the fake file.
251
252        Note: Access time is not set automatically in fake filesystem
253            on access.
254
255        Args:
256            path: the path to fake file.
257
258        Returns:
259            (int, float) the access time of the fake file in number of seconds
260                since the epoch.
261
262        Raises:
263            OSError: if the file does not exist.
264        """
265        try:
266            file_obj = self.filesystem.resolve(path)
267        except OSError:
268            self.filesystem.raise_os_error(errno.ENOENT)
269        return file_obj.st_atime  # pytype: disable=name-error
270
271    def getctime(self, path: AnyStr) -> float:
272        """Returns the creation time of the fake file.
273
274        Args:
275            path: the path to fake file.
276
277        Returns:
278            (int, float) the creation time of the fake file in number of
279                seconds since the epoch.
280
281        Raises:
282            OSError: if the file does not exist.
283        """
284        try:
285            file_obj = self.filesystem.resolve(path)
286        except OSError:
287            self.filesystem.raise_os_error(errno.ENOENT)
288        return file_obj.st_ctime  # pytype: disable=name-error
289
290    def abspath(self, path: AnyStr) -> AnyStr:
291        """Return the absolute version of a path."""
292
293        def getcwd():
294            """Return the current working directory."""
295            # pylint: disable=undefined-variable
296            if isinstance(path, bytes):
297                return self.os.getcwdb()
298            else:
299                return self.os.getcwd()
300
301        path = make_string_path(path)
302        if not self.isabs(path):
303            path = self.join(getcwd(), path)
304        elif self.filesystem.is_windows_fs and self.filesystem.starts_with_sep(path):
305            cwd = getcwd()
306            if self.filesystem.starts_with_drive_letter(cwd):
307                path = self.join(cwd[:2], path)
308        return self.normpath(path)
309
310    def join(self, *p: AnyStr) -> AnyStr:
311        """Return the completed path with a separator of the parts."""
312        return self.filesystem.joinpaths(*p)
313
314    def split(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
315        """Split the path into the directory and the filename of the path."""
316        return self.filesystem.splitpath(path)
317
318    def splitdrive(self, path: AnyStr) -> Tuple[AnyStr, AnyStr]:
319        """Split the path into the drive part and the rest of the path, if
320        supported."""
321        return self.filesystem.splitdrive(path)
322
323    def normpath(self, path: AnyStr) -> AnyStr:
324        """Normalize path, eliminating double slashes, etc."""
325        return self.filesystem.normpath(path)
326
327    def normcase(self, path: AnyStr) -> AnyStr:
328        """Convert to lower case under windows, replaces additional path
329        separator."""
330        path = self.filesystem.normcase(path)
331        if self.filesystem.is_windows_fs:
332            path = path.lower()
333        return path
334
335    def relpath(self, path: AnyStr, start: Optional[AnyStr] = None) -> AnyStr:
336        """We mostly rely on the native implementation and adapt the
337        path separator."""
338        if not path:
339            raise ValueError("no path specified")
340        path = make_string_path(path)
341        path = self.filesystem.replace_windows_root(path)
342        sep = matching_string(path, self.filesystem.path_separator)
343        if start is not None:
344            start = make_string_path(start)
345        else:
346            start = matching_string(path, self.filesystem.cwd)
347        start = self.filesystem.replace_windows_root(start)
348        system_sep = matching_string(path, self._os_path.sep)
349        if self.filesystem.alternative_path_separator is not None:
350            altsep = matching_string(path, self.filesystem.alternative_path_separator)
351            path = path.replace(altsep, system_sep)
352            start = start.replace(altsep, system_sep)
353        path = path.replace(sep, system_sep)
354        start = start.replace(sep, system_sep)
355        path = self._os_path.relpath(path, start)
356        return path.replace(system_sep, sep)
357
358    def realpath(self, filename: AnyStr, strict: Optional[bool] = None) -> AnyStr:
359        """Return the canonical path of the specified filename, eliminating any
360        symbolic links encountered in the path.
361        """
362        if strict is not None and sys.version_info < (3, 10):
363            raise TypeError("realpath() got an unexpected keyword argument 'strict'")
364        if strict:
365            # raises in strict mode if the file does not exist
366            self.filesystem.resolve(filename)
367        if self.filesystem.is_windows_fs:
368            return self.abspath(filename)
369        filename = make_string_path(filename)
370        path, ok = self._join_real_path(filename[:0], filename, {})
371        path = self.abspath(path)
372        return path
373
374    def samefile(self, path1: AnyStr, path2: AnyStr) -> bool:
375        """Return whether path1 and path2 point to the same file.
376
377        Args:
378            path1: first file path or path object
379            path2: second file path or path object
380
381        Raises:
382            OSError: if one of the paths does not point to an existing
383                file system object.
384        """
385        stat1 = self.filesystem.stat(path1)
386        stat2 = self.filesystem.stat(path2)
387        return stat1.st_ino == stat2.st_ino and stat1.st_dev == stat2.st_dev
388
389    @overload
390    def _join_real_path(
391        self, path: str, rest: str, seen: Dict[str, Optional[str]]
392    ) -> Tuple[str, bool]: ...
393
394    @overload
395    def _join_real_path(
396        self, path: bytes, rest: bytes, seen: Dict[bytes, Optional[bytes]]
397    ) -> Tuple[bytes, bool]: ...
398
399    def _join_real_path(
400        self, path: AnyStr, rest: AnyStr, seen: Dict[AnyStr, Optional[AnyStr]]
401    ) -> Tuple[AnyStr, bool]:
402        """Join two paths, normalizing and eliminating any symbolic links
403        encountered in the second path.
404        Taken from Python source and adapted.
405        """
406        curdir = matching_string(path, ".")
407        pardir = matching_string(path, "..")
408
409        sep = self.filesystem.get_path_separator(path)
410        if self.isabs(rest):
411            rest = rest[1:]
412            path = sep
413
414        while rest:
415            name, _, rest = rest.partition(sep)
416            if not name or name == curdir:
417                # current dir
418                continue
419            if name == pardir:
420                # parent dir
421                if path:
422                    path, name = self.filesystem.splitpath(path)
423                    if name == pardir:
424                        path = self.filesystem.joinpaths(path, pardir, pardir)
425                else:
426                    path = pardir
427                continue
428            newpath = self.filesystem.joinpaths(path, name)
429            if not self.filesystem.islink(newpath):
430                path = newpath
431                continue
432            # Resolve the symbolic link
433            if newpath in seen:
434                # Already seen this path
435                seen_path = seen[newpath]
436                if seen_path is not None:
437                    # use cached value
438                    path = seen_path
439                    continue
440                # The symlink is not resolved, so we must have a symlink loop.
441                # Return already resolved part + rest of the path unchanged.
442                return self.filesystem.joinpaths(newpath, rest), False
443            seen[newpath] = None  # not resolved symlink
444            path, ok = self._join_real_path(
445                path,
446                matching_string(path, self.filesystem.readlink(newpath)),
447                seen,
448            )
449            if not ok:
450                return self.filesystem.joinpaths(path, rest), False
451            seen[newpath] = path  # resolved symlink
452        return path, True
453
454    def dirname(self, path: AnyStr) -> AnyStr:
455        """Returns the first part of the result of `split()`."""
456        return self.split(path)[0]
457
458    def expanduser(self, path: AnyStr) -> AnyStr:
459        """Return the argument with an initial component of ~ or ~user
460        replaced by that user's home directory.
461        """
462        path = self._os_path.expanduser(path)
463        return path.replace(
464            matching_string(path, self._os_path.sep),
465            matching_string(path, self.sep),
466        )
467
468    def ismount(self, path: AnyStr) -> bool:
469        """Return true if the given path is a mount point.
470
471        Args:
472            path: Path to filesystem object to be checked
473
474        Returns:
475            `True` if path is a mount point added to the fake file system.
476            Under Windows also returns True for drive and UNC roots
477            (independent of their existence).
478        """
479        if not path:
480            return False
481        path_str = to_string(make_string_path(path))
482        normed_path = self.filesystem.absnormpath(path_str)
483        sep = self.filesystem.path_separator
484        if self.filesystem.is_windows_fs:
485            path_seps: Union[Tuple[str, Optional[str]], Tuple[str]]
486            if self.filesystem.alternative_path_separator is not None:
487                path_seps = (sep, self.filesystem.alternative_path_separator)
488            else:
489                path_seps = (sep,)
490            drive, rest = self.filesystem.splitdrive(normed_path)
491            if drive and drive[:1] in path_seps:
492                return (not rest) or (rest in path_seps)
493            if rest in path_seps:
494                return True
495        for mount_point in self.filesystem.mount_points:
496            if to_string(normed_path).rstrip(sep) == to_string(mount_point).rstrip(sep):
497                return True
498        return False
499
500    def __getattr__(self, name: str) -> Any:
501        """Forwards any non-faked calls to the real os.path."""
502        return getattr(self._os_path, name)
503
504
505if sys.platform == "win32":
506
507    class FakeNtModule:
508        """Under windows, a few function of `os.path` are taken from the `nt` module
509        for performance reasons. These are patched here.
510        """
511
512        @staticmethod
513        def dir():
514            if sys.version_info >= (3, 12):
515                return ["_path_exists", "_path_isfile", "_path_isdir", "_path_islink"]
516            else:
517                return ["_isdir"]
518
519        def __init__(self, filesystem: "FakeFilesystem"):
520            """Init.
521
522            Args:
523                filesystem: FakeFilesystem used to provide file system information
524            """
525            import nt  # type:ignore[import]
526
527            self.filesystem = filesystem
528            self.nt_module: Any = nt
529
530        def getcwd(self) -> str:
531            """Return current working directory."""
532            return to_string(self.filesystem.cwd)
533
534        def getcwdb(self) -> bytes:
535            """Return current working directory as bytes."""
536            return to_bytes(self.filesystem.cwd)
537
538        if sys.version_info >= (3, 12):
539
540            def _path_isdir(self, path: AnyStr) -> bool:
541                return self.filesystem.isdir(path)
542
543            def _path_isfile(self, path: AnyStr) -> bool:
544                return self.filesystem.isfile(path)
545
546            def _path_islink(self, path: AnyStr) -> bool:
547                return self.filesystem.islink(path)
548
549            def _path_exists(self, path: AnyStr) -> bool:
550                return self.filesystem.exists(path)
551
552        else:
553
554            def _isdir(self, path: AnyStr) -> bool:
555                return self.filesystem.isdir(path)
556
557        def __getattr__(self, name: str) -> Any:
558            """Forwards any non-faked calls to the real nt module."""
559            return getattr(self.nt_module, name)
560
561
562def handle_original_call(f: Callable) -> Callable:
563    """Decorator used for real pathlib Path methods to ensure that
564    real os functions instead of faked ones are used.
565    Applied to all non-private methods of `FakePathModule`."""
566
567    @functools.wraps(f)
568    def wrapped(*args, **kwargs):
569        if args:
570            self = args[0]
571            should_use_original = self.os.use_original
572            if not should_use_original and self.filesystem.patcher:
573                skip_names = self.filesystem.patcher.skip_names
574                if is_called_from_skipped_module(
575                    skip_names=skip_names,
576                    case_sensitive=self.filesystem.is_case_sensitive,
577                ):
578                    should_use_original = True
579
580            if should_use_original:
581                # remove the `self` argument for FakePathModule methods
582                if args and isinstance(args[0], FakePathModule):
583                    args = args[1:]
584                return getattr(os.path, f.__name__)(*args, **kwargs)
585
586        return f(*args, **kwargs)
587
588    return wrapped
589
590
591for name, fn in inspect.getmembers(FakePathModule, inspect.isfunction):
592    if not fn.__name__.startswith("_"):
593        setattr(FakePathModule, name, handle_original_call(fn))
594