Skip to content

Commit edd24f5

Browse files
author
nick.coghlan
committed
Fix several issues relating to access to source code inside zipfiles. Initial work by Alexander Belopolsky. See Misc/NEWS in this checkin for details.
git-svn-id: http://svn.python.org/projects/python/trunk@67750 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent 0d71ba5 commit edd24f5

10 files changed

Lines changed: 161 additions & 48 deletions

File tree

Lib/bdb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def format_stack_entry(self, frame_lineno, lprefix=': '):
347347
rv = frame.f_locals['__return__']
348348
s = s + '->'
349349
s = s + repr.repr(rv)
350-
line = linecache.getline(filename, lineno)
350+
line = linecache.getline(filename, lineno, frame.f_globals)
351351
if line: s = s + lprefix + line.strip()
352352
return s
353353

@@ -589,7 +589,7 @@ def user_line(self, frame):
589589
name = frame.f_code.co_name
590590
if not name: name = '???'
591591
fn = self.canonic(frame.f_code.co_filename)
592-
line = linecache.getline(fn, frame.f_lineno)
592+
line = linecache.getline(fn, frame.f_lineno, frame.f_globals)
593593
print '+++', fn, frame.f_lineno, name, ':', line.strip()
594594
def user_return(self, frame, retval):
595595
print '+++ return', retval

Lib/linecache.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,20 @@ def updatecache(filename, module_globals=None):
8888
get_source = getattr(loader, 'get_source', None)
8989

9090
if name and get_source:
91-
if basename.startswith(name.split('.')[-1]+'.'):
92-
try:
93-
data = get_source(name)
94-
except (ImportError, IOError):
95-
pass
96-
else:
97-
if data is None:
98-
# No luck, the PEP302 loader cannot find the source
99-
# for this module.
100-
return []
101-
cache[filename] = (
102-
len(data), None,
103-
[line+'\n' for line in data.splitlines()], fullname
104-
)
105-
return cache[filename][2]
91+
try:
92+
data = get_source(name)
93+
except (ImportError, IOError):
94+
pass
95+
else:
96+
if data is None:
97+
# No luck, the PEP302 loader cannot find the source
98+
# for this module.
99+
return []
100+
cache[filename] = (
101+
len(data), None,
102+
[line+'\n' for line in data.splitlines()], fullname
103+
)
104+
return cache[filename][2]
106105

107106
# Try looking through the module search path.
108107

