Skip to content

Commit e747739

Browse files
committed
Fix instantiation of standalone FakePathlibModule
- needs a patched os in Python >= 3.11
1 parent a69adc4 commit e747739

File tree

5 files changed

+58
-11
lines changed

5 files changed

+58
-11
lines changed

CHANGES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ The released versions correspond to PyPI releases.
2727
(see [#1121](../../issues/1121))
2828
* fixed workaround for recursion with pytest under Windows to ignore capitalization
2929
of pytest executable (see [#1096](../../issues/1096))
30-
* added missing `mode` property to fake file wrapper (see [#1162](../../issues/1096))
30+
* added missing `mode` property to fake file wrapper (see [#1162](../../issues/1162))
31+
* fixed instantiation of a standalone `FakePathlibModule` for Python >= 3.11
32+
(see [#1169](../../issues/1169))
3133

3234
### Infrastructure
3335
* adapt test for increased default buffer size in Python 3.14a6

pyfakefs/fake_filesystem_unittest.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -775,17 +775,26 @@ def _init_fake_module_classes(self) -> None:
775775
# it by adding an attribute in fixtures/module_with_attributes.py
776776
# and a test in fake_filesystem_unittest_test.py, class
777777
# TestAttributesWithFakeModuleNames.
778+
779+
# we instantiate the fake pathlib library with `from_patcher` set
780+
# to avoid faking pathlib.os (already faked by the patcher)
781+
def fake_pathlib_module(fs: FakeFilesystem):
782+
return fake_pathlib.FakePathlibModule(fs, from_patcher=True)
783+
784+
def fake_path_module(fs: FakeFilesystem):
785+
return fake_pathlib.FakePathlibPathModule(fs, from_patcher=True)
786+
778787
self._fake_module_classes = {
779788
"os": fake_os.FakeOsModule,
780789
"shutil": fake_filesystem_shutil.FakeShutilModule,
781790
"io": fake_io.FakeIoModule,
782-
"pathlib": fake_pathlib.FakePathlibModule,
791+
"pathlib": fake_pathlib_module,
783792
}
784793
if sys.version_info >= (3, 13):
785794
# for Python 3.13, we need both pathlib (path with __init__.py) and
786795
# pathlib._local (has the actual implementation);
787796
# depending on how pathlib is imported, either may be used
788-
self._fake_module_classes["pathlib._local"] = fake_pathlib.FakePathlibModule
797+
self._fake_module_classes["pathlib._local"] = fake_pathlib_module
789798
if IS_PYPY or sys.version_info >= (3, 12):
790799
# in PyPy and later cpython versions, the module is referenced as _io
791800
self._fake_module_classes["_io"] = fake_io.FakeIoModule2
@@ -813,7 +822,7 @@ def _init_fake_module_classes(self) -> None:
813822
self._unfaked_module_classes["pathlib2"] = fake_pathlib.RealPathlibModule
814823
if scandir:
815824
self._fake_module_classes["scandir"] = fake_legacy_modules.FakeScanDirModule
816-
self._fake_module_classes["Path"] = fake_pathlib.FakePathlibPathModule
825+
self._fake_module_classes["Path"] = fake_path_module
817826
self._unfaked_module_classes["Path"] = fake_pathlib.RealPathlibPathModule
818827

819828
def _init_fake_module_functions(self) -> None:

pyfakefs/fake_pathlib.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import warnings
4343
from pathlib import PurePath
4444
from typing import Callable, List, Optional
45+
from unittest import mock
4546
from urllib.parse import quote_from_bytes as urlquote_from_bytes
4647

4748
from pyfakefs import fake_scandir
@@ -554,6 +555,7 @@ def gethomedir(self, username):
554555

555556
def compile_pattern(self, pattern):
556557
return re.compile(fnmatch.translate(pattern)).fullmatch
558+
557559
else: # Python >= 3.12
558560

559561
class FakePosixPathModule(FakePathModule):
@@ -862,24 +864,48 @@ def _warn_is_reserved_deprecated():
862864
class FakePathlibModule:
863865
"""Uses FakeFilesystem to provide a fake pathlib module replacement.
864866
865-
You need a fake_filesystem to use this::
867+
Automatically created if using `fake_filesystem_unittest.TestCase`,
868+
`fs` fixture, or the `patchfs` decorator.
869+
870+
For creating it separately, a `fake_filesystem` instance is needed::
866871
867872
filesystem = fake_filesystem.FakeFilesystem()
868873
fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)
869874
"""
870875

871-
def __init__(self, filesystem):
876+
def __init__(self, filesystem, from_patcher=False):
872877
"""
873878
Initializes the module with the given filesystem.
874879
875880
Args:
876881
filesystem: FakeFilesystem used to provide file system information
882+
from_patcher: If `False` (the default), `pathlib.os` will be manually
883+
patched using `FakeOsModule`. This allows to instantiate the class
884+
manually for a test.
885+
Will be set to `True` if instantiated from `Patcher`.
877886
"""
878887
init_module(filesystem)
879888
self._pathlib_module = pathlib
889+
self._os = None
890+
self._os_patcher = None
891+
if not from_patcher:
892+
self.patch_os_module()
893+
894+
def patch_os_module(self):
895+
if sys.version_info >= (3, 11) and not isinstance(os, FakeOsModule):
896+
self._os = FakeOsModule(FakePath.filesystem)
897+
pathlib_os = (
898+
"pathlib._local.os" if sys.version_info[:2] == (3, 13) else "pathlib.os"
899+
)
900+
self._os_patcher = mock.patch(pathlib_os, self._os)
901+
self._os_patcher.start()
902+
903+
def __del__(self):
904+
if self._os_patcher is not None:
905+
self._os_patcher.stop()
880906

881907
class PurePosixPath(PurePath):
882-
"""A subclass of PurePath, that represents non-Windows filesystem
908+
"""A subclass of PurePath that represents non-Windows filesystem
883909
paths"""
884910

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

900926
class PureWindowsPath(PurePath):
901-
"""A subclass of PurePath, that represents Windows filesystem paths"""
927+
"""A subclass of PurePath that represents Windows filesystem paths"""
902928

903929
__slots__ = ()
904930

@@ -990,9 +1016,9 @@ class FakePathlibPathModule:
9901016

9911017
fake_pathlib = None
9921018

993-
def __init__(self, filesystem=None):
1019+
def __init__(self, filesystem=None, from_patcher=False):
9941020
if self.fake_pathlib is None:
995-
self.__class__.fake_pathlib = FakePathlibModule(filesystem)
1021+
self.__class__.fake_pathlib = FakePathlibModule(filesystem, from_patcher)
9961022

9971023
@property
9981024
def skip_names(self):

pyfakefs/tests/fake_filesystem_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2614,6 +2614,7 @@ def test_add_existing_real_paths_read_write(self):
26142614

26152615
@unittest.skipIf(pytest is None, "pytest is not installed")
26162616
@unittest.skipIf(sys.version_info < (3, 8), "importlib.metadata not available")
2617+
@unittest.skipIf("pathlib2" in sys.modules, "pathlib2 may break this test")
26172618
def test_add_package_metadata(self):
26182619
parent_path = pathlib.Path(pytest.__file__).parent.parent
26192620
pytest_dist_path = parent_path / f"pytest-{pytest.__version__}.dist-info"

pyfakefs/tests/fake_pathlib_test.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
from unittest.mock import patch
3232

3333
from pyfakefs import fake_pathlib, fake_filesystem, fake_filesystem_unittest, fake_os
34-
from pyfakefs.fake_filesystem import OSType
34+
from pyfakefs.fake_filesystem import OSType, FakeFilesystem
35+
from pyfakefs.fake_pathlib import FakePathlibModule
3536
from pyfakefs.helpers import IS_PYPY, is_root
3637
from pyfakefs.tests.skipped_pathlib import (
3738
check_exists_pathlib,
@@ -1646,5 +1647,13 @@ def test_exists(self):
16461647
self.assertTrue(check_exists_pathlib())
16471648

16481649

1650+
class InstantiatedPathlibTest(unittest.TestCase):
1651+
def test_instantiated_pathlib(self):
1652+
fake_fs = FakeFilesystem()
1653+
fake_pathlib_module = FakePathlibModule(fake_fs)
1654+
top_level_dirs = list(fake_pathlib_module.Path("/").iterdir())
1655+
self.assertEqual(0, len(top_level_dirs))
1656+
1657+
16491658
if __name__ == "__main__":
16501659
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)