From e15ab023bbd11d34eca8fa0c322673258c8f9e60 Mon Sep 17 00:00:00 2001 From: mrbean-bremen Date: Tue, 23 Jul 2024 21:06:33 +0200 Subject: [PATCH] Patch linecache in Python 3.13 - replaces previous workaround that somewhat broke the linecache - needed because os is now imported locally --- pyfakefs/fake_filesystem_unittest.py | 26 ++++++++++++++++++ pyfakefs/fake_open.py | 41 ++-------------------------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py index a9b76dd2..1fc9e475 100644 --- a/pyfakefs/fake_filesystem_unittest.py +++ b/pyfakefs/fake_filesystem_unittest.py @@ -77,6 +77,7 @@ PatchMode, FakeFilesystem, ) +from pyfakefs.fake_os import use_original_os from pyfakefs.helpers import IS_PYPY from pyfakefs.mox3_stubout import StubOutForTesting @@ -573,6 +574,8 @@ def __init__( # save the original open function for use in pytest plugin self.original_open = open self.patch_open_code = patch_open_code + self.linecache_updatecache = None + self.linecache_checkcache = None if additional_skip_names is not None: skip_names = [ @@ -649,6 +652,18 @@ def __init__( self._patching = False self._paused = False + def checkcache(self, filename=None): + """Calls the original linecache.checkcache making sure no fake OS calls + are used.""" + with use_original_os(): + return self.linecache_checkcache(filename) + + def updatecache(self, filename, module_globals=None): + """Calls the original linecache.updatecache making sure no fake OS calls + are used.""" + with use_original_os(): + return self.linecache_updatecache(filename, module_globals) + @classmethod def clear_fs_cache(cls) -> None: """Clear the module cache.""" @@ -957,6 +972,14 @@ def start_patching(self) -> None: self._patching = True self._paused = False + if sys.version_info >= (3, 13): + # in linecache, 'os' is now imported locally, which involves the + # dynamic patcher, therefore we patch the affected functions + self.linecache_updatecache = linecache.updatecache + linecache.updatecache = self.updatecache + self.linecache_checkcache = linecache.checkcache + linecache.checkcache = self.checkcache + self.patch_modules() self.patch_functions() self.patch_defaults() @@ -1047,6 +1070,9 @@ def stop_patching(self, temporary=False) -> None: if self.use_dynamic_patch and self._dyn_patcher: self._dyn_patcher.cleanup() sys.meta_path.pop(0) + if self.linecache_updatecache is not None: + linecache.updatecache = self.linecache_updatecache + linecache.checkcache = self.linecache_checkcache @property def is_patching(self): diff --git a/pyfakefs/fake_open.py b/pyfakefs/fake_open.py index 93da69a9..ef60d5b4 100644 --- a/pyfakefs/fake_open.py +++ b/pyfakefs/fake_open.py @@ -70,19 +70,6 @@ "x+": (False, True, True, False, False, True), } -real_call_line_no = None - - -def _real_call_line_no(): - global real_call_line_no - if real_call_line_no is None: - fake_io_source = os.path.join(os.path.dirname(__file__), "fake_io.py") - for i, line in enumerate(io_open(fake_io_source)): - if "return self._io_module.open_code(path)" in line: - real_call_line_no = i + 1 - break - return real_call_line_no - def fake_open( filesystem: "FakeFilesystem", @@ -99,25 +86,6 @@ def fake_open( """Redirect the call to FakeFileOpen. See FakeFileOpen.call() for description. """ - # since Python 3.13, we can run into a recursion in some instances here: - # traceback calls linecache.update_cache, which loads 'os' dynamically, - # which will be patched by the dynamic patcher and ends up here again; - # for these instances, we use a shortcut check here - if ( - isinstance(file, str) - and file.endswith(("fake_open.py", "fake_io.py")) - and os.path.split(os.path.dirname(file))[1] == "pyfakefs" - ): - return io_open( # pytype: disable=wrong-arg-count - file, - mode, - buffering, - encoding, - errors, - newline, - closefd, - opener, - ) # workaround for built-in open called from skipped modules (see #552) # as open is not imported explicitly, we cannot patch it for @@ -127,13 +95,8 @@ def fake_open( # handle the case that we try to call the original `open_code` # and get here instead (since Python 3.12) - # TODO: use a more generic approach (see other PR #1025) - if sys.version_info >= (3, 13): - # TODO: check if stacktrace line is still not filled in final version - from_open_code = ( - stack[0].name == "open_code" and stack[0].lineno == _real_call_line_no() - ) - elif sys.version_info >= (3, 12): + # TODO: use a more generic approach (see PR #1025) + if sys.version_info >= (3, 12): from_open_code = ( stack[0].name == "open_code" and stack[0].line == "return self._io_module.open_code(path)"