Lib/pdb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def checkline(self, filename, lineno):
440440
Return `lineno` if it is, 0 if not (e.g. a docstring, comment, blank
441441
line or EOF). Warning: testing is not comprehensive.
442442
"""
443-
line = linecache.getline(filename, lineno)
443+
line = linecache.getline(filename, lineno, self.curframe.f_globals)
444444
if not line:
445445
print >>self.stdout, 'End of file'
446446
return 0
@@ -768,7 +768,7 @@ def do_list(self, arg):
768768
breaklist = self.get_file_breaks(filename)
769769
try:
770770
for lineno in range(first, last+1):
771-
line = linecache.getline(filename, lineno)
771+
line = linecache.getline(filename, lineno, self.curframe.f_globals)
772772
if not line:
773773
print >>self.stdout, '[EOF]'
774774
break

Lib/runpy.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,14 @@ def _run_module_code(code, init_globals=None,
6565

6666
# This helper is needed due to a missing component in the PEP 302
6767
# loader protocol (specifically, "get_filename" is non-standard)
68+
# Since we can't introduce new features in maintenance releases,
69+
# support was added to zipimporter under the name '_get_filename'
6870
def _get_filename(loader, mod_name):
69-
try:
70-
get_filename = loader.get_filename
71-
except AttributeError:
72-
return None
73-
else:
74-
return get_filename(mod_name)
71+
for attr in ("get_filename", "_get_filename"):
72+
meth = getattr(loader, attr, None)
73+
if meth is not None:
74+
return meth(mod_name)
75+
return None
7576

7677
# Helper to get the loader, code and filename for a module
7778
def _get_module_details(mod_name):

Lib/test/test_cmd_line_script.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,36 +75,66 @@ def _compile_test_script(script_name):
7575
compiled_name = script_name + 'o'
7676
return compiled_name
7777

78-
def _make_test_zip(zip_dir, zip_basename, script_name):
78+
def _make_test_zip(zip_dir, zip_basename, script_name, name_in_zip=None):
7979
zip_filename = zip_basename+os.extsep+'zip'
8080
zip_name = os.path.join(zip_dir, zip_filename)
8181
zip_file = zipfile.ZipFile(zip_name, 'w')
82-
zip_file.write(script_name, os.path.basename(script_name))
82+
if name_in_zip is None:
83+
name_in_zip = os.path.basename(script_name)
84+
zip_file.write(script_name, name_in_zip)
8385
zip_file.close()
84-
# if verbose:
86+
#if verbose:
8587
# zip_file = zipfile.ZipFile(zip_name, 'r')
8688
# print 'Contents of %r:' % zip_name
8789
# zip_file.printdir()
8890
# zip_file.close()
89-
return zip_name
91+
return zip_name, os.path.join(zip_name, name_in_zip)
9092

9193
def _make_test_pkg(pkg_dir):
9294
os.mkdir(pkg_dir)
9395
_make_test_script(pkg_dir, '__init__', '')
9496

97+
def _make_test_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,
98+
source=test_source, depth=1):
99+
init_name = _make_test_script(zip_dir, '__init__', '')
100+
init_basename = os.path.basename(init_name)
101+
script_name = _make_test_script(zip_dir, script_basename, source)
102+
pkg_names = [os.sep.join([pkg_name]*i) for i in range(1, depth+1)]
103+
script_name_in_zip = os.path.join(pkg_names[-1], os.path.basename(script_name))
104+
zip_filename = zip_basename+os.extsep+'zip'
105+
zip_name = os.path.join(zip_dir, zip_filename)
106+
zip_file = zipfile.ZipFile(zip_name, 'w')
107+
for name in pkg_names:
108+
init_name_in_zip = os.path.join(name, init_basename)
109+
zip_file.write(init_name, init_name_in_zip)
110+
zip_file.write(script_name, script_name_in_zip)
111+
zip_file.close()
112+
os.unlink(init_name)
113+
os.unlink(script_name)
114+
#if verbose:
115+
# zip_file = zipfile.ZipFile(zip_name, 'r')
116+
# print 'Contents of %r:' % zip_name
117+
# zip_file.printdir()
118+
# zip_file.close()
119+
return zip_name, os.path.join(zip_name, script_name_in_zip)
120+
95121
# There's no easy way to pass the script directory in to get
96122
# -m to work (avoiding that is the whole point of making
97123
# directories and zipfiles executable!)
98124
# So we fake it for testing purposes with a custom launch script
99125
launch_source = """\
100126
import sys, os.path, runpy
101-
sys.path[0:0] = os.path.dirname(__file__)
127+
sys.path.insert(0, %s)
102128
runpy._run_module_as_main(%r)
103129
"""
104130

105-
def _make_launch_script(script_dir, script_basename, module_name):
106-
return _make_test_script(script_dir, script_basename,
107-
launch_source % module_name)
131+
def _make_launch_script(script_dir, script_basename, module_name, path=None):
132+
if path is None:
133+
path = "os.path.dirname(__file__)"
134+
else:
135+
path = repr(path)
136+
source = launch_source % (path, module_name)
137+
return _make_test_script(script_dir, script_basename, source)
108138

109139
class CmdLineTest(unittest.TestCase):
110140
def _check_script(self, script_name, expected_file,
@@ -155,24 +185,35 @@ def test_directory_compiled(self):
155185
def test_zipfile(self):
156186
with temp_dir() as script_dir:
157187
script_name = _make_test_script(script_dir, '__main__')
158-
zip_name = _make_test_zip(script_dir, 'test_zip', script_name)
159-
self._check_script(zip_name, None, zip_name, '')
188+
zip_name, run_name = _make_test_zip(script_dir, 'test_zip', script_name)
189+
self._check_script(zip_name, run_name, zip_name, '')
160190

161191
def test_zipfile_compiled(self):
162192
with temp_dir() as script_dir:
163193
script_name = _make_test_script(script_dir, '__main__')
164194
compiled_name = _compile_test_script(script_name)
165-
zip_name = _make_test_zip(script_dir, 'test_zip', compiled_name)
166-
self._check_script(zip_name, None, zip_name, '')
195+
zip_name, run_name = _make_test_zip(script_dir, 'test_zip', compiled_name)
196+
self._check_script(zip_name, run_name, zip_name, '')
167197

168198
def test_module_in_package(self):
169199
with temp_dir() as script_dir:
170200
pkg_dir = os.path.join(script_dir, 'test_pkg')
171201
_make_test_pkg(pkg_dir)
172202
script_name = _make_test_script(pkg_dir, 'script')
173203
launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.script')
174-
self._check_script(launch_name, script_name,
175-
script_name, 'test_pkg')
204+
self._check_script(launch_name, script_name, script_name, 'test_pkg')
205+
206+
def test_module_in_package_in_zipfile(self):
207+
with temp_dir() as script_dir:
208+
zip_name, run_name = _make_test_zip_pkg(script_dir, 'test_zip', 'test_pkg', 'script')
209+
launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.script', zip_name)
210+
self._check_script(launch_name, run_name, run_name, 'test_pkg')
211+
212+
def test_module_in_subpackage_in_zipfile(self):
213+
with temp_dir() as script_dir:
214+
zip_name, run_name = _make_test_zip_pkg(script_dir, 'test_zip', 'test_pkg', 'script', depth=2)
215+
launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.test_pkg.script', zip_name)
216+
self._check_script(launch_name, run_name, run_name, 'test_pkg.test_pkg')
176217

177218

178219
def test_main():

Lib/test/test_doctest.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import doctest
77
import warnings
88

9+
# NOTE: There are some additional tests relating to interaction with
10+
# zipimport in the test_zipimport_support test module.
11+
912
######################################################################
1013
## Sample Objects (used by test cases)
1114
######################################################################
@@ -369,7 +372,7 @@ def test_DocTestFinder(): r"""
369372
>>> tests = finder.find(sample_func)
370373
371374
>>> print tests # doctest: +ELLIPSIS
372-
[<DocTest sample_func from ...:13 (1 example)>]
375+
[<DocTest sample_func from ...:16 (1 example)>]
373376
374377
The exact name depends on how test_doctest was invoked, so allow for
375378
leading path components.

