gh-157146: Let linecache read sources from zip archives on sys.path (… · python/cpython@1d6e7d5 · GitHub
Skip to content

Commit 1d6e7d5

Browse files
authored
gh-157146: Let linecache read sources from zip archives on sys.path (GH-157147)
linecache.getline() now works for a .zip archive on sys.path without the caller having to pass module_globals. Callers that only have a file name, such as pdb, warnings, and doctest, now get source. Reads of such files go through the get_data() method of the path entry finder registered for the archive in sys.path_importer_cache. Prior to this: os.stat() would fail on the archive-internal path, the loader lookup needed the globals, and the sys.path search would only handle relative names and assumed a filesystem rather than using an importer.
1 parent e2311cf commit 1d6e7d5

3 files changed

Lines changed: 165 additions & 8 deletions

File tree

Lib/linecache.py

Lines changed: 54 additions & 8 deletions

Lib/test/test_linecache.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
""" Tests for the linecache module """
22

3+
import importlib
34
import linecache
45
import unittest
56
import os.path
7+
import sys
68
import tempfile
79
import threading
810
import tokenize
11+
import zipfile
12+
import zipimport
913
from importlib.machinery import ModuleSpec
1014
from test import support
15+
from test.support import import_helper
1116
from test.support import os_helper
1217
from test.support import threading_helper
1318
from test.support.script_helper import assert_python_ok
@@ -356,6 +361,17 @@ def test_linecache_python_string(self):
356361
self.assertEqual(stdout, b'')
357362
self.assertEqual(stderr, b'')
358363

364+
def test_path_importer_cache_None(self):
365+
# sys.path_importer_cache is set to None while the interpreter is
366+
# shutting down, before objects with a __del__ that may end up here
367+
# are released.
368+
filename = os.path.abspath(os_helper.TESTFN + '.py')
369+
with support.swap_attr(sys, 'path_importer_cache', None):
370+
self.assertEqual(linecache.getlines(filename), [])
371+
self.assertEqual(linecache.getline(filename, 1), '')
372+
self.assertNotIn(filename, linecache.cache)
373+
374+
359375
class LineCacheInvalidationTests(unittest.TestCase):
360376
def setUp(self):
361377
super().setUp()
@@ -398,6 +414,98 @@ def test_checkcache_with_no_parameter(self):
398414
self.assertIn(self.unchanged_file, linecache.cache)
399415

400416

417+
class ZipArchiveTests(unittest.TestCase):
418+
"""Sources of modules imported from a zip archive on sys.path."""
419+
420+
MODULE_SOURCE = (
421+
'"""A module inside a zip archive."""\n'
422+
'\n'
423+
'def f():\n'
424+
' return "from the zip"\n'
425+
)
426+
PACKAGE_SOURCE = 'value = 42\n'
427+
LATIN1_SOURCE = (
428+
'# -*- coding: latin-1 -*-\n'
429+
'value = "caf\xe9"\n'
430+
)
431+
432+
def setUp(self):
433+
linecache.clearcache()
434+
self.addCleanup(linecache.clearcache)
435+
tmpdir = self.enterContext(os_helper.temp_dir())
436+
self.zip_name = os.path.join(tmpdir, 'sources.zip')
437+
with zipfile.ZipFile(self.zip_name, 'w') as zf:
438+
zf.writestr('zipmod.py', self.MODULE_SOURCE)
439+
zf.writestr('zippkg/__init__.py', self.PACKAGE_SOURCE)
440+
zf.writestr('ziplatin1.py', self.LATIN1_SOURCE.encode('latin-1'))
441+
self.enterContext(import_helper.DirsOnSysPath(self.zip_name))
442+
for name in 'zipmod', 'zippkg', 'ziplatin1':
443+
self.addCleanup(import_helper.unload, name)
444+
self.addCleanup(sys.path_importer_cache.pop, self.zip_name, None)
445+
self.addCleanup(zipimport._zip_directory_cache.pop,
446+
self.zip_name, None)
447+
self.zipmod = importlib.import_module('zipmod')
448+
449+
def test_getlines_without_module_globals(self):
450+
filename = self.zipmod.__file__
451+
self.assertEqual(filename, os.path.join(self.zip_name, 'zipmod.py'))
452+
self.assertFalse(os.path.exists(filename))
453+
lines = self.MODULE_SOURCE.splitlines(keepends=True)
454+
self.assertEqual(linecache.getlines(filename), lines)
455+
self.assertEqual(linecache.getline(filename, 4),
456+
' return "from the zip"\n')
457+
self.assertEqual(linecache.getline(filename, 5), '')
458+
code = self.zipmod.f.__code__
459+
self.assertEqual(code.co_filename, filename)
460+
self.assertEqual(linecache.getline(filename, code.co_firstlineno),
461+
'def f():\n')
462+
463+
def test_relative_archive_path(self):
464+
# A relative sys.path entry gives its modules a relative __file__.
465+
tmpdir, zip_base = os.path.split(self.zip_name)
466+
self.addCleanup(sys.path_importer_cache.pop, zip_base, None)
467+
self.addCleanup(zipimport._zip_directory_cache.pop, zip_base, None)
468+
sys.path.insert(0, zip_base)
469+
self.addCleanup(sys.path.remove, zip_base)
470+
with os_helper.change_cwd(tmpdir):
471+
zippkg = importlib.import_module('zippkg')
472+
self.assertEqual(zippkg.__file__,
473+
os.path.join(zip_base, 'zippkg', '__init__.py'))
474+
self.assertEqual(linecache.getlines(zippkg.__file__),
475+
['value = 42\n'])
476+
477+
def test_package(self):
478+
zippkg = importlib.import_module('zippkg')
479+
self.assertEqual(linecache.getlines(zippkg.__file__),
480+
['value = 42\n'])
481+
482+
def test_encoding_declaration(self):
483+
ziplatin1 = importlib.import_module('ziplatin1')
484+
self.assertEqual(linecache.getlines(ziplatin1.__file__),
485+
self.LATIN1_SOURCE.splitlines(keepends=True))
486+
487+
def test_missing_file(self):
488+
filename = os.path.join(self.zip_name, 'missing.py')
489+
self.assertEqual(linecache.getlines(filename), [])
490+
self.assertEqual(linecache.getline(filename, 1), '')
491+
self.assertNotIn(filename, linecache.cache)
492+
493+
def test_checkcache_and_clearcache(self):
494+
filename = self.zipmod.__file__
495+
lines = linecache.getlines(filename)
496+
self.assertIn(filename, linecache.cache)
497+
# A file inside an archive has no mtime of its own, so checkcache()
498+
# keeps the entry, as it does for entries loaded through a loader.
499+
self.assertIsNone(linecache.cache[filename][1])
500+
linecache.checkcache(filename)
501+
linecache.checkcache()
502+
self.assertIn(filename, linecache.cache)
503+
self.assertEqual(linecache.getlines(filename), lines)
504+
linecache.clearcache()
505+
self.assertNotIn(filename, linecache.cache)
506+
self.assertEqual(linecache.getlines(filename), lines)
507+
508+
401509
class MultiThreadingTest(unittest.TestCase):
402510
@threading_helper.reap_threads
403511
@threading_helper.requires_working_threading()
Lines changed: 3 additions & 0 deletions

0 commit comments

Comments
 (0)