Repository: pytest-dev/pyfakefs
Pull request #1112 (2025-01-13 23:05:10)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.6 → v0.9.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.6...v0.9.1)
<!--pre-commit.ci end-->


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4e482ed..a3dc0b3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
           - "--py37-plus"
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: "v0.8.6"
+    rev: "v0.9.1"
     hooks:
       - id: ruff
         args: ["--fix"]
diff --git a/CHANGES.md b/CHANGES.md
index b381f4a..27fb6c3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -18,6 +18,8 @@ The released versions correspond to PyPI releases.
 * fixed a problem with module and session scoped fixtures in Python 3.13
   (see [#1101](../../issues/1101))
 * fixed handling of `cwd` if set to a `pathlib.Path` (see [#1108](../../issues/1108))
+* fixed documentation for cleanup handlers, added convenience handler `reload_cleanup_handler`
+  (see [#1105](../../issues/1105))
 
 ## [Version 5.7.3](https://pypi.python.org/pypi/pyfakefs/5.7.3) (2024-12-15)
 Fixes a regression in version 5.7.3.
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index f636c5b..940c9c0 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -374,9 +374,25 @@ be added to the patcher:
 
   @pytest.fixture
   def my_fs():
-      with Patcher():
-          patcher["modulename"] = handler_no_cleanup
-          yield
+      with Patcher() as patcher:
+          patcher.cleanup_handlers["modulename"] = handler_no_cleanup
+          yield patcher.fs
+
+
+A specific problem are modules that use filesystem functions and are imported by other modules locally
+(e.g. inside a function). These kinds of modules are not correctly reset and need to be reloaded manually.
+For this case, the cleanup handler `reload_cleanup_handler` in `pyfakefs.helpers` can be used:
+
+.. code:: python
+
+  from pyfakefs.helpers import reload_cleanup_handler
+
+
+  @pytest.fixture
+  def my_fs():
+      with Patcher() as patcher:
+          patcher.cleanup_handlers["modulename"] = reload_cleanup_handler
+          yield patcher.fs
 
 As this may not be trivial, we recommend to write an issue in ``pyfakefs`` with a reproducible example.
 We will analyze the problem, and if we find a solution we will either get this fixed in ``pyfakefs``
diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py
index efaa178..53d5265 100644
--- a/pyfakefs/fake_filesystem.py
+++ b/pyfakefs/fake_filesystem.py
@@ -1831,7 +1831,7 @@ class FakeFilesystem:
                 open_file = self.get_open_file(file_path).get_object()
                 assert isinstance(open_file, FakeFile)
                 return open_file
-            raise TypeError("path should be string, bytes or " "os.PathLike, not int")
+            raise TypeError("path should be string, bytes or os.PathLike, not int")
 
         if follow_symlinks:
             return self.get_object_from_normpath(
diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py
index 0b4a2cc..83c4011 100644
--- a/pyfakefs/fake_os.py
+++ b/pyfakefs/fake_os.py
@@ -971,8 +971,7 @@ class FakeOsModule:
                 raise NotImplementedError("dir_fd unavailable on this platform")
             if isinstance(path, int):
                 raise ValueError(
-                    "%s: Can't specify dir_fd without "
-                    "matching path_str" % fct.__name__
+                    "%s: Can't specify dir_fd without matching path_str" % fct.__name__
                 )
             if not self.path.isabs(path):
                 open_file = self.filesystem.get_open_file(dir_fd)
@@ -1068,7 +1067,7 @@ class FakeOsModule:
         """
         if self.filesystem.is_windows_fs and sys.version_info < (3, 13):
             raise AttributeError(
-                "module 'os' has no attribute 'fchmod'. " "Did you mean: 'chmod'?"
+                "module 'os' has no attribute 'fchmod'. Did you mean: 'chmod'?"
             )
         self.filesystem.chmod(fd, mode)
 
@@ -1094,7 +1093,7 @@ class FakeOsModule:
             self.chmod not in self.supports_follow_symlinks or IS_PYPY
         ):
             raise NotImplementedError(
-                "`follow_symlinks` for chmod() is not available " "on this system"
+                "`follow_symlinks` for chmod() is not available on this system"
             )
         path = self._path_with_dir_fd(path, self.chmod, dir_fd)
         self.filesystem.chmod(path, mode, follow_symlinks)
diff --git a/pyfakefs/fake_path.py b/pyfakefs/fake_path.py
index 7719ef0..d421b11 100644
--- a/pyfakefs/fake_path.py
+++ b/pyfakefs/fake_path.py
@@ -360,7 +360,7 @@ class FakePathModule:
         symbolic links encountered in the path.
         """
         if strict is not None and sys.version_info < (3, 10):
-            raise TypeError("realpath() got an unexpected " "keyword argument 'strict'")
+            raise TypeError("realpath() got an unexpected keyword argument 'strict'")
         if strict:
             # raises in strict mode if the file does not exist
             self.filesystem.resolve(filename)
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py
index 1df2a1c..3697085 100644
--- a/pyfakefs/fake_pathlib.py
+++ b/pyfakefs/fake_pathlib.py
@@ -179,14 +179,14 @@ class _FakeAccessor(accessor):  # type: ignore[valid-type, misc]
         if "follow_symlinks" in kwargs:
             if sys.version_info < (3, 10):
                 raise TypeError(
-                    "chmod() got an unexpected keyword " "argument 'follow_symlinks'"
+                    "chmod() got an unexpected keyword argument 'follow_symlinks'"
                 )
 
             if not kwargs["follow_symlinks"] and (
                 os.chmod not in os.supports_follow_symlinks or IS_PYPY
             ):
                 raise NotImplementedError(
-                    "`follow_symlinks` for chmod() is not available " "on this system"
+                    "`follow_symlinks` for chmod() is not available on this system"
                 )
         return pathobj.filesystem.chmod(str(pathobj), *args, **kwargs)
 
@@ -432,7 +432,7 @@ if sys.version_info < (3, 12):
                     return pwd.getpwnam(username).pw_dir
                 except KeyError:
                     raise RuntimeError(
-                        "Can't determine home directory " "for %r" % username
+                        "Can't determine home directory for %r" % username
                     )
 
     class _FakeWindowsFlavour(_FakeFlavour):
@@ -502,7 +502,7 @@ if sys.version_info < (3, 12):
                     drv, root, parts = self.parse_parts((userhome,))
                     if parts[-1] != os.environ["USERNAME"]:
                         raise RuntimeError(
-                            "Can't determine home directory " "for %r" % username
+                            "Can't determine home directory for %r" % username
                         )
                     parts[-1] = username
                     if drv or root:
@@ -549,7 +549,7 @@ if sys.version_info < (3, 12):
                     return pwd.getpwnam(username).pw_dir
                 except KeyError:
                     raise RuntimeError(
-                        "Can't determine home directory " "for %r" % username
+                        "Can't determine home directory for %r" % username
                     )
 
         def compile_pattern(self, pattern):
@@ -765,9 +765,7 @@ class FakePath(pathlib.Path):
         if not isinstance(data, str):
             raise TypeError("data must be str, not %s" % data.__class__.__name__)
         if newline is not None and sys.version_info < (3, 10):
-            raise TypeError(
-                "write_text() got an unexpected " "keyword argument 'newline'"
-            )
+            raise TypeError("write_text() got an unexpected keyword argument 'newline'")
         with fake_open(
             self.filesystem,
             self.skip_names,
diff --git a/pyfakefs/fake_scandir.py b/pyfakefs/fake_scandir.py
index fff2538..308dff6 100644
--- a/pyfakefs/fake_scandir.py
+++ b/pyfakefs/fake_scandir.py
@@ -127,7 +127,7 @@ class ScanDirIter:
         if isinstance(path, int):
             if self.filesystem.is_windows_fs:
                 raise NotImplementedError(
-                    "scandir does not support file descriptor " "path argument"
+                    "scandir does not support file descriptor path argument"
                 )
             self.abspath = self.filesystem.absnormpath(
                 self.filesystem.get_open_file(path).get_object().path
diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py
index 6db8d96..0e08813 100644
--- a/pyfakefs/helpers.py
+++ b/pyfakefs/helpers.py
@@ -13,6 +13,7 @@
 """Helper classes use for fake file system implementation."""
 
 import ctypes
+import importlib
 import io
 import locale
 import os
@@ -366,7 +367,7 @@ class FakeStatResult:
     def st_file_attributes(self) -> int:
         if not self.is_windows:
             raise AttributeError(
-                "module 'os.stat_result' " "has no attribute 'st_file_attributes'"
+                "module 'os.stat_result' has no attribute 'st_file_attributes'"
             )
         mode = 0
         st_mode = self.st_mode
@@ -384,7 +385,7 @@ class FakeStatResult:
     def st_reparse_tag(self) -> int:
         if not self.is_windows or sys.version_info < (3, 8):
             raise AttributeError(
-                "module 'os.stat_result' " "has no attribute 'st_reparse_tag'"
+                "module 'os.stat_result' has no attribute 'st_reparse_tag'"
             )
         if self.st_mode & stat.S_IFLNK:
             return stat.IO_REPARSE_TAG_SYMLINK  # type: ignore[attr-defined]
@@ -539,3 +540,12 @@ def is_called_from_skipped_module(
         ):
             return True
     return False
+
+
+def reload_cleanup_handler(name):
+    """Cleanup handler that reloads the module with the given name.
+    Maybe needed in cases where a module is imported locally.
+    """
+    if name in sys.modules:
+        importlib.reload(sys.modules[name])
+    return True


Test patch:
diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml
index 8c575e1..162b1d4 100644
--- a/.github/workflows/testsuite.yml
+++ b/.github/workflows/testsuite.yml
@@ -32,17 +32,18 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macOS-latest, windows-latest]
-        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
+        python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
         include:
           - python-version: "pypy-3.7"
-            os: ubuntu-latest
+            os: ubuntu-22.04
           - python-version: "pypy-3.9"
             os: ubuntu-latest
           - python-version: "pypy-3.10"
             os: ubuntu-latest
-        exclude:
-          - python-version: 3.7
-            os: macOS-latest
+          - python-version: "3.7"
+            os: ubuntu-22.04
+          - python-version: "3.7"
+            os: windows-latest
 
     steps:
     - uses: actions/checkout@v4
diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py
index 0460fbc..983c582 100644
--- a/pyfakefs/fake_filesystem_unittest.py
+++ b/pyfakefs/fake_filesystem_unittest.py
@@ -530,7 +530,7 @@ class Patcher:
     FS_DEFARGS: List[Tuple[FunctionType, int, Callable[..., Any]]] = []
     SKIPPED_FS_MODULES: Dict[str, Set[Tuple[ModuleType, str]]] = {}
 
-    assert None in SKIPMODULES, "sys.modules contains 'None' values;" " must skip them."
+    assert None in SKIPMODULES, "sys.modules contains 'None' values; must skip them."
 
     IS_WINDOWS = sys.platform in ("win32", "cygwin")
 
diff --git a/pyfakefs/pytest_tests/lib_using_pathlib.py b/pyfakefs/pytest_tests/lib_using_pathlib.py
new file mode 100644
index 0000000..944c471
--- /dev/null
+++ b/pyfakefs/pytest_tests/lib_using_pathlib.py
@@ -0,0 +1,5 @@
+import pathlib
+
+
+def use_pathlib(path: str):
+    return pathlib.Path(path)
diff --git a/pyfakefs/pytest_tests/local_import.py b/pyfakefs/pytest_tests/local_import.py
new file mode 100644
index 0000000..afe2676
--- /dev/null
+++ b/pyfakefs/pytest_tests/local_import.py
@@ -0,0 +1,4 @@
+def load(path: str) -> str:
+    from pyfakefs.pytest_tests import lib_using_pathlib
+
+    return lib_using_pathlib.use_pathlib(path)
diff --git a/pyfakefs/pytest_tests/test_reload_local_import.py b/pyfakefs/pytest_tests/test_reload_local_import.py
new file mode 100644
index 0000000..a29e06e
--- /dev/null
+++ b/pyfakefs/pytest_tests/test_reload_local_import.py
@@ -0,0 +1,26 @@
+import pytest
+
+from pyfakefs.fake_filesystem_unittest import Patcher
+from pyfakefs.fake_pathlib import FakePathlibModule
+from pyfakefs.helpers import reload_cleanup_handler
+from pyfakefs.pytest_tests import local_import
+
+
+@pytest.fixture
+def test_fs():
+    with Patcher() as patcher:
+        patcher.cleanup_handlers["pyfakefs.pytest_tests.lib_using_pathlib"] = (
+            reload_cleanup_handler
+        )
+        yield patcher.fs
+
+
+class TestReloadCleanupHandler:
+    def test1(self, test_fs):
+        path = local_import.load("some_path")
+        assert isinstance(path, FakePathlibModule.Path)
+
+    def test2(self):
+        path = local_import.load("some_path")
+        # will fail without reload handler
+        assert not isinstance(path, FakePathlibModule.Path)
diff --git a/pyfakefs/tests/example_test.py b/pyfakefs/tests/example_test.py
index 2e5c03b..83e301f 100644
--- a/pyfakefs/tests/example_test.py
+++ b/pyfakefs/tests/example_test.py
@@ -89,7 +89,7 @@ class TestExample(fake_filesystem_unittest.TestCase):  # pylint: disable=R0904
 
     def test_delete_file(self):
         """Test example.delete_file() which uses `os.remove()`."""
-        self.fs.create_file("/test/full.txt", contents="First line\n" "Second Line\n")
+        self.fs.create_file("/test/full.txt", contents="First line\nSecond Line\n")
         self.assertTrue(os.path.exists("/test/full.txt"))
         example.delete_file("/test/full.txt")
         self.assertFalse(os.path.exists("/test/full.txt"))
diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py
index 96f7046..9af21ee 100644
--- a/pyfakefs/tests/fake_filesystem_test.py
+++ b/pyfakefs/tests/fake_filesystem_test.py
@@ -1260,7 +1260,7 @@ class FakePathModuleTest(TestCase):
         if private_path_function:
             self.assertTrue(
                 hasattr(self.path, private_path_function),
-                "Get a real os.path function " "not implemented in fake os.path",
+                "Get a real os.path function not implemented in fake os.path",
             )
         self.assertFalse(hasattr(self.path, "nonexistent"))
 
@@ -1771,7 +1771,7 @@ class DiskSpaceTest(TestCase):
             self.fs.create_file("!foo!bar", contents=b"a" * 100)
         except OSError:
             self.fail(
-                "File with contents fitting into disk space " "could not be written."
+                "File with contents fitting into disk space could not be written."
             )
 
         self.assertEqual(initial_usage.used + 100, self.fs.get_disk_usage().used)
@@ -1863,7 +1863,7 @@ class DiskSpaceTest(TestCase):
             self.fs.create_file("!mount_unlimited!foo", st_size=1000000)
         except OSError:
             self.fail(
-                "File with contents fitting into " "disk space could not be written."
+                "File with contents fitting into disk space could not be written."
             )
 
     def test_that_disk_usage_of_correct_mount_point_is_used(self):
diff --git a/pyfakefs/tests/fake_filesystem_unittest_test.py b/pyfakefs/tests/fake_filesystem_unittest_test.py
index ff571d6..24d6e78 100644
--- a/pyfakefs/tests/fake_filesystem_unittest_test.py
+++ b/pyfakefs/tests/fake_filesystem_unittest_test.py
@@ -112,7 +112,7 @@ class TestPyfakefsUnittest(TestPyfakefsUnittestBase):  # pylint: disable=R0904
         with open("/fake_file.txt", encoding="utf8") as f:
             content = f.read()
         self.assertEqual(
-            "This test file was created using the " "open() function.\n",
+            "This test file was created using the open() function.\n",
             content,
         )
 
@@ -120,12 +120,12 @@ class TestPyfakefsUnittest(TestPyfakefsUnittestBase):  # pylint: disable=R0904
         """Fake io module is bound"""
         self.assertFalse(os.path.exists("/fake_file.txt"))
         with io_open("/fake_file.txt", "w", encoding="utf8") as f:
-            f.write("This test file was created using the" " io.open() function.\n")
+            f.write("This test file was created using the io.open() function.\n")
         self.assertTrue(self.fs.exists("/fake_file.txt"))
         with open("/fake_file.txt", encoding="utf8") as f:
             content = f.read()
         self.assertEqual(
-            "This test file was created using the " "io.open() function.\n",
+            "This test file was created using the io.open() function.\n",
             content,
         )
 
diff --git a/pyfakefs/tests/fake_open_test.py b/pyfakefs/tests/fake_open_test.py
index fdec816..f5ffa87 100644
--- a/pyfakefs/tests/fake_open_test.py
+++ b/pyfakefs/tests/fake_open_test.py
@@ -284,7 +284,7 @@ class FakeFileOpenTest(FakeFileOpenTestBase):
     def test_append_existing_file(self):
         file_path = self.make_path("appendfile")
         contents = [
-            "Contents of original file" "Appended contents",
+            "Contents of original fileAppended contents",
         ]
 
         self.create_file(file_path, contents=contents[0])
@@ -1445,7 +1445,7 @@ class OpenFileWithEncodingTest(FakeFileOpenTestBase):
 
     def test_create_file_with_append(self):
         contents = [
-            "Allons enfants de la Patrie," "Le jour de gloire est arrivé!",
+            "Allons enfants de la Patrie,Le jour de gloire est arrivé!",
             "Contre nous de la tyrannie,",
             "L’étendard sanglant est levé.",
         ]
@@ -1458,7 +1458,7 @@ class OpenFileWithEncodingTest(FakeFileOpenTestBase):
 
     def test_append_existing_file(self):
         contents = [
-            "Оригинальное содержание" "Дополнительное содержание",
+            "Оригинальное содержаниеДополнительное содержание",
         ]
         self.create_file(self.file_path, contents=contents[0], encoding="cyrillic")
         with self.open(self.file_path, "a", encoding="cyrillic") as fake_file:
diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py
index a110207..6395248 100644
--- a/pyfakefs/tests/fake_os_test.py
+++ b/pyfakefs/tests/fake_os_test.py
@@ -4792,7 +4792,7 @@ class FakeOsModuleWalkTest(FakeOsModuleTestBase):
     def test_walk_calls_on_error_if_not_directory(self):
         """Calls onerror with correct errno when walking non-directory."""
         self.ResetErrno()
-        filename = self.make_path("foo" "bar")
+        filename = self.make_path("foobar")
         self.create_file(filename)
         self.assertEqual(True, self.os.path.exists(filename))
         # Calling `os.walk` on a file should trigger a call to the