Skip to content

Fix instantiation of standalone FakePathlibModule #1170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ The released versions correspond to PyPI releases.
(see [#1121](../../issues/1121))
* fixed workaround for recursion with pytest under Windows to ignore capitalization
of pytest executable (see [#1096](../../issues/1096))
* added missing `mode` property to fake file wrapper (see [#1162](../../issues/1096))
* added missing `mode` property to fake file wrapper (see [#1162](../../issues/1162))
* fixed instantiation of a standalone `FakePathlibModule` for Python >= 3.11
(see [#1169](../../issues/1169))

### Infrastructure
* adapt test for increased default buffer size in Python 3.14a6
Expand Down
15 changes: 12 additions & 3 deletions pyfakefs/fake_filesystem_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,17 +775,26 @@ def _init_fake_module_classes(self) -> None:
# it by adding an attribute in fixtures/module_with_attributes.py
# and a test in fake_filesystem_unittest_test.py, class
# TestAttributesWithFakeModuleNames.

# we instantiate the fake pathlib library with `from_patcher` set
# to avoid faking pathlib.os (already faked by the patcher)
def fake_pathlib_module(fs: FakeFilesystem):
return fake_pathlib.FakePathlibModule(fs, from_patcher=True)

def fake_path_module(fs: FakeFilesystem):
return fake_pathlib.FakePathlibPathModule(fs, from_patcher=True)

self._fake_module_classes = {
"os": fake_os.FakeOsModule,
"shutil": fake_filesystem_shutil.FakeShutilModule,
"io": fake_io.FakeIoModule,
"pathlib": fake_pathlib.FakePathlibModule,
"pathlib": fake_pathlib_module,
}
if sys.version_info >= (3, 13):
# for Python 3.13, we need both pathlib (path with __init__.py) and
# pathlib._local (has the actual implementation);
# depending on how pathlib is imported, either may be used
self._fake_module_classes["pathlib._local"] = fake_pathlib.FakePathlibModule
self._fake_module_classes["pathlib._local"] = fake_pathlib_module
if IS_PYPY or sys.version_info >= (3, 12):
# in PyPy and later cpython versions, the module is referenced as _io
self._fake_module_classes["_io"] = fake_io.FakeIoModule2
Expand Down Expand Up @@ -813,7 +822,7 @@ def _init_fake_module_classes(self) -> None:
self._unfaked_module_classes["pathlib2"] = fake_pathlib.RealPathlibModule
if scandir:
self._fake_module_classes["scandir"] = fake_legacy_modules.FakeScanDirModule
self._fake_module_classes["Path"] = fake_pathlib.FakePathlibPathModule
self._fake_module_classes["Path"] = fake_path_module
self._unfaked_module_classes["Path"] = fake_pathlib.RealPathlibPathModule

def _init_fake_module_functions(self) -> None:
Expand Down
38 changes: 32 additions & 6 deletions pyfakefs/fake_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import warnings
from pathlib import PurePath
from typing import Callable, List, Optional
from unittest import mock
from urllib.parse import quote_from_bytes as urlquote_from_bytes

from pyfakefs import fake_scandir
Expand Down Expand Up @@ -554,6 +555,7 @@ def gethomedir(self, username):

def compile_pattern(self, pattern):
return re.compile(fnmatch.translate(pattern)).fullmatch

else: # Python >= 3.12

class FakePosixPathModule(FakePathModule):
Expand Down Expand Up @@ -862,24 +864,48 @@ def _warn_is_reserved_deprecated():
class FakePathlibModule:
"""Uses FakeFilesystem to provide a fake pathlib module replacement.

You need a fake_filesystem to use this::
Automatically created if using `fake_filesystem_unittest.TestCase`,
`fs` fixture, or the `patchfs` decorator.

For creating it separately, a `fake_filesystem` instance is needed::

filesystem = fake_filesystem.FakeFilesystem()
fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)
"""

def __init__(self, filesystem):
def __init__(self, filesystem, from_patcher=False):
"""
Initializes the module with the given filesystem.

Args:
filesystem: FakeFilesystem used to provide file system information
from_patcher: If `False` (the default), `pathlib.os` will be manually
patched using `FakeOsModule`. This allows to instantiate the class
manually for a test.
Will be set to `True` if instantiated from `Patcher`.
"""
init_module(filesystem)
self._pathlib_module = pathlib
self._os = None
self._os_patcher = None
if not from_patcher:
self.patch_os_module()

def patch_os_module(self):
if sys.version_info >= (3, 11) and not isinstance(os, FakeOsModule):
self._os = FakeOsModule(FakePath.filesystem)
pathlib_os = (
"pathlib._local.os" if sys.version_info[:2] == (3, 13) else "pathlib.os"
)
self._os_patcher = mock.patch(pathlib_os, self._os)
self._os_patcher.start()

def __del__(self):
if self._os_patcher is not None:
self._os_patcher.stop()

class PurePosixPath(PurePath):
"""A subclass of PurePath, that represents non-Windows filesystem
"""A subclass of PurePath that represents non-Windows filesystem
paths"""

__slots__ = ()
Expand All @@ -898,7 +924,7 @@ def joinpath(self, *pathsegments):
return super().joinpath(*pathsegments)

class PureWindowsPath(PurePath):
"""A subclass of PurePath, that represents Windows filesystem paths"""
"""A subclass of PurePath that represents Windows filesystem paths"""

__slots__ = ()

Expand Down Expand Up @@ -990,9 +1016,9 @@ class FakePathlibPathModule:

fake_pathlib = None

def __init__(self, filesystem=None):
def __init__(self, filesystem=None, from_patcher=False):
if self.fake_pathlib is None:
self.__class__.fake_pathlib = FakePathlibModule(filesystem)
self.__class__.fake_pathlib = FakePathlibModule(filesystem, from_patcher)

@property
def skip_names(self):
Expand Down
1 change: 1 addition & 0 deletions pyfakefs/tests/fake_filesystem_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2614,6 +2614,7 @@ def test_add_existing_real_paths_read_write(self):

@unittest.skipIf(pytest is None, "pytest is not installed")
@unittest.skipIf(sys.version_info < (3, 8), "importlib.metadata not available")
@unittest.skipIf("pathlib2" in sys.modules, "pathlib2 may break this test")
def test_add_package_metadata(self):
parent_path = pathlib.Path(pytest.__file__).parent.parent
pytest_dist_path = parent_path / f"pytest-{pytest.__version__}.dist-info"
Expand Down
11 changes: 10 additions & 1 deletion pyfakefs/tests/fake_pathlib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
from unittest.mock import patch

from pyfakefs import fake_pathlib, fake_filesystem, fake_filesystem_unittest, fake_os
from pyfakefs.fake_filesystem import OSType
from pyfakefs.fake_filesystem import OSType, FakeFilesystem
from pyfakefs.fake_pathlib import FakePathlibModule
from pyfakefs.helpers import IS_PYPY, is_root
from pyfakefs.tests.skipped_pathlib import (
check_exists_pathlib,
Expand Down Expand Up @@ -1646,5 +1647,13 @@ def test_exists(self):
self.assertTrue(check_exists_pathlib())


class InstantiatedPathlibTest(unittest.TestCase):
def test_instantiated_pathlib(self):
fake_fs = FakeFilesystem()
fake_pathlib_module = FakePathlibModule(fake_fs)
top_level_dirs = list(fake_pathlib_module.Path("/").iterdir())
self.assertEqual(0, len(top_level_dirs))


if __name__ == "__main__":
unittest.main(verbosity=2)
Loading