Skip to content

Commit

Permalink
Patch linecache in Python 3.13
Browse files Browse the repository at this point in the history
- replaces previous workaround that somewhat broke the linecache
- needed because os is now imported locally
  • Loading branch information
mrbean-bremen committed Jul 23, 2024
1 parent 91ad1ad commit e15ab02
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 39 deletions.
26 changes: 26 additions & 0 deletions pyfakefs/fake_filesystem_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
41 changes: 2 additions & 39 deletions pyfakefs/fake_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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)"
Expand Down

0 comments on commit e15ab02

Please sign in to comment.