Coverage for /pythoncovmergedfiles/medio/medio/src/py/py/_code/source.py: 35%
278 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:15 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:15 +0000
1from __future__ import generators
3from bisect import bisect_right
4import sys
5import inspect, tokenize
6import py
7from types import ModuleType
8cpy_compile = compile
10try:
11 import _ast
12 from _ast import PyCF_ONLY_AST as _AST_FLAG
13except ImportError:
14 _AST_FLAG = 0
15 _ast = None
18class Source(object):
19 """ a immutable object holding a source code fragment,
20 possibly deindenting it.
21 """
22 _compilecounter = 0
23 def __init__(self, *parts, **kwargs):
24 self.lines = lines = []
25 de = kwargs.get('deindent', True)
26 rstrip = kwargs.get('rstrip', True)
27 for part in parts:
28 if not part:
29 partlines = []
30 if isinstance(part, Source):
31 partlines = part.lines
32 elif isinstance(part, (tuple, list)):
33 partlines = [x.rstrip("\n") for x in part]
34 elif isinstance(part, py.builtin._basestring):
35 partlines = part.split('\n')
36 if rstrip:
37 while partlines:
38 if partlines[-1].strip():
39 break
40 partlines.pop()
41 else:
42 partlines = getsource(part, deindent=de).lines
43 if de:
44 partlines = deindent(partlines)
45 lines.extend(partlines)
47 def __eq__(self, other):
48 try:
49 return self.lines == other.lines
50 except AttributeError:
51 if isinstance(other, str):
52 return str(self) == other
53 return False
55 def __getitem__(self, key):
56 if isinstance(key, int):
57 return self.lines[key]
58 else:
59 if key.step not in (None, 1):
60 raise IndexError("cannot slice a Source with a step")
61 return self.__getslice__(key.start, key.stop)
63 def __len__(self):
64 return len(self.lines)
66 def __getslice__(self, start, end):
67 newsource = Source()
68 newsource.lines = self.lines[start:end]
69 return newsource
71 def strip(self):
72 """ return new source object with trailing
73 and leading blank lines removed.
74 """
75 start, end = 0, len(self)
76 while start < end and not self.lines[start].strip():
77 start += 1
78 while end > start and not self.lines[end-1].strip():
79 end -= 1
80 source = Source()
81 source.lines[:] = self.lines[start:end]
82 return source
84 def putaround(self, before='', after='', indent=' ' * 4):
85 """ return a copy of the source object with
86 'before' and 'after' wrapped around it.
87 """
88 before = Source(before)
89 after = Source(after)
90 newsource = Source()
91 lines = [ (indent + line) for line in self.lines]
92 newsource.lines = before.lines + lines + after.lines
93 return newsource
95 def indent(self, indent=' ' * 4):
96 """ return a copy of the source object with
97 all lines indented by the given indent-string.
98 """
99 newsource = Source()
100 newsource.lines = [(indent+line) for line in self.lines]
101 return newsource
103 def getstatement(self, lineno, assertion=False):
104 """ return Source statement which contains the
105 given linenumber (counted from 0).
106 """
107 start, end = self.getstatementrange(lineno, assertion)
108 return self[start:end]
110 def getstatementrange(self, lineno, assertion=False):
111 """ return (start, end) tuple which spans the minimal
112 statement region which containing the given lineno.
113 """
114 if not (0 <= lineno < len(self)):
115 raise IndexError("lineno out of range")
116 ast, start, end = getstatementrange_ast(lineno, self)
117 return start, end
119 def deindent(self, offset=None):
120 """ return a new source object deindented by offset.
121 If offset is None then guess an indentation offset from
122 the first non-blank line. Subsequent lines which have a
123 lower indentation offset will be copied verbatim as
124 they are assumed to be part of multilines.
125 """
126 # XXX maybe use the tokenizer to properly handle multiline
127 # strings etc.pp?
128 newsource = Source()
129 newsource.lines[:] = deindent(self.lines, offset)
130 return newsource
132 def isparseable(self, deindent=True):
133 """ return True if source is parseable, heuristically
134 deindenting it by default.
135 """
136 try:
137 import parser
138 except ImportError:
139 syntax_checker = lambda x: compile(x, 'asd', 'exec')
140 else:
141 syntax_checker = parser.suite
143 if deindent:
144 source = str(self.deindent())
145 else:
146 source = str(self)
147 try:
148 #compile(source+'\n', "x", "exec")
149 syntax_checker(source+'\n')
150 except KeyboardInterrupt:
151 raise
152 except Exception:
153 return False
154 else:
155 return True
157 def __str__(self):
158 return "\n".join(self.lines)
160 def compile(self, filename=None, mode='exec',
161 flag=generators.compiler_flag,
162 dont_inherit=0, _genframe=None):
163 """ return compiled code object. if filename is None
164 invent an artificial filename which displays
165 the source/line position of the caller frame.
166 """
167 if not filename or py.path.local(filename).check(file=0):
168 if _genframe is None:
169 _genframe = sys._getframe(1) # the caller
170 fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno
171 base = "<%d-codegen " % self._compilecounter
172 self.__class__._compilecounter += 1
173 if not filename:
174 filename = base + '%s:%d>' % (fn, lineno)
175 else:
176 filename = base + '%r %s:%d>' % (filename, fn, lineno)
177 source = "\n".join(self.lines) + '\n'
178 try:
179 co = cpy_compile(source, filename, mode, flag)
180 except SyntaxError:
181 ex = sys.exc_info()[1]
182 # re-represent syntax errors from parsing python strings
183 msglines = self.lines[:ex.lineno]
184 if ex.offset:
185 msglines.append(" "*ex.offset + '^')
186 msglines.append("(code was compiled probably from here: %s)" % filename)
187 newex = SyntaxError('\n'.join(msglines))
188 newex.offset = ex.offset
189 newex.lineno = ex.lineno
190 newex.text = ex.text
191 raise newex
192 else:
193 if flag & _AST_FLAG:
194 return co
195 lines = [(x + "\n") for x in self.lines]
196 import linecache
197 linecache.cache[filename] = (1, None, lines, filename)
198 return co
200#
201# public API shortcut functions
202#
204def compile_(source, filename=None, mode='exec', flags=
205 generators.compiler_flag, dont_inherit=0):
206 """ compile the given source to a raw code object,
207 and maintain an internal cache which allows later
208 retrieval of the source code for the code object
209 and any recursively created code objects.
210 """
211 if _ast is not None and isinstance(source, _ast.AST):
212 # XXX should Source support having AST?
213 return cpy_compile(source, filename, mode, flags, dont_inherit)
214 _genframe = sys._getframe(1) # the caller
215 s = Source(source)
216 co = s.compile(filename, mode, flags, _genframe=_genframe)
217 return co
220def getfslineno(obj):
221 """ Return source location (path, lineno) for the given object.
222 If the source cannot be determined return ("", -1)
223 """
224 try:
225 code = py.code.Code(obj)
226 except TypeError:
227 try:
228 fn = (inspect.getsourcefile(obj) or
229 inspect.getfile(obj))
230 except TypeError:
231 return "", -1
233 fspath = fn and py.path.local(fn) or None
234 lineno = -1
235 if fspath:
236 try:
237 _, lineno = findsource(obj)
238 except IOError:
239 pass
240 else:
241 fspath = code.path
242 lineno = code.firstlineno
243 assert isinstance(lineno, int)
244 return fspath, lineno
246#
247# helper functions
248#
250def findsource(obj):
251 try:
252 sourcelines, lineno = inspect.findsource(obj)
253 except py.builtin._sysex:
254 raise
255 except:
256 return None, -1
257 source = Source()
258 source.lines = [line.rstrip() for line in sourcelines]
259 return source, lineno
261def getsource(obj, **kwargs):
262 obj = py.code.getrawcode(obj)
263 try:
264 strsrc = inspect.getsource(obj)
265 except IndentationError:
266 strsrc = "\"Buggy python version consider upgrading, cannot get source\""
267 assert isinstance(strsrc, str)
268 return Source(strsrc, **kwargs)
270def deindent(lines, offset=None):
271 if offset is None:
272 for line in lines:
273 line = line.expandtabs()
274 s = line.lstrip()
275 if s:
276 offset = len(line)-len(s)
277 break
278 else:
279 offset = 0
280 if offset == 0:
281 return list(lines)
282 newlines = []
283 def readline_generator(lines):
284 for line in lines:
285 yield line + '\n'
286 while True:
287 yield ''
289 it = readline_generator(lines)
291 try:
292 for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)):
293 if sline > len(lines):
294 break # End of input reached
295 if sline > len(newlines):
296 line = lines[sline - 1].expandtabs()
297 if line.lstrip() and line[:offset].isspace():
298 line = line[offset:] # Deindent
299 newlines.append(line)
301 for i in range(sline, eline):
302 # Don't deindent continuing lines of
303 # multiline tokens (i.e. multiline strings)
304 newlines.append(lines[i])
305 except (IndentationError, tokenize.TokenError):
306 pass
307 # Add any lines we didn't see. E.g. if an exception was raised.
308 newlines.extend(lines[len(newlines):])
309 return newlines
312def get_statement_startend2(lineno, node):
313 import ast
314 # flatten all statements and except handlers into one lineno-list
315 # AST's line numbers start indexing at 1
316 l = []
317 for x in ast.walk(node):
318 if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
319 l.append(x.lineno - 1)
320 for name in "finalbody", "orelse":
321 val = getattr(x, name, None)
322 if val:
323 # treat the finally/orelse part as its own statement
324 l.append(val[0].lineno - 1 - 1)
325 l.sort()
326 insert_index = bisect_right(l, lineno)
327 start = l[insert_index - 1]
328 if insert_index >= len(l):
329 end = None
330 else:
331 end = l[insert_index]
332 return start, end
335def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
336 if astnode is None:
337 content = str(source)
338 try:
339 astnode = compile(content, "source", "exec", 1024) # 1024 for AST
340 except ValueError:
341 start, end = getstatementrange_old(lineno, source, assertion)
342 return None, start, end
343 start, end = get_statement_startend2(lineno, astnode)
344 # we need to correct the end:
345 # - ast-parsing strips comments
346 # - there might be empty lines
347 # - we might have lesser indented code blocks at the end
348 if end is None:
349 end = len(source.lines)
351 if end > start + 1:
352 # make sure we don't span differently indented code blocks
353 # by using the BlockFinder helper used which inspect.getsource() uses itself
354 block_finder = inspect.BlockFinder()
355 # if we start with an indented line, put blockfinder to "started" mode
356 block_finder.started = source.lines[start][0].isspace()
357 it = ((x + "\n") for x in source.lines[start:end])
358 try:
359 for tok in tokenize.generate_tokens(lambda: next(it)):
360 block_finder.tokeneater(*tok)
361 except (inspect.EndOfBlock, IndentationError):
362 end = block_finder.last + start
363 except Exception:
364 pass
366 # the end might still point to a comment or empty line, correct it
367 while end:
368 line = source.lines[end - 1].lstrip()
369 if line.startswith("#") or not line:
370 end -= 1
371 else:
372 break
373 return astnode, start, end
376def getstatementrange_old(lineno, source, assertion=False):
377 """ return (start, end) tuple which spans the minimal
378 statement region which containing the given lineno.
379 raise an IndexError if no such statementrange can be found.
380 """
381 # XXX this logic is only used on python2.4 and below
382 # 1. find the start of the statement
383 from codeop import compile_command
384 for start in range(lineno, -1, -1):
385 if assertion:
386 line = source.lines[start]
387 # the following lines are not fully tested, change with care
388 if 'super' in line and 'self' in line and '__init__' in line:
389 raise IndexError("likely a subclass")
390 if "assert" not in line and "raise" not in line:
391 continue
392 trylines = source.lines[start:lineno+1]
393 # quick hack to prepare parsing an indented line with
394 # compile_command() (which errors on "return" outside defs)
395 trylines.insert(0, 'def xxx():')
396 trysource = '\n '.join(trylines)
397 # ^ space here
398 try:
399 compile_command(trysource)
400 except (SyntaxError, OverflowError, ValueError):
401 continue
403 # 2. find the end of the statement
404 for end in range(lineno+1, len(source)+1):
405 trysource = source[start:end]
406 if trysource.isparseable():
407 return start, end
408 raise SyntaxError("no valid source range around line %d " % (lineno,))