##// END OF EJS Templates
deepreload: Use `sys.builtin_module_names` to exclude builtins
Nikita Kniazev -
Show More
@@ -1,299 +1,310 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 Provides a reload() function that acts recursively.
3 Provides a reload() function that acts recursively.
4
4
5 Python's normal :func:`python:reload` function only reloads the module that it's
5 Python's normal :func:`python:reload` function only reloads the module that it's
6 passed. The :func:`reload` function in this module also reloads everything
6 passed. The :func:`reload` function in this module also reloads everything
7 imported from that module, which is useful when you're changing files deep
7 imported from that module, which is useful when you're changing files deep
8 inside a package.
8 inside a package.
9
9
10 To use this as your default reload function, type this::
10 To use this as your default reload function, type this::
11
11
12 import builtins
12 import builtins
13 from IPython.lib import deepreload
13 from IPython.lib import deepreload
14 builtins.reload = deepreload.reload
14 builtins.reload = deepreload.reload
15
15
16 A reference to the original :func:`python:reload` is stored in this module as
16 A reference to the original :func:`python:reload` is stored in this module as
17 :data:`original_reload`, so you can restore it later.
17 :data:`original_reload`, so you can restore it later.
18
18
19 This code is almost entirely based on knee.py, which is a Python
19 This code is almost entirely based on knee.py, which is a Python
20 re-implementation of hierarchical module import.
20 re-implementation of hierarchical module import.
21 """
21 """
22 #*****************************************************************************
22 #*****************************************************************************
23 # Copyright (C) 2001 Nathaniel Gray <n8gray@caltech.edu>
23 # Copyright (C) 2001 Nathaniel Gray <n8gray@caltech.edu>
24 #
24 #
25 # Distributed under the terms of the BSD License. The full license is in
25 # Distributed under the terms of the BSD License. The full license is in
26 # the file COPYING, distributed as part of this software.
26 # the file COPYING, distributed as part of this software.
27 #*****************************************************************************
27 #*****************************************************************************
28
28
29 import builtins as builtin_mod
29 import builtins as builtin_mod
30 from contextlib import contextmanager
30 from contextlib import contextmanager
31 import importlib
31 import importlib
32 import sys
32 import sys
33
33
34 from types import ModuleType
34 from types import ModuleType
35 from warnings import warn
35 from warnings import warn
36 import types
36 import types
37
37
38 original_import = builtin_mod.__import__
38 original_import = builtin_mod.__import__
39
39
40 @contextmanager
40 @contextmanager
41 def replace_import_hook(new_import):
41 def replace_import_hook(new_import):
42 saved_import = builtin_mod.__import__
42 saved_import = builtin_mod.__import__
43 builtin_mod.__import__ = new_import
43 builtin_mod.__import__ = new_import
44 try:
44 try:
45 yield
45 yield
46 finally:
46 finally:
47 builtin_mod.__import__ = saved_import
47 builtin_mod.__import__ = saved_import
48
48
49 def get_parent(globals, level):
49 def get_parent(globals, level):
50 """
50 """
51 parent, name = get_parent(globals, level)
51 parent, name = get_parent(globals, level)
52
52
53 Return the package that an import is being performed in. If globals comes
53 Return the package that an import is being performed in. If globals comes
54 from the module foo.bar.bat (not itself a package), this returns the
54 from the module foo.bar.bat (not itself a package), this returns the
55 sys.modules entry for foo.bar. If globals is from a package's __init__.py,
55 sys.modules entry for foo.bar. If globals is from a package's __init__.py,
56 the package's entry in sys.modules is returned.
56 the package's entry in sys.modules is returned.
57
57
58 If globals doesn't come from a package or a module in a package, or a
58 If globals doesn't come from a package or a module in a package, or a
59 corresponding entry is not found in sys.modules, None is returned.
59 corresponding entry is not found in sys.modules, None is returned.
60 """
60 """
61 orig_level = level
61 orig_level = level
62
62
63 if not level or not isinstance(globals, dict):
63 if not level or not isinstance(globals, dict):
64 return None, ''
64 return None, ''
65
65
66 pkgname = globals.get('__package__', None)
66 pkgname = globals.get('__package__', None)
67
67
68 if pkgname is not None:
68 if pkgname is not None:
69 # __package__ is set, so use it
69 # __package__ is set, so use it
70 if not hasattr(pkgname, 'rindex'):
70 if not hasattr(pkgname, 'rindex'):
71 raise ValueError('__package__ set to non-string')
71 raise ValueError('__package__ set to non-string')
72 if len(pkgname) == 0:
72 if len(pkgname) == 0:
73 if level > 0:
73 if level > 0:
74 raise ValueError('Attempted relative import in non-package')
74 raise ValueError('Attempted relative import in non-package')
75 return None, ''
75 return None, ''
76 name = pkgname
76 name = pkgname
77 else:
77 else:
78 # __package__ not set, so figure it out and set it
78 # __package__ not set, so figure it out and set it
79 if '__name__' not in globals:
79 if '__name__' not in globals:
80 return None, ''
80 return None, ''
81 modname = globals['__name__']
81 modname = globals['__name__']
82
82
83 if '__path__' in globals:
83 if '__path__' in globals:
84 # __path__ is set, so modname is already the package name
84 # __path__ is set, so modname is already the package name
85 globals['__package__'] = name = modname
85 globals['__package__'] = name = modname
86 else:
86 else:
87 # Normal module, so work out the package name if any
87 # Normal module, so work out the package name if any
88 lastdot = modname.rfind('.')
88 lastdot = modname.rfind('.')
89 if lastdot < 0 < level:
89 if lastdot < 0 < level:
90 raise ValueError("Attempted relative import in non-package")
90 raise ValueError("Attempted relative import in non-package")
91 if lastdot < 0:
91 if lastdot < 0:
92 globals['__package__'] = None
92 globals['__package__'] = None
93 return None, ''
93 return None, ''
94 globals['__package__'] = name = modname[:lastdot]
94 globals['__package__'] = name = modname[:lastdot]
95
95
96 dot = len(name)
96 dot = len(name)
97 for x in range(level, 1, -1):
97 for x in range(level, 1, -1):
98 try:
98 try:
99 dot = name.rindex('.', 0, dot)
99 dot = name.rindex('.', 0, dot)
100 except ValueError as e:
100 except ValueError as e:
101 raise ValueError("attempted relative import beyond top-level "
101 raise ValueError("attempted relative import beyond top-level "
102 "package") from e
102 "package") from e
103 name = name[:dot]
103 name = name[:dot]
104
104
105 try:
105 try:
106 parent = sys.modules[name]
106 parent = sys.modules[name]
107 except BaseException as e:
107 except BaseException as e:
108 if orig_level < 1:
108 if orig_level < 1:
109 warn("Parent module '%.200s' not found while handling absolute "
109 warn("Parent module '%.200s' not found while handling absolute "
110 "import" % name)
110 "import" % name)
111 parent = None
111 parent = None
112 else:
112 else:
113 raise SystemError("Parent module '%.200s' not loaded, cannot "
113 raise SystemError("Parent module '%.200s' not loaded, cannot "
114 "perform relative import" % name) from e
114 "perform relative import" % name) from e
115
115
116 # We expect, but can't guarantee, if parent != None, that:
116 # We expect, but can't guarantee, if parent != None, that:
117 # - parent.__name__ == name
117 # - parent.__name__ == name
118 # - parent.__dict__ is globals
118 # - parent.__dict__ is globals
119 # If this is violated... Who cares?
119 # If this is violated... Who cares?
120 return parent, name
120 return parent, name
121
121
122 def load_next(mod, altmod, name, buf):
122 def load_next(mod, altmod, name, buf):
123 """
123 """
124 mod, name, buf = load_next(mod, altmod, name, buf)
124 mod, name, buf = load_next(mod, altmod, name, buf)
125
125
126 altmod is either None or same as mod
126 altmod is either None or same as mod
127 """
127 """
128
128
129 if len(name) == 0:
129 if len(name) == 0:
130 # completely empty module name should only happen in
130 # completely empty module name should only happen in
131 # 'from . import' (or '__import__("")')
131 # 'from . import' (or '__import__("")')
132 return mod, None, buf
132 return mod, None, buf
133
133
134 dot = name.find('.')
134 dot = name.find('.')
135 if dot == 0:
135 if dot == 0:
136 raise ValueError('Empty module name')
136 raise ValueError('Empty module name')
137
137
138 if dot < 0:
138 if dot < 0:
139 subname = name
139 subname = name
140 next = None
140 next = None
141 else:
141 else:
142 subname = name[:dot]
142 subname = name[:dot]
143 next = name[dot+1:]
143 next = name[dot+1:]
144
144
145 if buf != '':
145 if buf != '':
146 buf += '.'
146 buf += '.'
147 buf += subname
147 buf += subname
148
148
149 result = import_submodule(mod, subname, buf)
149 result = import_submodule(mod, subname, buf)
150 if result is None and mod != altmod:
150 if result is None and mod != altmod:
151 result = import_submodule(altmod, subname, subname)
151 result = import_submodule(altmod, subname, subname)
152 if result is not None:
152 if result is not None:
153 buf = subname
153 buf = subname
154
154
155 if result is None:
155 if result is None:
156 raise ImportError("No module named %.200s" % name)
156 raise ImportError("No module named %.200s" % name)
157
157
158 return result, next, buf
158 return result, next, buf
159
159
160
160
161 # Need to keep track of what we've already reloaded to prevent cyclic evil
161 # Need to keep track of what we've already reloaded to prevent cyclic evil
162 found_now = {}
162 found_now = {}
163
163
164 def import_submodule(mod, subname, fullname):
164 def import_submodule(mod, subname, fullname):
165 """m = import_submodule(mod, subname, fullname)"""
165 """m = import_submodule(mod, subname, fullname)"""
166 # Require:
166 # Require:
167 # if mod == None: subname == fullname
167 # if mod == None: subname == fullname
168 # else: mod.__name__ + "." + subname == fullname
168 # else: mod.__name__ + "." + subname == fullname
169
169
170 global found_now
170 global found_now
171 if fullname in found_now and fullname in sys.modules:
171 if fullname in found_now and fullname in sys.modules:
172 m = sys.modules[fullname]
172 m = sys.modules[fullname]
173 else:
173 else:
174 print('Reloading', fullname)
174 print('Reloading', fullname)
175 found_now[fullname] = 1
175 found_now[fullname] = 1
176 oldm = sys.modules.get(fullname, None)
176 oldm = sys.modules.get(fullname, None)
177 try:
177 try:
178 if oldm is not None:
178 if oldm is not None:
179 m = importlib.reload(oldm)
179 m = importlib.reload(oldm)
180 else:
180 else:
181 m = importlib.import_module(subname, mod)
181 m = importlib.import_module(subname, mod)
182 except:
182 except:
183 # load_module probably removed name from modules because of
183 # load_module probably removed name from modules because of
184 # the error. Put back the original module object.
184 # the error. Put back the original module object.
185 if oldm:
185 if oldm:
186 sys.modules[fullname] = oldm
186 sys.modules[fullname] = oldm
187 raise
187 raise
188
188
189 add_submodule(mod, m, fullname, subname)
189 add_submodule(mod, m, fullname, subname)
190
190
191 return m
191 return m
192
192
193 def add_submodule(mod, submod, fullname, subname):
193 def add_submodule(mod, submod, fullname, subname):
194 """mod.{subname} = submod"""
194 """mod.{subname} = submod"""
195 if mod is None:
195 if mod is None:
196 return #Nothing to do here.
196 return #Nothing to do here.
197
197
198 if submod is None:
198 if submod is None:
199 submod = sys.modules[fullname]
199 submod = sys.modules[fullname]
200
200
201 setattr(mod, subname, submod)
201 setattr(mod, subname, submod)
202
202
203 return
203 return
204
204
205 def ensure_fromlist(mod, fromlist, buf, recursive):
205 def ensure_fromlist(mod, fromlist, buf, recursive):
206 """Handle 'from module import a, b, c' imports."""
206 """Handle 'from module import a, b, c' imports."""
207 if not hasattr(mod, '__path__'):
207 if not hasattr(mod, '__path__'):
208 return
208 return
209 for item in fromlist:
209 for item in fromlist:
210 if not hasattr(item, 'rindex'):
210 if not hasattr(item, 'rindex'):
211 raise TypeError("Item in ``from list'' not a string")
211 raise TypeError("Item in ``from list'' not a string")
212 if item == '*':
212 if item == '*':
213 if recursive:
213 if recursive:
214 continue # avoid endless recursion
214 continue # avoid endless recursion
215 try:
215 try:
216 all = mod.__all__
216 all = mod.__all__
217 except AttributeError:
217 except AttributeError:
218 pass
218 pass
219 else:
219 else:
220 ret = ensure_fromlist(mod, all, buf, 1)
220 ret = ensure_fromlist(mod, all, buf, 1)
221 if not ret:
221 if not ret:
222 return 0
222 return 0
223 elif not hasattr(mod, item):
223 elif not hasattr(mod, item):
224 import_submodule(mod, item, buf + '.' + item)
224 import_submodule(mod, item, buf + '.' + item)
225
225
226 def deep_import_hook(name, globals=None, locals=None, fromlist=None, level=-1):
226 def deep_import_hook(name, globals=None, locals=None, fromlist=None, level=-1):
227 """Replacement for __import__()"""
227 """Replacement for __import__()"""
228 parent, buf = get_parent(globals, level)
228 parent, buf = get_parent(globals, level)
229
229
230 head, name, buf = load_next(parent, None if level < 0 else parent, name, buf)
230 head, name, buf = load_next(parent, None if level < 0 else parent, name, buf)
231
231
232 tail = head
232 tail = head
233 while name:
233 while name:
234 tail, name, buf = load_next(tail, tail, name, buf)
234 tail, name, buf = load_next(tail, tail, name, buf)
235
235
236 # If tail is None, both get_parent and load_next found
236 # If tail is None, both get_parent and load_next found
237 # an empty module name: someone called __import__("") or
237 # an empty module name: someone called __import__("") or
238 # doctored faulty bytecode
238 # doctored faulty bytecode
239 if tail is None:
239 if tail is None:
240 raise ValueError('Empty module name')
240 raise ValueError('Empty module name')
241
241
242 if not fromlist:
242 if not fromlist:
243 return head
243 return head
244
244
245 ensure_fromlist(tail, fromlist, buf, 0)
245 ensure_fromlist(tail, fromlist, buf, 0)
246 return tail
246 return tail
247
247
248 modules_reloading = {}
248 modules_reloading = {}
249
249
250 def deep_reload_hook(m):
250 def deep_reload_hook(m):
251 """Replacement for reload()."""
251 """Replacement for reload()."""
252 # Hardcode this one as it would raise a NotImplementedError from the
252 # Hardcode this one as it would raise a NotImplementedError from the
253 # bowels of Python and screw up the import machinery after.
253 # bowels of Python and screw up the import machinery after.
254 # unlike other imports the `exclude` list already in place is not enough.
254 # unlike other imports the `exclude` list already in place is not enough.
255
255
256 if m is types:
256 if m is types:
257 return m
257 return m
258 if not isinstance(m, ModuleType):
258 if not isinstance(m, ModuleType):
259 raise TypeError("reload() argument must be module")
259 raise TypeError("reload() argument must be module")
260
260
261 name = m.__name__
261 name = m.__name__
262
262
263 if name not in sys.modules:
263 if name not in sys.modules:
264 raise ImportError("reload(): module %.200s not in sys.modules" % name)
264 raise ImportError("reload(): module %.200s not in sys.modules" % name)
265
265
266 global modules_reloading
266 global modules_reloading
267 try:
267 try:
268 return modules_reloading[name]
268 return modules_reloading[name]
269 except:
269 except:
270 modules_reloading[name] = m
270 modules_reloading[name] = m
271
271
272 try:
272 try:
273 newm = importlib.reload(m)
273 newm = importlib.reload(m)
274 except:
274 except:
275 sys.modules[name] = m
275 sys.modules[name] = m
276 raise
276 raise
277 finally:
277 finally:
278 modules_reloading.clear()
278 modules_reloading.clear()
279 return newm
279 return newm
280
280
281 # Save the original hooks
281 # Save the original hooks
282 original_reload = importlib.reload
282 original_reload = importlib.reload
283
283
284 # Replacement for reload()
284 # Replacement for reload()
285 def reload(module, exclude=('sys', 'os.path', 'builtins', '__main__',
285 def reload(
286 'numpy', 'numpy._globals')):
286 module,
287 exclude=(
288 *sys.builtin_module_names,
289 "sys",
290 "os.path",
291 "builtins",
292 "__main__",
293 "numpy",
294 "numpy._globals",
295 ),
296 ):
287 """Recursively reload all modules used in the given module. Optionally
297 """Recursively reload all modules used in the given module. Optionally
288 takes a list of modules to exclude from reloading. The default exclude
298 takes a list of modules to exclude from reloading. The default exclude
289 list contains sys, __main__, and __builtin__, to prevent, e.g., resetting
299 list contains modules listed in sys.builtin_module_names with additional
300 sys, os.path, builtins and __main__, to prevent, e.g., resetting
290 display, exception, and io hooks.
301 display, exception, and io hooks.
291 """
302 """
292 global found_now
303 global found_now
293 for i in exclude:
304 for i in exclude:
294 found_now[i] = 1
305 found_now[i] = 1
295 try:
306 try:
296 with replace_import_hook(deep_import_hook):
307 with replace_import_hook(deep_import_hook):
297 return deep_reload_hook(module)
308 return deep_reload_hook(module)
298 finally:
309 finally:
299 found_now = {}
310 found_now = {}
General Comments 0
You need to be logged in to leave comments. Login now