Skip to content

Commit 95c4dcb

Browse files
committed
Refactored flask.ext process to not swallow exceptions on weird Pythons.
1 parent f01b654 commit 95c4dcb

File tree

4 files changed

+210
-108
lines changed

4 files changed

+210
-108
lines changed

flask/ext/__init__.py

Lines changed: 6 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -19,91 +19,11 @@
1919
"""
2020

2121

22-
class _ExtensionImporter(object):
23-
"""This importer redirects imports from this submodule to other locations.
24-
This makes it possible to transition from the old flaskext.name to the
25-
newer flask_name without people having a hard time.
26-
"""
27-
_module_choices = ['flask_%s', 'flaskext.%s']
22+
def setup():
23+
from ..exthook import ExtensionImporter
24+
importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__)
25+
importer.install()
2826

29-
def __init__(self):
30-
from sys import meta_path
31-
self.prefix = __name__ + '.'
32-
self.prefix_cutoff = __name__.count('.') + 1
3327

34-
# since people might reload the flask.ext module (by accident or
35-
# intentionally) we have to make sure to not add more than one
36-
# import hook. We can't check class types here either since a new
37-
# class will be created on reload. As a result of that we check
38-
# the name of the class and remove stale instances.
39-
def _name(x):
40-
cls = type(x)
41-
return cls.__module__ + '.' + cls.__name__
42-
this = _name(self)
43-
meta_path[:] = [x for x in meta_path if _name(x) != this] + [self]
44-
45-
def find_module(self, fullname, path=None):
46-
if fullname.startswith(self.prefix):
47-
return self
48-
49-
def load_module(self, fullname):
50-
from sys import modules, exc_info
51-
if fullname in modules:
52-
return modules[fullname]
53-
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff]
54-
for path in self._module_choices:
55-
realname = path % modname
56-
try:
57-
__import__(realname)
58-
except ImportError:
59-
exc_type, exc_value, tb = exc_info()
60-
# since we only establish the entry in sys.modules at the
61-
# very this seems to be redundant, but if recursive imports
62-
# happen we will call into the move import a second time.
63-
# On the second invocation we still don't have an entry for
64-
# fullname in sys.modules, but we will end up with the same
65-
# fake module name and that import will succeed since this
66-
# one already has a temporary entry in the modules dict.
67-
# Since this one "succeeded" temporarily that second
68-
# invocation now will have created a fullname entry in
69-
# sys.modules which we have to kill.
70-
modules.pop(fullname, None)
71-
if self.is_important_traceback(realname, tb):
72-
raise exc_type, exc_value, tb
73-
continue
74-
module = modules[fullname] = modules[realname]
75-
if '.' not in modname:
76-
setattr(modules[__name__], modname, module)
77-
return module
78-
raise ImportError('No module named %s' % fullname)
79-
80-
def is_important_traceback(self, important_module, tb):
81-
"""Walks a traceback's frames and checks if any of the frames
82-
originated in the given important module. If that is the case
83-
then we were able to import the module itself but apparently
84-
something went wrong when the module was imported. (Eg: import
85-
of an import failed).
86-
"""
87-
# Why can we access f_globals' __name__ here and the value is
88-
# not None? I honestly don't know but here is my thinking.
89-
# The module owns a reference to globals and the frame has one.
90-
# Each function only keeps a reference to the globals not do the
91-
# module which normally causes the problem that when the module
92-
# shuts down all globals are set to None. Now however when the
93-
# import system fails Python takes the short way out and does not
94-
# actually properly shut down the module by Noneing the values
95-
# but by just removing the entry from sys.modules. This means
96-
# that the regular reference based cleanup kicks in.
97-
#
98-
# The good thing: At worst we will swallow an exception we should
99-
# not and the error message will be messed up. However I think
100-
# this should be sufficiently reliable.
101-
while tb is not None:
102-
if tb.tb_frame.f_globals.get('__name__') == important_module:
103-
return True
104-
tb = tb.tb_next
105-
return False
106-
107-
108-
_ExtensionImporter()
109-
del _ExtensionImporter
28+
setup()
29+
del setup

flask/exthook.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
flask.exthook
4+
~~~~~~~~~~~~~
5+
6+
Redirect imports for extensions. This module basically makes it possible
7+
for us to transition from flaskext.foo to flask_foo without having to
8+
force all extensions to upgrade at the same time.
9+
10+
When a user does ``from flask.ext.foo import bar`` it will attempt to
11+
import ``from flask_foo import bar`` first and when that fails it will
12+
try to import ``from flaskext.foo import bar``.
13+
14+
We're switching from namespace packages because it was just too painful for
15+
everybody involved.
16+
17+
This is used by `flask.ext`.
18+
19+
:copyright: (c) 2011 by Armin Ronacher.
20+
:license: BSD, see LICENSE for more details.
21+
"""
22+
import sys
23+
import os
24+
25+
26+
class ExtensionImporter(object):
27+
"""This importer redirects imports from this submodule to other locations.
28+
This makes it possible to transition from the old flaskext.name to the
29+
newer flask_name without people having a hard time.
30+
"""
31+
32+
def __init__(self, module_choices, wrapper_module):
33+
self.module_choices = module_choices
34+
self.wrapper_module = wrapper_module
35+
self.prefix = wrapper_module + '.'
36+
self.prefix_cutoff = wrapper_module.count('.') + 1
37+
38+
def __eq__(self, other):
39+
return self.__class__.__module__ == other.__class__.__module__ and \
40+
self.__class__.__name__ == other.__class__.__name__ and \
41+
self.wrapper_module == other.wrapper_module and \
42+
self.module_choices == other.module_choices
43+
44+
def __ne__(self, other):
45+
return not self.__eq__(other)
46+
47+
def install(self):
48+
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self]
49+
50+
def find_module(self, fullname, path=None):
51+
if fullname.startswith(self.prefix):
52+
return self
53+
54+
def load_module(self, fullname):
55+
if fullname in sys.modules:
56+
return sys.modules[fullname]
57+
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff]
58+
for path in self.module_choices:
59+
realname = path % modname
60+
try:
61+
__import__(realname)
62+
except ImportError:
63+
exc_type, exc_value, tb = sys.exc_info()
64+
# since we only establish the entry in sys.modules at the
65+
# very this seems to be redundant, but if recursive imports
66+
# happen we will call into the move import a second time.
67+
# On the second invocation we still don't have an entry for
68+
# fullname in sys.modules, but we will end up with the same
69+
# fake module name and that import will succeed since this
70+
# one already has a temporary entry in the modules dict.
71+
# Since this one "succeeded" temporarily that second
72+
# invocation now will have created a fullname entry in
73+
# sys.modules which we have to kill.
74+
sys.modules.pop(fullname, None)
75+
76+
# If it's an important traceback we reraise it, otherwise
77+
# we swallow it and try the next choice. The skipped frame
78+
# is the one from __import__ above which we don't care about
79+
if self.is_important_traceback(realname, tb):
80+
raise exc_type, exc_value, tb.tb_next
81+
continue
82+
module = sys.modules[fullname] = sys.modules[realname]
83+
if '.' not in modname:
84+
setattr(sys.modules[self.wrapper_module], modname, module)
85+
return module
86+
raise ImportError('No module named %s' % fullname)
87+
88+
def is_important_traceback(self, important_module, tb):
89+
"""Walks a traceback's frames and checks if any of the frames
90+
originated in the given important module. If that is the case then we
91+
were able to import the module itself but apparently something went
92+
wrong when the module was imported. (Eg: import of an import failed).
93+
"""
94+
while tb is not None:
95+
if self.is_important_frame(important_module, tb):
96+
return True
97+
tb = tb.tb_next
98+
return False
99+
100+
def is_important_frame(self, important_module, tb):
101+
"""Checks a single frame if it's important."""
102+
g = tb.tb_frame.f_globals
103+
if '__name__' not in g:
104+
return False
105+
106+
module_name = g['__name__']
107+
108+
# Python 2.7 Behavior. Modules are cleaned up late so the
109+
# name shows up properly here. Success!
110+
if module_name == important_module:
111+
return True
112+
113+
# Some python verisons will will clean up modules so early that the
114+
# module name at that point is no longer set. Try guessing from
115+
# the filename then.
116+
filename = os.path.abspath(tb.tb_frame.f_code.co_filename)
117+
test_string = os.path.sep + important_module.replace('.', os.path.sep)
118+
return test_string + '.py' in filename or \
119+
test_string + os.path.sep + '__init__.py' in filename

