Commit 0c09f4aa authored by Phillip J. Eby's avatar Phillip J. Eby

Updated the warnings, linecache, inspect, traceback, site, and doctest modules

to work correctly with modules imported from zipfiles or via other PEP 302
__loader__ objects.  Tests and doc updates are included.
parent f2bb2830
...@@ -15,7 +15,7 @@ the formatted traceback. ...@@ -15,7 +15,7 @@ the formatted traceback.
The \module{linecache} module defines the following functions: The \module{linecache} module defines the following functions:
\begin{funcdesc}{getline}{filename, lineno} \begin{funcdesc}{getline}{filename, lineno\optional{, module_globals}}
Get line \var{lineno} from file named \var{filename}. This function Get line \var{lineno} from file named \var{filename}. This function
will never throw an exception --- it will return \code{''} on errors will never throw an exception --- it will return \code{''} on errors
(the terminating newline character will be included for lines that are (the terminating newline character will be included for lines that are
...@@ -23,7 +23,11 @@ found). ...@@ -23,7 +23,11 @@ found).
If a file named \var{filename} is not found, the function will look If a file named \var{filename} is not found, the function will look
for it in the module\indexiii{module}{search}{path} search path, for it in the module\indexiii{module}{search}{path} search path,
\code{sys.path}. \code{sys.path}, after first checking for a PEP 302 \code{__loader__}
in \var{module_globals}, in case the module was imported from a zipfile
or other non-filesystem import source.
\versionadded[The \var{module_globals} parameter was added]{2.5}
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{clearcache}{} \begin{funcdesc}{clearcache}{}
......
...@@ -169,7 +169,8 @@ the latter would defeat the purpose of the warning message). ...@@ -169,7 +169,8 @@ the latter would defeat the purpose of the warning message).
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{warn_explicit}{message, category, filename, \begin{funcdesc}{warn_explicit}{message, category, filename,
lineno\optional{, module\optional{, registry}}} lineno\optional{, module\optional{, registry\optional{,
module_globals}}}}
This is a low-level interface to the functionality of This is a low-level interface to the functionality of
\function{warn()}, passing in explicitly the message, category, \function{warn()}, passing in explicitly the message, category,
filename and line number, and optionally the module name and the filename and line number, and optionally the module name and the
...@@ -179,6 +180,11 @@ stripped; if no registry is passed, the warning is never suppressed. ...@@ -179,6 +180,11 @@ stripped; if no registry is passed, the warning is never suppressed.
\var{message} must be a string and \var{category} a subclass of \var{message} must be a string and \var{category} a subclass of
\exception{Warning} or \var{message} may be a \exception{Warning} instance, \exception{Warning} or \var{message} may be a \exception{Warning} instance,
in which case \var{category} will be ignored. in which case \var{category} will be ignored.
\var{module_globals}, if supplied, should be the global namespace in use
by the code for which the warning is issued. (This argument is used to
support displaying source for modules found in zipfiles or other
non-filesystem import sources, and was added in Python 2.5.)
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{showwarning}{message, category, filename, \begin{funcdesc}{showwarning}{message, category, filename,
......
...@@ -236,6 +236,15 @@ def _normalize_module(module, depth=2): ...@@ -236,6 +236,15 @@ def _normalize_module(module, depth=2):
else: else:
raise TypeError("Expected a module, string, or None") raise TypeError("Expected a module, string, or None")
def _load_testfile(filename, package, module_relative):
if module_relative:
package = _normalize_module(package, 3)
filename = _module_relative_path(package, filename)
if hasattr(package, '__loader__'):
if hasattr(package.__loader__, 'get_data'):
return package.__loader__.get_data(filename), filename
return open(filename).read(), filename
def _indent(s, indent=4): def _indent(s, indent=4):
""" """
Add the given number of space characters to the beginning every Add the given number of space characters to the beginning every
...@@ -1319,13 +1328,13 @@ class DocTestRunner: ...@@ -1319,13 +1328,13 @@ class DocTestRunner:
__LINECACHE_FILENAME_RE = re.compile(r'<doctest ' __LINECACHE_FILENAME_RE = re.compile(r'<doctest '
r'(?P<name>[\w\.]+)' r'(?P<name>[\w\.]+)'
r'\[(?P<examplenum>\d+)\]>$') r'\[(?P<examplenum>\d+)\]>$')
def __patched_linecache_getlines(self, filename): def __patched_linecache_getlines(self, filename, module_globals=None):
m = self.__LINECACHE_FILENAME_RE.match(filename) m = self.__LINECACHE_FILENAME_RE.match(filename)
if m and m.group('name') == self.test.name: if m and m.group('name') == self.test.name:
example = self.test.examples[int(m.group('examplenum'))] example = self.test.examples[int(m.group('examplenum'))]
return example.source.splitlines(True) return example.source.splitlines(True)
else: else:
return self.save_linecache_getlines(filename) return self.save_linecache_getlines(filename, module_globals)
def run(self, test, compileflags=None, out=None, clear_globs=True): def run(self, test, compileflags=None, out=None, clear_globs=True):
""" """
...@@ -1933,9 +1942,7 @@ def testfile(filename, module_relative=True, name=None, package=None, ...@@ -1933,9 +1942,7 @@ def testfile(filename, module_relative=True, name=None, package=None,
"relative paths.") "relative paths.")
# Relativize the path # Relativize the path
if module_relative: text, filename = _load_testfile(filename, package, module_relative)
package = _normalize_module(package)
filename = _module_relative_path(package, filename)
# If no name was given, then use the file's name. # If no name was given, then use the file's name.
if name is None: if name is None:
...@@ -1955,8 +1962,7 @@ def testfile(filename, module_relative=True, name=None, package=None, ...@@ -1955,8 +1962,7 @@ def testfile(filename, module_relative=True, name=None, package=None,
runner = DocTestRunner(verbose=verbose, optionflags=optionflags) runner = DocTestRunner(verbose=verbose, optionflags=optionflags)
# Read the file, convert it to a test, and run it. # Read the file, convert it to a test, and run it.
s = open(filename).read() test = parser.get_doctest(text, globs, name, filename, 0)
test = parser.get_doctest(s, globs, name, filename, 0)
runner.run(test) runner.run(test)
if report: if report:
...@@ -2336,15 +2342,13 @@ def DocFileTest(path, module_relative=True, package=None, ...@@ -2336,15 +2342,13 @@ def DocFileTest(path, module_relative=True, package=None,
"relative paths.") "relative paths.")
# Relativize the path. # Relativize the path.
if module_relative: doc, path = _load_testfile(path, package, module_relative)
package = _normalize_module(package)
path = _module_relative_path(package, path)
if "__file__" not in globs: if "__file__" not in globs:
globs["__file__"] = path globs["__file__"] = path
# Find the file and read it. # Find the file and read it.
name = os.path.basename(path) name = os.path.basename(path)
doc = open(path).read()
# Convert it to a test, and wrap it in a DocFileCase. # Convert it to a test, and wrap it in a DocFileCase.
test = parser.get_doctest(doc, globs, name, path, 0) test = parser.get_doctest(doc, globs, name, path, 0)
......
...@@ -353,7 +353,7 @@ def getsourcefile(object): ...@@ -353,7 +353,7 @@ def getsourcefile(object):
if 'b' in mode and string.lower(filename[-len(suffix):]) == suffix: if 'b' in mode and string.lower(filename[-len(suffix):]) == suffix:
# Looks like a binary file. We want to only return a text file. # Looks like a binary file. We want to only return a text file.
return None return None
if os.path.exists(filename): if os.path.exists(filename) or hasattr(getmodule(object),'__loader__'):
return filename return filename
def getabsfile(object): def getabsfile(object):
...@@ -379,7 +379,7 @@ def getmodule(object): ...@@ -379,7 +379,7 @@ def getmodule(object):
if file in modulesbyfile: if file in modulesbyfile:
return sys.modules.get(modulesbyfile[file]) return sys.modules.get(modulesbyfile[file])
for module in sys.modules.values(): for module in sys.modules.values():
if hasattr(module, '__file__'): if ismodule(module) and hasattr(module, '__file__'):
modulesbyfile[ modulesbyfile[
os.path.realpath( os.path.realpath(
getabsfile(module))] = module.__name__ getabsfile(module))] = module.__name__
...@@ -406,7 +406,7 @@ def findsource(object): ...@@ -406,7 +406,7 @@ def findsource(object):
in the file and the line number indexes a line in that list. An IOError in the file and the line number indexes a line in that list. An IOError
is raised if the source code cannot be retrieved.""" is raised if the source code cannot be retrieved."""
file = getsourcefile(object) or getfile(object) file = getsourcefile(object) or getfile(object)
lines = linecache.getlines(file) lines = linecache.getlines(file, getmodule(object).__dict__)
if not lines: if not lines:
raise IOError('could not get source code') raise IOError('could not get source code')
......
...@@ -10,8 +10,8 @@ import os ...@@ -10,8 +10,8 @@ import os
__all__ = ["getline", "clearcache", "checkcache"] __all__ = ["getline", "clearcache", "checkcache"]
def getline(filename, lineno): def getline(filename, lineno, module_globals=None):
lines = getlines(filename) lines = getlines(filename, module_globals)
if 1 <= lineno <= len(lines): if 1 <= lineno <= len(lines):
return lines[lineno-1] return lines[lineno-1]
else: else:
...@@ -30,14 +30,14 @@ def clearcache(): ...@@ -30,14 +30,14 @@ def clearcache():
cache = {} cache = {}
def getlines(filename): def getlines(filename, module_globals=None):
"""Get the lines for a file from the cache. """Get the lines for a file from the cache.
Update the cache if it doesn't contain an entry for this file already.""" Update the cache if it doesn't contain an entry for this file already."""
if filename in cache: if filename in cache:
return cache[filename][2] return cache[filename][2]
else: else:
return updatecache(filename) return updatecache(filename,module_globals)
def checkcache(filename=None): def checkcache(filename=None):
...@@ -54,6 +54,8 @@ def checkcache(filename=None): ...@@ -54,6 +54,8 @@ def checkcache(filename=None):
for filename in filenames: for filename in filenames:
size, mtime, lines, fullname = cache[filename] size, mtime, lines, fullname = cache[filename]
if mtime is None:
continue # no-op for files loaded via a __loader__
try: try:
stat = os.stat(fullname) stat = os.stat(fullname)
except os.error: except os.error:
...@@ -63,7 +65,7 @@ def checkcache(filename=None): ...@@ -63,7 +65,7 @@ def checkcache(filename=None):
del cache[filename] del cache[filename]
def updatecache(filename): def updatecache(filename, module_globals=None):
"""Update a cache entry and return its list of lines. """Update a cache entry and return its list of lines.
If something's wrong, print a message, discard the cache entry, If something's wrong, print a message, discard the cache entry,
and return an empty list.""" and return an empty list."""
...@@ -72,12 +74,34 @@ def updatecache(filename): ...@@ -72,12 +74,34 @@ def updatecache(filename):
del cache[filename] del cache[filename]
if not filename or filename[0] + filename[-1] == '<>': if not filename or filename[0] + filename[-1] == '<>':
return [] return []
fullname = filename fullname = filename
try: try:
stat = os.stat(fullname) stat = os.stat(fullname)
except os.error, msg: except os.error, msg:
# Try looking through the module search path.
basename = os.path.split(filename)[1] basename = os.path.split(filename)[1]
# Try for a __loader__, if available
if module_globals and '__loader__' in module_globals:
name = module_globals.get('__name__')
loader = module_globals['__loader__']
get_source = getattr(loader, 'get_source' ,None)
if name and get_source:
if basename.startswith(name.split('.')[-1]+'.'):
try:
data = get_source(name)
except (ImportError,IOError):
pass
else:
cache[filename] = (
len(data), None,
[line+'\n' for line in data.splitlines()], fullname
)
return cache[filename][2]
# Try looking through the module search path.
for dirname in sys.path: for dirname in sys.path:
# When using imputil, sys.path may contain things other than # When using imputil, sys.path may contain things other than
# strings; ignore them when it happens. # strings; ignore them when it happens.
......
...@@ -69,6 +69,8 @@ def makepath(*paths): ...@@ -69,6 +69,8 @@ def makepath(*paths):
def abs__file__(): def abs__file__():
"""Set all module' __file__ attribute to an absolute path""" """Set all module' __file__ attribute to an absolute path"""
for m in sys.modules.values(): for m in sys.modules.values():
if hasattr(m,'__loader__'):
continue # don't mess with a PEP 302-supplied __file__
try: try:
m.__file__ = os.path.abspath(m.__file__) m.__file__ = os.path.abspath(m.__file__)
except AttributeError: except AttributeError:
......
...@@ -12,7 +12,12 @@ from test import test_support ...@@ -12,7 +12,12 @@ from test import test_support
from test.test_importhooks import ImportHooksBaseTestCase, test_src, test_co from test.test_importhooks import ImportHooksBaseTestCase, test_src, test_co
import zipimport import zipimport
import linecache
import doctest
import inspect
import StringIO
from traceback import extract_tb, extract_stack, print_tb
raise_src = 'def do_raise(): raise TypeError\n'
# so we only run testAFakeZlib once if this test is run repeatedly # so we only run testAFakeZlib once if this test is run repeatedly
# which happens when we look for ref leaks # which happens when we look for ref leaks
...@@ -54,7 +59,8 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase): ...@@ -54,7 +59,8 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
def setUp(self): def setUp(self):
# We're reusing the zip archive path, so we must clear the # We're reusing the zip archive path, so we must clear the
# cached directory info. # cached directory info and linecache
linecache.clearcache()
zipimport._zip_directory_cache.clear() zipimport._zip_directory_cache.clear()
ImportHooksBaseTestCase.setUp(self) ImportHooksBaseTestCase.setUp(self)
...@@ -83,6 +89,11 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase): ...@@ -83,6 +89,11 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
mod = __import__(".".join(modules), globals(), locals(), mod = __import__(".".join(modules), globals(), locals(),
["__dummy__"]) ["__dummy__"])
call = kw.get('call')
if call is not None:
call(mod)
if expected_ext: if expected_ext:
file = mod.get_file() file = mod.get_file()
self.assertEquals(file, os.path.join(TEMP_ZIP, self.assertEquals(file, os.path.join(TEMP_ZIP,
...@@ -249,6 +260,74 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase): ...@@ -249,6 +260,74 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
self.doTest(".py", files, TESTMOD, self.doTest(".py", files, TESTMOD,
stuff="Some Stuff"*31) stuff="Some Stuff"*31)
def assertModuleSource(self, module):
self.assertEqual(inspect.getsource(module), test_src)
def testGetSource(self):
files = {TESTMOD + ".py": (NOW, test_src)}
self.doTest(".py", files, TESTMOD, call=self.assertModuleSource)
def testGetCompiledSource(self):
pyc = make_pyc(compile(test_src, "<???>", "exec"), NOW)
files = {TESTMOD + ".py": (NOW, test_src),
TESTMOD + pyc_ext: (NOW, pyc)}
self.doTest(pyc_ext, files, TESTMOD, call=self.assertModuleSource)
def runDoctest(self, callback):
files = {TESTMOD + ".py": (NOW, test_src),
"xyz.txt": (NOW, ">>> log.append(True)\n")}
self.doTest(".py", files, TESTMOD, call=callback)
def doDoctestFile(self, module):
log = []
old_master, doctest.master = doctest.master, None
try:
doctest.testfile(
'xyz.txt', package=module, module_relative=True,
globs=locals()
)
finally:
doctest.master = old_master
self.assertEqual(log,[True])
def testDoctestFile(self):
self.runDoctest(self.doDoctestFile)
def doDoctestSuite(self, module):
log = []
doctest.DocFileTest(
'xyz.txt', package=module, module_relative=True,
globs=locals()
).run()
self.assertEqual(log,[True])
def testDoctestSuite(self):
self.runDoctest(self.doDoctestSuite)
def doTraceback(self, module):
try:
module.do_raise()
except:
tb = sys.exc_info()[2].tb_next
f,lno,n,line = extract_tb(tb, 1)[0]
self.assertEqual(line, raise_src.strip())
f,lno,n,line = extract_stack(tb.tb_frame, 1)[0]
self.assertEqual(line, raise_src.strip())
s = StringIO.StringIO()
print_tb(tb, 1, s)
self.failUnless(s.getvalue().endswith(raise_src))
else:
raise AssertionError("This ought to be impossible")
def testTraceback(self):
files = {TESTMOD + ".py": (NOW, raise_src)}
self.doTest(None, files, TESTMOD, call=self.doTraceback)
class CompressedZipImportTestCase(UncompressedZipImportTestCase): class CompressedZipImportTestCase(UncompressedZipImportTestCase):
compression = ZIP_DEFLATED compression = ZIP_DEFLATED
......
...@@ -66,7 +66,7 @@ def print_tb(tb, limit=None, file=None): ...@@ -66,7 +66,7 @@ def print_tb(tb, limit=None, file=None):
_print(file, _print(file,
' File "%s", line %d, in %s' % (filename,lineno,name)) ' File "%s", line %d, in %s' % (filename,lineno,name))
linecache.checkcache(filename) linecache.checkcache(filename)
line = linecache.getline(filename, lineno) line = linecache.getline(filename, lineno, f.f_globals)
if line: _print(file, ' ' + line.strip()) if line: _print(file, ' ' + line.strip())
tb = tb.tb_next tb = tb.tb_next
n = n+1 n = n+1
...@@ -98,7 +98,7 @@ def extract_tb(tb, limit = None): ...@@ -98,7 +98,7 @@ def extract_tb(tb, limit = None):
filename = co.co_filename filename = co.co_filename
name = co.co_name name = co.co_name
linecache.checkcache(filename) linecache.checkcache(filename)
line = linecache.getline(filename, lineno) line = linecache.getline(filename, lineno, f.f_globals)
if line: line = line.strip() if line: line = line.strip()
else: line = None else: line = None
list.append((filename, lineno, name, line)) list.append((filename, lineno, name, line))
...@@ -281,7 +281,7 @@ def extract_stack(f=None, limit = None): ...@@ -281,7 +281,7 @@ def extract_stack(f=None, limit = None):
filename = co.co_filename filename = co.co_filename
name = co.co_name name = co.co_name
linecache.checkcache(filename) linecache.checkcache(filename)
line = linecache.getline(filename, lineno) line = linecache.getline(filename, lineno, f.f_globals)
if line: line = line.strip() if line: line = line.strip()
else: line = None else: line = None
list.append((filename, lineno, name, line)) list.append((filename, lineno, name, line))
......
...@@ -58,10 +58,11 @@ def warn(message, category=None, stacklevel=1): ...@@ -58,10 +58,11 @@ def warn(message, category=None, stacklevel=1):
if not filename: if not filename:
filename = module filename = module
registry = globals.setdefault("__warningregistry__", {}) registry = globals.setdefault("__warningregistry__", {})
warn_explicit(message, category, filename, lineno, module, registry) warn_explicit(message, category, filename, lineno, module, registry,
globals)
def warn_explicit(message, category, filename, lineno, def warn_explicit(message, category, filename, lineno,
module=None, registry=None): module=None, registry=None, module_globals=None):
if module is None: if module is None:
module = filename or "<unknown>" module = filename or "<unknown>"
if module[-3:].lower() == ".py": if module[-3:].lower() == ".py":
...@@ -92,6 +93,11 @@ def warn_explicit(message, category, filename, lineno, ...@@ -92,6 +93,11 @@ def warn_explicit(message, category, filename, lineno,
if action == "ignore": if action == "ignore":
registry[key] = 1 registry[key] = 1
return return
# Prime the linecache for formatting, in case the
# "file" is actually in a zipfile or something.
linecache.getlines(filename, module_globals)
if action == "error": if action == "error":
raise message raise message
# Other actions # Other actions
......
...@@ -30,6 +30,10 @@ Extension Modules ...@@ -30,6 +30,10 @@ Extension Modules
Library Library
------- -------
- The warnings, linecache, inspect, traceback, site, and doctest modules
were updated to work correctly with modules imported from zipfiles or
via other PEP 302 __loader__ objects.
- Patch #1467770: Reduce usage of subprocess._active to processes which - Patch #1467770: Reduce usage of subprocess._active to processes which
the application hasn't waited on. the application hasn't waited on.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment