From 5dbab966785c4943a726f66c780d3fe34a82b09e Mon Sep 17 00:00:00 2001 From: CPython Developers <> Date: Mon, 15 Aug 2022 02:58:35 +0900 Subject: [PATCH] Update linecache from CPython 3.10.6 --- Lib/linecache.py | 51 ++++++++++++++++------------- Lib/test/test_linecache.py | 66 +++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/Lib/linecache.py b/Lib/linecache.py index 805bc0bef..8f011b93a 100644 --- a/Lib/linecache.py +++ b/Lib/linecache.py @@ -13,17 +13,8 @@ except ImportError: import _dummy_os as os import tokenize -__all__ = ["getline", "clearcache", "checkcache"] +__all__ = ["getline", "clearcache", "checkcache", "lazycache"] -def getline(filename, lineno, module_globals=None): - lines = getlines(filename, module_globals) - if 1 <= lineno <= len(lines): - return lines[lineno-1] - else: - return '' - - -# The cache # The cache. Maps filenames to either a thunk which will provide source code, # or a tuple (size, mtime, lines, fullname) once loaded. @@ -32,9 +23,17 @@ cache = {} def clearcache(): """Clear the cache entirely.""" + cache.clear() - global cache - cache = {} + +def getline(filename, lineno, module_globals=None): + """Get a line for a Python source file from the cache. + Update the cache if it doesn't contain an entry for this file already.""" + + lines = getlines(filename, module_globals) + if 1 <= lineno <= len(lines): + return lines[lineno - 1] + return '' def getlines(filename, module_globals=None): @@ -59,11 +58,10 @@ def checkcache(filename=None): if filename is None: filenames = list(cache.keys()) + elif filename in cache: + filenames = [filename] else: - if filename in cache: - filenames = [filename] - else: - return + return for filename in filenames: entry = cache[filename] @@ -112,8 +110,10 @@ def updatecache(filename, module_globals=None): # for this module. return [] cache[filename] = ( - len(data), None, - [line+'\n' for line in data.splitlines()], fullname + len(data), + None, + [line + '\n' for line in data.splitlines()], + fullname ) return cache[filename][2] @@ -138,7 +138,7 @@ def updatecache(filename, module_globals=None): try: with tokenize.open(fullname) as fp: lines = fp.readlines() - except OSError: + except (OSError, UnicodeDecodeError, SyntaxError): return [] if lines and not lines[-1].endswith('\n'): lines[-1] += '\n' @@ -157,7 +157,7 @@ def lazycache(filename, module_globals): :return: True if a lazy load is registered in the cache, otherwise False. To register such a load a module loader with a - get_source method must be found, the filename must be a cachable + get_source method must be found, the filename must be a cacheable filename, and the filename must not be already cached. """ if filename in cache: @@ -168,9 +168,14 @@ def lazycache(filename, module_globals): if not filename or (filename.startswith('<') and filename.endswith('>')): return False # Try for a __loader__, if available - if module_globals and '__loader__' in module_globals: - name = module_globals.get('__name__') - loader = module_globals['__loader__'] + if module_globals and '__name__' in module_globals: + name = module_globals['__name__'] + if (loader := module_globals.get('__loader__')) is None: + if spec := module_globals.get('__spec__'): + try: + loader = spec.loader + except AttributeError: + pass get_source = getattr(loader, 'get_source', None) if name and get_source: diff --git a/Lib/test/test_linecache.py b/Lib/test/test_linecache.py index 9232e8a8e..706daf65b 100644 --- a/Lib/test/test_linecache.py +++ b/Lib/test/test_linecache.py @@ -73,12 +73,10 @@ class GetLineTestsBadData(TempFile): # file_byte_string = b'Bad data goes here' def test_getline(self): - self.assertRaises((SyntaxError, UnicodeDecodeError), - linecache.getline, self.file_name, 1) + self.assertEqual(linecache.getline(self.file_name, 1), '') def test_getlines(self): - self.assertRaises((SyntaxError, UnicodeDecodeError), - linecache.getlines, self.file_name) + self.assertEqual(linecache.getlines(self.file_name), []) class EmptyFile(GetLineTestsGoodData, unittest.TestCase): @@ -92,9 +90,11 @@ class SingleEmptyLine(GetLineTestsGoodData, unittest.TestCase): class GoodUnicode(GetLineTestsGoodData, unittest.TestCase): file_list = ['á\n', 'b\n', 'abcdef\n', 'ááááá\n'] +class BadUnicode_NoDeclaration(GetLineTestsBadData, unittest.TestCase): + file_byte_string = b'\n\x80abc' -class BadUnicode(GetLineTestsBadData, unittest.TestCase): - file_byte_string = b'\x80abc' +class BadUnicode_WithDeclaration(GetLineTestsBadData, unittest.TestCase): + file_byte_string = b'# coding=utf-8\n\x80abc' class LineCacheTests(unittest.TestCase): @@ -116,7 +116,7 @@ class LineCacheTests(unittest.TestCase): # Check module loading for entry in MODULES: filename = os.path.join(MODULE_PATH, entry) + '.py' - with open(filename) as file: + with open(filename, encoding='utf-8') as file: for index, line in enumerate(file): self.assertEqual(line, getline(filename, index + 1)) @@ -126,7 +126,7 @@ class LineCacheTests(unittest.TestCase): def test_no_ending_newline(self): self.addCleanup(os_helper.unlink, os_helper.TESTFN) - with open(os_helper.TESTFN, "w") as fp: + with open(os_helper.TESTFN, "w", encoding='utf-8') as fp: fp.write(SOURCE_3) lines = linecache.getlines(os_helper.TESTFN) self.assertEqual(lines, ["\n", "def f():\n", " return 3\n"]) @@ -153,18 +153,18 @@ class LineCacheTests(unittest.TestCase): # Create a source file and cache its contents source_name = os_helper.TESTFN + '.py' self.addCleanup(os_helper.unlink, source_name) - with open(source_name, 'w') as source: + with open(source_name, 'w', encoding='utf-8') as source: source.write(SOURCE_1) getline(source_name, 1) # Keep a copy of the old contents source_list = [] - with open(source_name) as source: + with open(source_name, encoding='utf-8') as source: for index, line in enumerate(source): self.assertEqual(line, getline(source_name, index + 1)) source_list.append(line) - with open(source_name, 'w') as source: + with open(source_name, 'w', encoding='utf-8') as source: source.write(SOURCE_2) # Try to update a bogus cache entry @@ -176,7 +176,7 @@ class LineCacheTests(unittest.TestCase): # Update the cache and check whether it matches the new source file linecache.checkcache(source_name) - with open(source_name) as source: + with open(source_name, encoding='utf-8') as source: for index, line in enumerate(source): self.assertEqual(line, getline(source_name, index + 1)) source_list.append(line) @@ -245,5 +245,47 @@ class LineCacheTests(unittest.TestCase): self.assertEqual(linecache.getlines(FILENAME), lines) +class LineCacheInvalidationTests(unittest.TestCase): + def setUp(self): + super().setUp() + linecache.clearcache() + self.deleted_file = os_helper.TESTFN + '.1' + self.modified_file = os_helper.TESTFN + '.2' + self.unchanged_file = os_helper.TESTFN + '.3' + + for fname in (self.deleted_file, + self.modified_file, + self.unchanged_file): + self.addCleanup(os_helper.unlink, fname) + with open(fname, 'w', encoding='utf-8') as source: + source.write(f'print("I am {fname}")') + + self.assertNotIn(fname, linecache.cache) + linecache.getlines(fname) + self.assertIn(fname, linecache.cache) + + os.remove(self.deleted_file) + with open(self.modified_file, 'w', encoding='utf-8') as source: + source.write('print("was modified")') + + def test_checkcache_for_deleted_file(self): + linecache.checkcache(self.deleted_file) + self.assertNotIn(self.deleted_file, linecache.cache) + self.assertIn(self.modified_file, linecache.cache) + self.assertIn(self.unchanged_file, linecache.cache) + + def test_checkcache_for_modified_file(self): + linecache.checkcache(self.modified_file) + self.assertIn(self.deleted_file, linecache.cache) + self.assertNotIn(self.modified_file, linecache.cache) + self.assertIn(self.unchanged_file, linecache.cache) + + def test_checkcache_with_no_parameter(self): + linecache.checkcache() + self.assertNotIn(self.deleted_file, linecache.cache) + self.assertNotIn(self.modified_file, linecache.cache) + self.assertIn(self.unchanged_file, linecache.cache) + + if __name__ == "__main__": unittest.main()