Lib/test/test_inspect.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
# getclasstree, getargspec, getargvalues, formatargspec, formatargvalues,
1717
# currentframe, stack, trace, isdatadescriptor
1818

19+
# NOTE: There are some additional tests relating to interaction with
20+
# zipimport in the test_zipimport_support test module.
21+
1922
modfile = mod.__file__
2023
if modfile.endswith(('c', 'o')):
2124
modfile = modfile[:-1]

Lib/test/test_zipimport.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,16 +214,24 @@ def testZipImporterMethods(self):
214214
zi = zipimport.zipimporter(TEMP_ZIP)
215215
self.assertEquals(zi.archive, TEMP_ZIP)
216216
self.assertEquals(zi.is_package(TESTPACK), True)
217-
zi.load_module(TESTPACK)
217+
mod = zi.load_module(TESTPACK)
218+
self.assertEquals(zi._get_filename(TESTPACK), mod.__file__)
218219

219220
self.assertEquals(zi.is_package(packdir + '__init__'), False)
220221
self.assertEquals(zi.is_package(packdir + TESTPACK2), True)
221222
self.assertEquals(zi.is_package(packdir2 + TESTMOD), False)
222223

223-
mod_name = packdir2 + TESTMOD
224-
mod = __import__(module_path_to_dotted_name(mod_name))
224+
mod_path = packdir2 + TESTMOD
225+
mod_name = module_path_to_dotted_name(mod_path)
226+
pkg = __import__(mod_name)
227+
mod = sys.modules[mod_name]
225228
self.assertEquals(zi.get_source(TESTPACK), None)
226-
self.assertEquals(zi.get_source(mod_name), None)
229+
self.assertEquals(zi.get_source(mod_path), None)
230+
self.assertEquals(zi._get_filename(mod_path), mod.__file__)
231+
# To pass in the module name instead of the path, we must use the right importer
232+
loader = mod.__loader__
233+
self.assertEquals(loader.get_source(mod_name), None)
234+
self.assertEquals(loader._get_filename(mod_name), mod.__file__)
227235

228236
# test prefix and archivepath members
229237
zi2 = zipimport.zipimporter(TEMP_ZIP + os.sep + TESTPACK)
@@ -251,15 +259,23 @@ def testZipImporterMethodsInSubDirectory(self):
251259
self.assertEquals(zi.archive, TEMP_ZIP)
252260
self.assertEquals(zi.prefix, packdir)
253261
self.assertEquals(zi.is_package(TESTPACK2), True)
254-
zi.load_module(TESTPACK2)
262+
mod = zi.load_module(TESTPACK2)
263+
self.assertEquals(zi._get_filename(TESTPACK2), mod.__file__)
255264