flask/testsuite/ext.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ def setup(self):
3535
import_hooks = 0
3636
for item in sys.meta_path:
3737
cls = type(item)
38-
if cls.__module__ == 'flask.ext' and \
39-
cls.__name__ == '_ExtensionImporter':
38+
if cls.__module__ == 'flask.exthook' and \
39+
cls.__name__ == 'ExtensionImporter':
4040
import_hooks += 1
4141
self.assert_equal(import_hooks, 1)
4242

@@ -104,6 +104,18 @@ def test_flaskext_broken_package_no_module_caching(self):
104104
with self.assert_raises(ImportError):
105105
import flask.ext.broken
106106

107+
def test_no_error_swallowing(self):
108+
try:
109+
import flask.ext.broken
110+
except ImportError:
111+
exc_type, exc_value, tb = sys.exc_info()
112+
self.assert_(exc_type is ImportError)
113+
self.assert_equal(str(exc_value), 'No module named missing_module')
114+
self.assert_(tb.tb_frame.f_globals is globals())
115+
116+
next = tb.tb_next
117+
self.assert_('flask_broken/__init__.py' in next.tb_frame.f_code.co_filename)
118+
107119

108120
def suite():
109121
suite = unittest.TestSuite()

scripts/flaskext_compat.py

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,33 @@
1515
:license: BSD, see LICENSE for more details.
1616
"""
1717
import sys
18+
import os
1819
import imp
1920

2021

21-
ext_module = imp.new_module('flask.ext')
22-
ext_module.__path__ = []
23-
ext_module.__package__ = ext_module.__name__
22+
class ExtensionImporter(object):
23+
"""This importer redirects imports from this submodule to other locations.
24+
This makes it possible to transition from the old flaskext.name to the
25+
newer flask_name without people having a hard time.
26+
"""
2427

28+
def __init__(self, module_choices, wrapper_module):
29+
self.module_choices = module_choices
30+
self.wrapper_module = wrapper_module
31+
self.prefix = wrapper_module + '.'
32+
self.prefix_cutoff = wrapper_module.count('.') + 1
2533

26-
class _ExtensionImporter(object):
27-
"""This importer redirects imports from the flask.ext module to other
28-
locations. For implementation details see the code in Flask 0.8
29-
that does the same.
30-
"""
31-
_module_choices = ['flask_%s', 'flaskext.%s']
32-
prefix = ext_module.__name__ + '.'
33-
prefix_cutoff = prefix.count('.')
34+
def __eq__(self, other):
35+
return self.__class__.__module__ == other.__class__.__module__ and \
36+
self.__class__.__name__ == other.__class__.__name__ and \
37+
self.wrapper_module == other.wrapper_module and \
38+
self.module_choices == other.module_choices
39+
40+
def __ne__(self, other):
41+
return not self.__eq__(other)
42+
43+
def install(self):
44+
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self]
3445

3546
def find_module(self, fullname, path=None):
3647
if fullname.startswith(self.prefix):
@@ -40,34 +51,74 @@ def load_module(self, fullname):
4051
if fullname in sys.modules:
4152
return sys.modules[fullname]
4253
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff]
43-
for path in self._module_choices:
54+
for path in self.module_choices:
4455
realname = path % modname
4556
try:
4657
__import__(realname)
4758
except ImportError:
4859
exc_type, exc_value, tb = sys.exc_info()
60+
# since we only establish the entry in sys.modules at the
61+
# very this seems to be redundant, but if recursive imports
62+
# happen we will call into the move import a second time.
63+
# On the second invocation we still don't have an entry for
64+
# fullname in sys.modules, but we will end up with the same
65+
# fake module name and that import will succeed since this
66+
# one already has a temporary entry in the modules dict.
67+
# Since this one "succeeded" temporarily that second
68+
# invocation now will have created a fullname entry in
69+
# sys.modules which we have to kill.
4970
sys.modules.pop(fullname, None)
71+
72+
# If it's an important traceback we reraise it, otherwise
73+
# we swallow it and try the next choice. The skipped frame
74+
# is the one from __import__ above which we don't care about
5075
if self.is_important_traceback(realname, tb):
51-
raise exc_type, exc_value, tb
76+
raise exc_type, exc_value, tb.tb_next
5277
continue
5378
module = sys.modules[fullname] = sys.modules[realname]
5479
if '.' not in modname:
55-
setattr(ext_module, modname, module)
80+
setattr(sys.modules[self.wrapper_module], modname, module)
5681
return module
5782
raise ImportError('No module named %s' % fullname)
5883

5984
def is_important_traceback(self, important_module, tb):
85+
"""Walks a traceback's frames and checks if any of the frames
86+
originated in the given important module. If that is the case then we
87+
were able to import the module itself but apparently something went
88+
wrong when the module was imported. (Eg: import of an import failed).
89+
"""
6090
while tb is not None:
61-
if tb.tb_frame.f_globals.get('__name__') == important_module:
91+
if self.is_important_frame(important_module, tb):
6292
return True
6393
tb = tb.tb_next
6494
return False
6595

96+
def is_important_frame(self, important_module, tb):
97+
"""Checks a single frame if it's important."""
98+
g = tb.tb_frame.f_globals
99+
if '__name__' not in g:
100+
return False
101+
102+
module_name = g['__name__']
103+
104+
# Python 2.7 Behavior. Modules are cleaned up late so the
105+
# name shows up properly here. Success!
106+
if module_name == important_module:
107+
return True
108+
109+
# Some python verisons will will clean up modules so early that the
110+
# module name at that point is no longer set. Try guessing from
111+
# the filename then.
112+
filename = os.path.abspath(tb.tb_frame.f_code.co_filename)
113+
test_string = os.path.sep + important_module.replace('.', os.path.sep)
114+
return test_string + '.py' in filename or \
115+
test_string + os.path.sep + '__init__.py' in filename
116+
66117

67118
def activate():
68-
"""Activates the compatibility system."""
69119
import flask
70-
if hasattr(flask, 'ext'):
71-
return
72-
sys.modules['flask.ext'] = flask.ext = ext_module
73-
sys.meta_path.append(_ExtensionImporter())
120+
ext_module = imp.new_module('flask.ext')
121+
ext_module.__path__ = []
122+
flask.ext = sys.modules['flask.ext'] = ext_module
123+
importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], 'flask.ext')
124+
importer.install()

0 commit comments

Comments
 (0)