55that name.
66"""
77
8- import functools
9- import sys
10- import os
11- import tokenize
12-
138__all__ = ["getline" , "clearcache" , "checkcache" , "lazycache" ]
149
1510
1611# The cache. Maps filenames to either a thunk which will provide source code,
1712# or a tuple (size, mtime, lines, fullname) once loaded.
1813cache = {}
14+ _interactive_cache = {}
1915
2016
2117def clearcache ():
@@ -49,28 +45,54 @@ def getlines(filename, module_globals=None):
4945 return []
5046
5147
48+ def _getline_from_code (filename , lineno ):
49+ lines = _getlines_from_code (filename )
50+ if 1 <= lineno <= len (lines ):
51+ return lines [lineno - 1 ]
52+ return ''
53+
54+ def _make_key (code ):
55+ return (code .co_filename , code .co_qualname , code .co_firstlineno )
56+
57+ def _getlines_from_code (code ):
58+ code_id = _make_key (code )
59+ if code_id in _interactive_cache :
60+ entry = _interactive_cache [code_id ]
61+ if len (entry ) != 1 :
62+ return _interactive_cache [code_id ][2 ]
63+ return []
64+
65+
5266def checkcache (filename = None ):
5367 """Discard cache entries that are out of date.
5468 (This is not checked upon each call!)"""
5569
5670 if filename is None :
57- filenames = list (cache .keys ())
58- elif filename in cache :
59- filenames = [filename ]
71+ # get keys atomically
72+ filenames = cache .copy ().keys ()
6073 else :
61- return
74+ filenames = [ filename ]
6275
6376 for filename in filenames :
64- entry = cache [filename ]
77+ try :
78+ entry = cache [filename ]
79+ except KeyError :
80+ continue
81+
6582 if len (entry ) == 1 :
6683 # lazy cache entry, leave it lazy.
6784 continue
6885 size , mtime , lines , fullname = entry
6986 if mtime is None :
7087 continue # no-op for files loaded via a __loader__
88+ try :
89+ # This import can fail if the interpreter is shutting down
90+ import os
91+ except ImportError :
92+ return
7193 try :
7294 stat = os .stat (fullname )
73- except OSError :
95+ except ( OSError , ValueError ) :
7496 cache .pop (filename , None )
7597 continue
7698 if size != stat .st_size or mtime != stat .st_mtime :
@@ -82,6 +104,17 @@ def updatecache(filename, module_globals=None):
82104 If something's wrong, print a message, discard the cache entry,
83105 and return an empty list."""
84106
107+ # These imports are not at top level because linecache is in the critical
108+ # path of the interpreter startup and importing os and sys take a lot of time
109+ # and slows down the startup sequence.
110+ try :
111+ import os
112+ import sys
113+ import tokenize
114+ except ImportError :
115+ # These import can fail if the interpreter is shutting down
116+ return []
117+
85118 if filename in cache :
86119 if len (cache [filename ]) != 1 :
87120 cache .pop (filename , None )
@@ -128,16 +161,20 @@ def updatecache(filename, module_globals=None):
128161 try :
129162 stat = os .stat (fullname )
130163 break
131- except OSError :
164+ except ( OSError , ValueError ) :
132165 pass
133166 else :
134167 return []
168+ except ValueError : # may be raised by os.stat()
169+ return []
135170 try :
136171 with tokenize .open (fullname ) as fp :
137172 lines = fp .readlines ()
138173 except (OSError , UnicodeDecodeError , SyntaxError ):
139174 return []
140- if lines and not lines [- 1 ].endswith ('\n ' ):
175+ if not lines :
176+ lines = ['\n ' ]
177+ elif not lines [- 1 ].endswith ('\n ' ):
141178 lines [- 1 ] += '\n '
142179 size , mtime = stat .st_size , stat .st_mtime
143180 cache [filename ] = size , mtime , lines , fullname
@@ -166,17 +203,29 @@ def lazycache(filename, module_globals):
166203 return False
167204 # Try for a __loader__, if available
168205 if module_globals and '__name__' in module_globals :
169- name = module_globals ['__name__' ]
170- if (loader := module_globals .get ('__loader__' )) is None :
171- if spec := module_globals .get ('__spec__' ):
172- try :
173- loader = spec .loader
174- except AttributeError :
175- pass
206+ spec = module_globals .get ('__spec__' )
207+ name = getattr (spec , 'name' , None ) or module_globals ['__name__' ]
208+ loader = getattr (spec , 'loader' , None )
209+ if loader is None :
210+ loader = module_globals .get ('__loader__' )
176211 get_source = getattr (loader , 'get_source' , None )
177212
178213 if name and get_source :
179- get_lines = functools .partial (get_source , name )
214+ def get_lines (name = name , * args , ** kwargs ):
215+ return get_source (name , * args , ** kwargs )
180216 cache [filename ] = (get_lines ,)
181217 return True
182218 return False
219+
220+ def _register_code (code , string , name ):
221+ entry = (len (string ),
222+ None ,
223+ [line + '\n ' for line in string .splitlines ()],
224+ name )
225+ stack = [code ]
226+ while stack :
227+ code = stack .pop ()
228+ for const in code .co_consts :
229+ if isinstance (const , type (code )):
230+ stack .append (const )
231+ _interactive_cache [_make_key (code )] = entry
0 commit comments