256265
self.assertEquals(zi.is_package(TESTPACK2 + os.sep + '__init__'), False)
257266
self.assertEquals(zi.is_package(TESTPACK2 + os.sep + TESTMOD), False)
258267

259-
mod_name = TESTPACK2 + os.sep + TESTMOD
260-
mod = __import__(module_path_to_dotted_name(mod_name))
268+
mod_path = TESTPACK2 + os.sep + TESTMOD
269+
mod_name = module_path_to_dotted_name(mod_path)
270+
pkg = __import__(mod_name)
271+
mod = sys.modules[mod_name]
261272
self.assertEquals(zi.get_source(TESTPACK2), None)
262-
self.assertEquals(zi.get_source(mod_name), None)
273+
self.assertEquals(zi.get_source(mod_path), None)
274+
self.assertEquals(zi._get_filename(mod_path), mod.__file__)
275+
# To pass in the module name instead of the path, we must use the right importer
276+
loader = mod.__loader__
277+
self.assertEquals(loader.get_source(mod_name), None)
278+
self.assertEquals(loader._get_filename(mod_name), mod.__file__)
263279
finally:
264280
z.close()
265281
os.remove(TEMP_ZIP)

Misc/NEWS

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,25 @@ Core and Builtins
7474
Library
7575
-------
7676

77+
- Issue #4223: inspect.getsource() will now correctly display source code
78+
for packages loaded via zipimport (or any other conformant PEP 302
79+
loader). Original patch by Alexander Belopolsky.
80+
81+
- Issue #4201: pdb can now access and display source code loaded via
82+
zipimport (or any other conformant PEP 302 loader). Original patch by
83+
Alexander Belopolsky.
84+
85+
- Issue #4197: doctests in modules loaded via zipimport (or any other PEP
86+
302 conformant loader) will now work correctly in most cases (they
87+
are still subject to the constraints that exist for all code running
88+
from inside a module loaded via a PEP 302 loader and attempting to
89+
perform IO operations based on __file__). Original patch by
90+
Alexander Belopolsky.
91+
92+
- Issues #4082 and #4512: Add runpy support to zipimport in a manner that
93+
allows backporting to maintenance branches. Original patch by
94+
Alexander Belopolsky.
95+
7796
- Issue #4163: Use unicode-friendly word splitting in the textwrap functions
7897
when given an unicode string.
7998

Modules/zipimport.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,29 @@ zipimporter_load_module(PyObject *obj, PyObject *args)
369369
return NULL;
370370
}
371371

372+
/* Return a string matching __file__ for the named module */
373+
static PyObject *
374+
zipimporter_get_filename(PyObject *obj, PyObject *args)
375+
{
376+
ZipImporter *self = (ZipImporter *)obj;
377+
PyObject *code;
378+
char *fullname, *modpath;
379+
int ispackage;
380+
381+
if (!PyArg_ParseTuple(args, "s:zipimporter._get_filename",
382+
&fullname))
383+
return NULL;
384+
385+
/* Deciding the filename requires working out where the code
386+
would come from if the module was actually loaded */
387+
code = get_module_code(self, fullname, &ispackage, &modpath);
388+
if (code == NULL)
389+
return NULL;
390+
Py_DECREF(code); /* Only need the path info */
391+
392+
return PyString_FromString(modpath);
393+
}
394+
372395
/* Return a bool signifying whether the module is a package or not. */
373396
static PyObject *
374397
zipimporter_is_package(PyObject *obj, PyObject *args)
@@ -528,6 +551,12 @@ Return the source code for the specified module. Raise ZipImportError\n\
528551
is the module couldn't be found, return None if the archive does\n\
529552
contain the module, but has no source for it.");
530553

554+
555+
PyDoc_STRVAR(doc_get_filename,
556+
"_get_filename(fullname) -> filename string.\n\
557+
\n\
558+
Return the filename for the specified module.");
559+
531560
static PyMethodDef zipimporter_methods[] = {
532561
{"find_module", zipimporter_find_module, METH_VARARGS,
533562
doc_find_module},
@@ -539,6 +568,8 @@ static PyMethodDef zipimporter_methods[] = {
539568
doc_get_code},
540569
{"get_source", zipimporter_get_source, METH_VARARGS,
541570
doc_get_source},
571+
{"_get_filename", zipimporter_get_filename, METH_VARARGS,
572+
doc_get_filename},
542573
{"is_package", zipimporter_is_package, METH_VARARGS,
543574
doc_is_package},
544575
{NULL, NULL} /* sentinel */

0 commit comments

Comments
 (0)