commit 72547acfd5d8e54c7d51443341c303213eb25e8d Author: Robert Collins Date: Wed Jan 14 14:24:15 2015 +1300 Issue #17911: Make it possible to defer loading lines. diff --git a/Doc/library/linecache.rst b/Doc/library/linecache.rst index dacf8aa..49bf90c 100644 --- a/Doc/library/linecache.rst +++ b/Doc/library/linecache.rst @@ -43,6 +43,12 @@ The :mod:`linecache` module defines the following functions: changed on disk, and you require the updated version. If *filename* is omitted, it will check all the entries in the cache. +.. function:: deferredcache(filename, module_globals) + + Capture enough detail about a non-file based module to permit getting its + lines later via :func:`getline` even if *module_globals* is None in the later + call. This permits avoiding doing I/O until a line is actually needed, + without having to carry the module globals around indefinitely. Example:: diff --git a/Lib/linecache.py b/Lib/linecache.py index 02a9eb5..82467d6 100644 --- a/Lib/linecache.py +++ b/Lib/linecache.py @@ -5,6 +5,7 @@ is not found, it will look down the module search path for a file by that name. """ +import functools import sys import os import tokenize @@ -36,6 +37,9 @@ def getlines(filename, module_globals=None): Update the cache if it doesn't contain an entry for this file already.""" if filename in cache: + entry = cache[filename] + if len(entry) == 1: + return updatecache(filename, module_globals) return cache[filename][2] else: return updatecache(filename, module_globals) @@ -54,7 +58,11 @@ def checkcache(filename=None): return for filename in filenames: - size, mtime, lines, fullname = cache[filename] + entry = cache[filename] + if len(entry) == 1: + # deferred cache entry, leave it deferred. + continue + size, mtime, lines, fullname = entry if mtime is None: continue # no-op for files loaded via a __loader__ try: @@ -72,7 +80,8 @@ def updatecache(filename, module_globals=None): and return an empty list.""" if filename in cache: - del cache[filename] + if len(cache[filename]) != 1: + del cache[filename] if not filename or (filename.startswith('<') and filename.endswith('>')): return [] @@ -82,27 +91,24 @@ def updatecache(filename, module_globals=None): except OSError: basename = filename - # 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: - try: - data = get_source(name) - except (ImportError, OSError): - pass - else: - if data is None: - # No luck, the PEP302 loader cannot find the source - # for this module. - return [] - cache[filename] = ( - len(data), None, - [line+'\n' for line in data.splitlines()], fullname - ) - return cache[filename][2] + # Realise a deferred loader based lookup if there is one + # otherwise try to lookup right now. + if ((filename in cache and len(cache[filename]) == 1) or + deferredcache(filename, module_globals)): + try: + data = cache[filename][0]() + except (ImportError, OSError): + pass + else: + if data is None: + # No luck, the PEP302 loader cannot find the source + # for this module. + return [] + cache[filename] = ( + len(data), None, + [line+'\n' for line in data.splitlines()], fullname + ) + return cache[filename][2] # Try looking through the module search path, which is only useful # when handling a relative filename. @@ -132,3 +138,25 @@ def updatecache(filename, module_globals=None): size, mtime = stat.st_size, stat.st_mtime cache[filename] = size, mtime, lines, fullname return lines + + +def deferredcache(filename, module_globals): + """Seed the cache for filename with module_globals. + + The module loader will be asked for the source only when getlines is + called, not immediately. + + :return: True if a module loader with a get_source method was found, + otherwise False. + """ + # 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: + get_lines = functools.partial(get_source, name) + cache[filename] = (get_lines,) + return True + return False diff --git a/Lib/test/test_linecache.py b/Lib/test/test_linecache.py index 5fe0554..b162dcb 100644 --- a/Lib/test/test_linecache.py +++ b/Lib/test/test_linecache.py @@ -7,6 +7,7 @@ from test import support FILENAME = linecache.__file__ +NONEXISTANT_FILENAME = FILENAME + '.missing' INVALID_NAME = '!@$)(!@#_1' EMPTY = '' TESTS = 'inspect_fodder inspect_fodder2 mapping_tests' @@ -126,6 +127,34 @@ class LineCacheTests(unittest.TestCase): self.assertEqual(line, getline(source_name, index + 1)) source_list.append(line) + def test_deferredcache_no_globals(self): + lines = linecache.getlines(FILENAME) + linecache.clearcache() + self.assertEqual(False, linecache.deferredcache(FILENAME, None)) + self.assertEqual(lines, linecache.getlines(FILENAME)) + + def test_deferredcache_smoke(self): + lines = linecache.getlines(NONEXISTANT_FILENAME, globals()) + linecache.clearcache() + self.assertEqual( + True, linecache.deferredcache(NONEXISTANT_FILENAME, globals())) + self.assertEqual(1, len(linecache.cache[NONEXISTANT_FILENAME])) + # Note here that we're looking up a non existant filename with no + # globals: this would error if the deferred value wasn't resolved. + self.assertEqual(lines, linecache.getlines(NONEXISTANT_FILENAME)) + + def test_deferredcache_update(self): + lines = linecache.getlines(NONEXISTANT_FILENAME) + linecache.clearcache() + linecache.deferredcache(NONEXISTANT_FILENAME, globals()) + self.assertEqual(lines, linecache.updatecache(NONEXISTANT_FILENAME)) + + def test_deferredcache_check(self): + lines = linecache.clearcache() + linecache.deferredcache(NONEXISTANT_FILENAME, globals()) + linecache.checkcache() + + def test_main(): support.run_unittest(LineCacheTests) diff --git a/Misc/NEWS b/Misc/NEWS index ec2e74a..372c0d5 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -203,6 +203,9 @@ Core and Builtins Library ------- +- Issue #17911: Provide a way to seed the linecache for a PEP-302 module + without actually loading the code. + - Issue #19777: Provide a home() classmethod on Path objects. Contributed by Victor Salgado and Mayank Tripathi.