-
-
Notifications
You must be signed in to change notification settings - Fork 137
/
Copy pathreload.py
218 lines (189 loc) · 8.36 KB
/
reload.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import builtins
import functools
import importlib
import sys
import types
from contextlib import contextmanager
import sublime_plugin
from .debug import StackMeter, trace
dprint = trace.for_tag("reload")
def reload_plugin():
"""Reload the GitSavvy plugin among with all its modules."""
from GitSavvy import git_savvy
dprint("begin", fill='═')
modules = {name: module for name, module in sys.modules.items()
if name.startswith("GitSavvy.")}
try:
reload_modules(git_savvy, modules)
except:
dprint("ERROR", fill='─')
reload_modules(git_savvy, modules, perform_reload=False)
raise
finally:
ensure_loaded(git_savvy, modules)
dprint("end", fill='━')
def ensure_loaded(main, modules):
# More simple (comparing to reload_modules(perform_reload=False)) and dumb
# approach to ensure all modules are back. Quite useful when debugging the
# "reload" module itself, i.e. for cases when reloading might fail due to
# bugs in reload_modules().
missing_modules = {name: module for name, module in modules.items()
if name not in sys.modules}
if missing_modules:
for name, module in missing_modules:
sys.modules[name] = modules
print("GS [reload] BUG!", "restored", name)
sublime_plugin.reload_plugin(git_savvy.__name__)
def reload_modules(main, modules, perform_reload=True):
"""Implements the machinery for reloading a given plugin module."""
#
# Here's the approach in general:
#
# - Hide GitSavvy modules from the sys.modules temporarily;
#
# - Install a special import hook onto sys.meta_path;
#
# - Call sublime_plugin.reload_plugin(), which imports the main
# "git_savvy" module under the hood, triggering the hook;
#
# - The hook, instead of creating a new module object, peeks the saved
# one and reloads it. Once the module encounters an import statement
# requesting another module, not yet reloaded, the hook reenters and
# processes that new module recursively, then get back to the previous
# one, and so on.
#
# This makes the modules reload in the very same order as they were loaded
# initially, as if they were imported from scratch.
#
if perform_reload:
sublime_plugin.unload_module(main)
# Insert the main "git_savvy" module at the beginning to make the reload
# order be as close to the order of the "natural" import as possible.
module_names = [main.__name__] + sorted(name for name in modules
if name != main.__name__)
# First, remove all the loaded modules from the sys.modules cache,
# otherwise the reloading hook won't be called.
loaded_modules = dict(sys.modules)
for name in loaded_modules:
if name in modules:
del sys.modules[name]
stack_meter = StackMeter()
@FilteringImportHook.when(condition=lambda name: name in modules)
def module_reloader(name):
module = modules[name]
sys.modules[name] = module # restore the module back
if perform_reload:
with stack_meter as depth:
dprint("reloading", ('╿ '*depth) + '┡━─', name)
try:
return module.__loader__.load_module(name)
except:
if name in sys.modules:
del sys.modules[name] # to indicate an error
raise
else:
if name not in loaded_modules:
dprint("NO RELOAD", '╺━─', name)
return module
with intercepting_imports(module_reloader), \
importing_fromlist_aggresively(modules):
# Now, import all the modules back, in order, starting with the main
# module. This will reload all the modules directly or indirectly
# referenced by the main one, i.e. usually most of our modules.
sublime_plugin.reload_plugin(main.__name__)
# Be sure to bring back *all* the modules that used to be loaded, not
# only these imported through the main one. Otherwise, some of them
# might end up being created from scratch as new module objects in
# case of being imported after detaching the hook. In general, most of
# the imports below (if not all) are no-ops though.
for name in module_names:
importlib.import_module(name)
@contextmanager
def importing_fromlist_aggresively(modules):
orig___import__ = builtins.__import__
@functools.wraps(orig___import__)
def __import__(name, globals=None, locals=None, fromlist=(), level=0):
# Given an import statement like this:
#
# from .some.module import something
#
# The original __import__ performs roughly the following steps:
#
# - Import ".some.module", just like the importlib.import_module()
# function would do, i.e. resolve packages, calculate the absolute
# name, check sys.modules for that module, invoke import hooks and
# so on...
#
# - For each name specified in the "fromlist" (a "something" in our
# case), ensure the module have that name in its namespace. This
# could be:
#
# - a regular name defined within that module, like a function
# named "something", and in this case we're done;
#
# - or, in case the module is missing that attribute, there's a
# chance that the requested name refers to a submodule of that
# module, ".some.module.something", and we need to import it.
# Once imported it will take care to register itself within
# the parent's namespace.
#
# This looks natural and it is indeed in case of loading a module for
# the first time. But things start to behave slightly different once
# you try to reload a module.
#
# The main difference is that during the reload the module code is
# executed with its dictionary retained. And this has an undesired
# effect on handling the "fromlist" as described above: the second
# part (involving import of a submodule) is only executed when the
# module dictionary is missing the submodule name, which is not the
# case during the reload.
#
# This is generally not a problem: the name refers to the submodule
# imported earlier anyway. But we need to import it in order to force
# the necessary hook to reload that submodule too.
module = orig___import__(name, globals, locals, fromlist, level)
if fromlist and module.__name__ in modules:
# Refer to _handle_fromlist() from "importlib/_bootstrap.py"
if '*' in fromlist:
fromlist = list(fromlist)
fromlist.remove('*')
fromlist.extend(getattr(module, '__all__', []))
for x in fromlist:
# Here's an altered part of logic.
#
# The original __import__ doesn't even try to import a
# submodule if its name is already in the module namespace,
# but we do that for certain set of the known submodule.
if isinstance(getattr(module, x, None), types.ModuleType):
from_name = '{}.{}'.format(module.__name__, x)
if from_name in modules:
importlib.import_module(from_name)
return module
builtins.__import__ = __import__
try:
yield
finally:
builtins.__import__ = orig___import__
@contextmanager
def intercepting_imports(hook):
sys.meta_path.insert(0, hook)
try:
yield hook
finally:
if hook in sys.meta_path:
sys.meta_path.remove(hook)
class FilteringImportHook:
"""
PEP-302 importer that delegates loading of given modules to a function.
"""
def __init__(self, condition, load_module):
super().__init__()
self.condition = condition
self.load_module = load_module
@classmethod
def when(cls, condition):
"""A handy loader function decorator."""
return lambda load_module: cls(condition, load_module)
def find_module(self, name, path=None):
if self.condition(name):
return self