Skip to content

Commit

Permalink
Merge pull request #45 from alexmojaki/reload
Browse files Browse the repository at this point in the history
Clear caches when modules are reloaded
  • Loading branch information
alexmojaki committed Aug 13, 2022
2 parents 428db94 + d27bd11 commit 2287b88
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 1 deletion.
29 changes: 29 additions & 0 deletions executing/executing.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,26 @@ def code_qualname(self, code):
return self._qualnames.get((code.co_name, code.co_firstlineno), code.co_name)


if PY3:
from importlib.abc import MetaPathFinder

class ReloadCacheFinder(MetaPathFinder):
def find_spec(self, fullname, path, target=None):
# Based on https://github.com/ipython/ipython/issues/13598#issuecomment-1207869067
# to fix that issue.
# `target` should be a module (rather than None) if and only if the module is being reloaded.
filename = getattr(target, "__file__", None)
if not filename:
return
# Clear any cached data for this filename.
linecache.checkcache(filename)
for source_class in all_subclasses(Source):
source_cache = source_class._class_local("__source_cache", {})
source_cache.pop(filename, None)

sys.meta_path.insert(0, ReloadCacheFinder())


class Executing(object):
"""
Information about the operation a frame is currently executing.
Expand Down Expand Up @@ -1121,3 +1141,12 @@ def node_linenos(node):
linenos = [node.lineno]
for lineno in linenos:
yield lineno


def all_subclasses(cls):
"""
Returns a set of all subclasses of a given class, including the class itself,
and all direct and indirect descendants.
"""
direct = set(cls.__subclasses__())
return {cls} | {s for c in direct for s in all_subclasses(c)}
62 changes: 61 additions & 1 deletion tests/test_pytest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import importlib
import os
import sys
from time import sleep

from littleutils import SimpleNamespace

from executing.executing import is_ipython_cell_code, attr_names_match
from executing import Source
from executing.executing import is_ipython_cell_code, attr_names_match, all_subclasses, PY3

sys.path.append(os.path.dirname(os.path.dirname(__file__)))

Expand Down Expand Up @@ -58,3 +61,60 @@ def test_attr_names_match():
assert not attr_names_match("_Class__foo", "__foo")
assert not attr_names_match("__foo", "Class__foo")
assert not attr_names_match("__foo", "_Class_foo")


class MySource(Source):
pass


class MySource2(MySource):
pass


def test_all_subclasses():
assert all_subclasses(Source) == {Source, MySource, MySource2}


def test_source_reload(tmpdir):
if not PY3:
return

from executing.executing import ReloadCacheFinder
assert sum(isinstance(x, ReloadCacheFinder) for x in sys.meta_path) == 1

check_source_reload(tmpdir, Source)
check_source_reload(tmpdir, MySource)
check_source_reload(tmpdir, MySource2)


def check_source_reload(tmpdir, SourceClass):
from pathlib import Path

modname = "test_tmp_module_" + SourceClass.__name__
path = Path(str(tmpdir)) / ("%s.py" % modname)
with path.open("w") as f:
f.write("1\n")

sys.path.append(str(tmpdir))
mod = importlib.import_module(modname)
# ReloadCacheFinder uses __file__ so it needs to be the same
# as what we pass to Source.for_filename here.
assert mod.__file__ == str(path)

# Initial sanity check.
source = SourceClass.for_filename(path)
assert source.text == "1\n"

# Wait a little before changing the file so that the mtime is different
# so that linecache.checkcache() notices.
sleep(0.01)
with path.open("w") as f:
f.write("2\n")
source = SourceClass.for_filename(path)
# Since the module wasn't reloaded, the text hasn't been updated.
assert source.text == "1\n"

# Reload the module. This should update the text.
importlib.reload(mod)
source = SourceClass.for_filename(path)
assert source.text == "2\n"

0 comments on commit 2287b88

Please sign in to comment.