PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/py/_code/source.py

https://bitbucket.org/kcr/pypy
Python | 358 lines | 309 code | 18 blank | 31 comment | 45 complexity | 9c3407414b7acbfae197b8a9a42a89a6 MD5 | raw file
Possible License(s): Apache-2.0
  1. from __future__ import generators
  2. import sys
  3. import inspect, tokenize
  4. import py
  5. from types import ModuleType
  6. cpy_compile = compile
  7. try:
  8. import _ast
  9. from _ast import PyCF_ONLY_AST as _AST_FLAG
  10. except ImportError:
  11. _AST_FLAG = 0
  12. _ast = None
  13. class Source(object):
  14. """ a immutable object holding a source code fragment,
  15. possibly deindenting it.
  16. """
  17. _compilecounter = 0
  18. def __init__(self, *parts, **kwargs):
  19. self.lines = lines = []
  20. de = kwargs.get('deindent', True)
  21. rstrip = kwargs.get('rstrip', True)
  22. for part in parts:
  23. if not part:
  24. partlines = []
  25. if isinstance(part, Source):
  26. partlines = part.lines
  27. elif isinstance(part, (tuple, list)):
  28. partlines = [x.rstrip("\n") for x in part]
  29. elif isinstance(part, py.builtin._basestring):
  30. partlines = part.split('\n')
  31. if rstrip:
  32. while partlines:
  33. if partlines[-1].strip():
  34. break
  35. partlines.pop()
  36. else:
  37. partlines = getsource(part, deindent=de).lines
  38. if de:
  39. partlines = deindent(partlines)
  40. lines.extend(partlines)
  41. def __eq__(self, other):
  42. try:
  43. return self.lines == other.lines
  44. except AttributeError:
  45. if isinstance(other, str):
  46. return str(self) == other
  47. return False
  48. def __getitem__(self, key):
  49. if isinstance(key, int):
  50. return self.lines[key]
  51. else:
  52. if key.step not in (None, 1):
  53. raise IndexError("cannot slice a Source with a step")
  54. return self.__getslice__(key.start, key.stop)
  55. def __len__(self):
  56. return len(self.lines)
  57. def __getslice__(self, start, end):
  58. newsource = Source()
  59. newsource.lines = self.lines[start:end]
  60. return newsource
  61. def strip(self):
  62. """ return new source object with trailing
  63. and leading blank lines removed.
  64. """
  65. start, end = 0, len(self)
  66. while start < end and not self.lines[start].strip():
  67. start += 1
  68. while end > start and not self.lines[end-1].strip():
  69. end -= 1
  70. source = Source()
  71. source.lines[:] = self.lines[start:end]
  72. return source
  73. def putaround(self, before='', after='', indent=' ' * 4):
  74. """ return a copy of the source object with
  75. 'before' and 'after' wrapped around it.
  76. """
  77. before = Source(before)
  78. after = Source(after)
  79. newsource = Source()
  80. lines = [ (indent + line) for line in self.lines]
  81. newsource.lines = before.lines + lines + after.lines
  82. return newsource
  83. def indent(self, indent=' ' * 4):
  84. """ return a copy of the source object with
  85. all lines indented by the given indent-string.
  86. """
  87. newsource = Source()
  88. newsource.lines = [(indent+line) for line in self.lines]
  89. return newsource
  90. def getstatement(self, lineno, assertion=False):
  91. """ return Source statement which contains the
  92. given linenumber (counted from 0).
  93. """
  94. start, end = self.getstatementrange(lineno, assertion)
  95. return self[start:end]
  96. def getstatementrange(self, lineno, assertion=False):
  97. """ return (start, end) tuple which spans the minimal
  98. statement region which containing the given lineno.
  99. raise an IndexError if no such statementrange can be found.
  100. """
  101. # XXX there must be a better than these heuristic ways ...
  102. # XXX there may even be better heuristics :-)
  103. if not (0 <= lineno < len(self)):
  104. raise IndexError("lineno out of range")
  105. # 1. find the start of the statement
  106. from codeop import compile_command
  107. end = None
  108. for start in range(lineno, -1, -1):
  109. if assertion:
  110. line = self.lines[start]
  111. # the following lines are not fully tested, change with care
  112. if 'super' in line and 'self' in line and '__init__' in line:
  113. raise IndexError("likely a subclass")
  114. if "assert" not in line and "raise" not in line:
  115. continue
  116. trylines = self.lines[start:lineno+1]
  117. # quick hack to indent the source and get it as a string in one go
  118. trylines.insert(0, 'if xxx:')
  119. trysource = '\n '.join(trylines)
  120. # ^ space here
  121. try:
  122. compile_command(trysource)
  123. except (SyntaxError, OverflowError, ValueError):
  124. continue
  125. # 2. find the end of the statement
  126. for end in range(lineno+1, len(self)+1):
  127. trysource = self[start:end]
  128. if trysource.isparseable():
  129. return start, end
  130. if end == start + 100: # XXX otherwise, it takes forever
  131. break # XXX
  132. if end is None:
  133. raise IndexError("no valid source range around line %d " % (lineno,))
  134. return start, end
  135. def getblockend(self, lineno):
  136. # XXX
  137. lines = [x + '\n' for x in self.lines[lineno:]]
  138. blocklines = inspect.getblock(lines)
  139. #print blocklines
  140. return lineno + len(blocklines) - 1
  141. def deindent(self, offset=None):
  142. """ return a new source object deindented by offset.
  143. If offset is None then guess an indentation offset from
  144. the first non-blank line. Subsequent lines which have a
  145. lower indentation offset will be copied verbatim as
  146. they are assumed to be part of multilines.
  147. """
  148. # XXX maybe use the tokenizer to properly handle multiline
  149. # strings etc.pp?
  150. newsource = Source()
  151. newsource.lines[:] = deindent(self.lines, offset)
  152. return newsource
  153. def isparseable(self, deindent=True):
  154. """ return True if source is parseable, heuristically
  155. deindenting it by default.
  156. """
  157. try:
  158. import parser
  159. except ImportError:
  160. syntax_checker = lambda x: compile(x, 'asd', 'exec')
  161. else:
  162. syntax_checker = parser.suite
  163. if deindent:
  164. source = str(self.deindent())
  165. else:
  166. source = str(self)
  167. try:
  168. #compile(source+'\n', "x", "exec")
  169. syntax_checker(source+'\n')
  170. except KeyboardInterrupt:
  171. raise
  172. except Exception:
  173. return False
  174. else:
  175. return True
  176. def __str__(self):
  177. return "\n".join(self.lines)
  178. def compile(self, filename=None, mode='exec',
  179. flag=generators.compiler_flag,
  180. dont_inherit=0, _genframe=None):
  181. """ return compiled code object. if filename is None
  182. invent an artificial filename which displays
  183. the source/line position of the caller frame.
  184. """
  185. if not filename or py.path.local(filename).check(file=0):
  186. if _genframe is None:
  187. _genframe = sys._getframe(1) # the caller
  188. fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno
  189. base = "<%d-codegen " % self._compilecounter
  190. self.__class__._compilecounter += 1
  191. if not filename:
  192. filename = base + '%s:%d>' % (fn, lineno)
  193. else:
  194. filename = base + '%r %s:%d>' % (filename, fn, lineno)
  195. source = "\n".join(self.lines) + '\n'
  196. try:
  197. co = cpy_compile(source, filename, mode, flag)
  198. except SyntaxError:
  199. ex = sys.exc_info()[1]
  200. # re-represent syntax errors from parsing python strings
  201. msglines = self.lines[:ex.lineno]
  202. if ex.offset:
  203. msglines.append(" "*ex.offset + '^')
  204. msglines.append("(code was compiled probably from here: %s)" % filename)
  205. newex = SyntaxError('\n'.join(msglines))
  206. newex.offset = ex.offset
  207. newex.lineno = ex.lineno
  208. newex.text = ex.text
  209. raise newex
  210. else:
  211. if flag & _AST_FLAG:
  212. return co
  213. lines = [(x + "\n") for x in self.lines]
  214. if sys.version_info[0] >= 3:
  215. # XXX py3's inspect.getsourcefile() checks for a module
  216. # and a pep302 __loader__ ... we don't have a module
  217. # at code compile-time so we need to fake it here
  218. m = ModuleType("_pycodecompile_pseudo_module")
  219. py.std.inspect.modulesbyfile[filename] = None
  220. py.std.sys.modules[None] = m
  221. m.__loader__ = 1
  222. py.std.linecache.cache[filename] = (1, None, lines, filename)
  223. return co
  224. #
  225. # public API shortcut functions
  226. #
  227. def compile_(source, filename=None, mode='exec', flags=
  228. generators.compiler_flag, dont_inherit=0):
  229. """ compile the given source to a raw code object,
  230. and maintain an internal cache which allows later
  231. retrieval of the source code for the code object
  232. and any recursively created code objects.
  233. """
  234. if _ast is not None and isinstance(source, _ast.AST):
  235. # XXX should Source support having AST?
  236. return cpy_compile(source, filename, mode, flags, dont_inherit)
  237. _genframe = sys._getframe(1) # the caller
  238. s = Source(source)
  239. co = s.compile(filename, mode, flags, _genframe=_genframe)
  240. return co
  241. def getfslineno(obj):
  242. """ Return source location (path, lineno) for the given object.
  243. If the source cannot be determined return ("", -1)
  244. """
  245. try:
  246. code = py.code.Code(obj)
  247. except TypeError:
  248. try:
  249. fn = (py.std.inspect.getsourcefile(obj) or
  250. py.std.inspect.getfile(obj))
  251. except TypeError:
  252. return "", -1
  253. fspath = fn and py.path.local(fn) or None
  254. lineno = -1
  255. if fspath:
  256. try:
  257. _, lineno = findsource(obj)
  258. except IOError:
  259. pass
  260. else:
  261. fspath = code.path
  262. lineno = code.firstlineno
  263. assert isinstance(lineno, int)
  264. return fspath, lineno
  265. #
  266. # helper functions
  267. #
  268. def findsource(obj):
  269. try:
  270. sourcelines, lineno = py.std.inspect.findsource(obj)
  271. except py.builtin._sysex:
  272. raise
  273. except:
  274. return None, -1
  275. source = Source()
  276. source.lines = [line.rstrip() for line in sourcelines]
  277. return source, lineno
  278. def getsource(obj, **kwargs):
  279. obj = py.code.getrawcode(obj)
  280. try:
  281. strsrc = inspect.getsource(obj)
  282. except IndentationError:
  283. strsrc = "\"Buggy python version consider upgrading, cannot get source\""
  284. assert isinstance(strsrc, str)
  285. return Source(strsrc, **kwargs)
  286. def deindent(lines, offset=None):
  287. if offset is None:
  288. for line in lines:
  289. line = line.expandtabs()
  290. s = line.lstrip()
  291. if s:
  292. offset = len(line)-len(s)
  293. break
  294. else:
  295. offset = 0
  296. if offset == 0:
  297. return list(lines)
  298. newlines = []
  299. def readline_generator(lines):
  300. for line in lines:
  301. yield line + '\n'
  302. while True:
  303. yield ''
  304. r = readline_generator(lines)
  305. try:
  306. readline = r.next
  307. except AttributeError:
  308. readline = r.__next__
  309. try:
  310. for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(readline):
  311. if sline > len(lines):
  312. break # End of input reached
  313. if sline > len(newlines):
  314. line = lines[sline - 1].expandtabs()
  315. if line.lstrip() and line[:offset].isspace():
  316. line = line[offset:] # Deindent
  317. newlines.append(line)
  318. for i in range(sline, eline):
  319. # Don't deindent continuing lines of
  320. # multiline tokens (i.e. multiline strings)
  321. newlines.append(lines[i])
  322. except (IndentationError, tokenize.TokenError):
  323. pass
  324. # Add any lines we didn't see. E.g. if an exception was raised.
  325. newlines.extend(lines[len(newlines):])
  326. return newlines