PageRenderTime 38ms CodeModel.GetById 8ms RepoModel.GetById 1ms app.codeStats 0ms

/py/_code/source.py

https://bitbucket.org/pwaller/pypy
Python | 356 lines | 307 code | 18 blank | 31 comment | 45 complexity | c4c491957042868dd141f812d839a2f7 MD5 | raw file
  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 is None:
  131. raise IndexError("no valid source range around line %d " % (lineno,))
  132. return start, end
  133. def getblockend(self, lineno):
  134. # XXX
  135. lines = [x + '\n' for x in self.lines[lineno:]]
  136. blocklines = inspect.getblock(lines)
  137. #print blocklines
  138. return lineno + len(blocklines) - 1
  139. def deindent(self, offset=None):
  140. """ return a new source object deindented by offset.
  141. If offset is None then guess an indentation offset from
  142. the first non-blank line. Subsequent lines which have a
  143. lower indentation offset will be copied verbatim as
  144. they are assumed to be part of multilines.
  145. """
  146. # XXX maybe use the tokenizer to properly handle multiline
  147. # strings etc.pp?
  148. newsource = Source()
  149. newsource.lines[:] = deindent(self.lines, offset)
  150. return newsource
  151. def isparseable(self, deindent=True):
  152. """ return True if source is parseable, heuristically
  153. deindenting it by default.
  154. """
  155. try:
  156. import parser
  157. except ImportError:
  158. syntax_checker = lambda x: compile(x, 'asd', 'exec')
  159. else:
  160. syntax_checker = parser.suite
  161. if deindent:
  162. source = str(self.deindent())
  163. else:
  164. source = str(self)
  165. try:
  166. #compile(source+'\n', "x", "exec")
  167. syntax_checker(source+'\n')
  168. except KeyboardInterrupt:
  169. raise
  170. except Exception:
  171. return False
  172. else:
  173. return True
  174. def __str__(self):
  175. return "\n".join(self.lines)
  176. def compile(self, filename=None, mode='exec',
  177. flag=generators.compiler_flag,
  178. dont_inherit=0, _genframe=None):
  179. """ return compiled code object. if filename is None
  180. invent an artificial filename which displays
  181. the source/line position of the caller frame.
  182. """
  183. if not filename or py.path.local(filename).check(file=0):
  184. if _genframe is None:
  185. _genframe = sys._getframe(1) # the caller
  186. fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno
  187. base = "<%d-codegen " % self._compilecounter
  188. self.__class__._compilecounter += 1
  189. if not filename:
  190. filename = base + '%s:%d>' % (fn, lineno)
  191. else:
  192. filename = base + '%r %s:%d>' % (filename, fn, lineno)
  193. source = "\n".join(self.lines) + '\n'
  194. try:
  195. co = cpy_compile(source, filename, mode, flag)
  196. except SyntaxError:
  197. ex = sys.exc_info()[1]
  198. # re-represent syntax errors from parsing python strings
  199. msglines = self.lines[:ex.lineno]
  200. if ex.offset:
  201. msglines.append(" "*ex.offset + '^')
  202. msglines.append("(code was compiled probably from here: %s)" % filename)
  203. newex = SyntaxError('\n'.join(msglines))
  204. newex.offset = ex.offset
  205. newex.lineno = ex.lineno
  206. newex.text = ex.text
  207. raise newex
  208. else:
  209. if flag & _AST_FLAG:
  210. return co
  211. lines = [(x + "\n") for x in self.lines]
  212. if sys.version_info[0] >= 3:
  213. # XXX py3's inspect.getsourcefile() checks for a module
  214. # and a pep302 __loader__ ... we don't have a module
  215. # at code compile-time so we need to fake it here
  216. m = ModuleType("_pycodecompile_pseudo_module")
  217. py.std.inspect.modulesbyfile[filename] = None
  218. py.std.sys.modules[None] = m
  219. m.__loader__ = 1
  220. py.std.linecache.cache[filename] = (1, None, lines, filename)
  221. return co
  222. #
  223. # public API shortcut functions
  224. #
  225. def compile_(source, filename=None, mode='exec', flags=
  226. generators.compiler_flag, dont_inherit=0):
  227. """ compile the given source to a raw code object,
  228. and maintain an internal cache which allows later
  229. retrieval of the source code for the code object
  230. and any recursively created code objects.
  231. """
  232. if _ast is not None and isinstance(source, _ast.AST):
  233. # XXX should Source support having AST?
  234. return cpy_compile(source, filename, mode, flags, dont_inherit)
  235. _genframe = sys._getframe(1) # the caller
  236. s = Source(source)
  237. co = s.compile(filename, mode, flags, _genframe=_genframe)
  238. return co
  239. def getfslineno(obj):
  240. """ Return source location (path, lineno) for the given object.
  241. If the source cannot be determined return ("", -1)
  242. """
  243. try:
  244. code = py.code.Code(obj)
  245. except TypeError:
  246. try:
  247. fn = (py.std.inspect.getsourcefile(obj) or
  248. py.std.inspect.getfile(obj))
  249. except TypeError:
  250. return "", -1
  251. fspath = fn and py.path.local(fn) or None
  252. lineno = -1
  253. if fspath:
  254. try:
  255. _, lineno = findsource(obj)
  256. except IOError:
  257. pass
  258. else:
  259. fspath = code.path
  260. lineno = code.firstlineno
  261. assert isinstance(lineno, int)
  262. return fspath, lineno
  263. #
  264. # helper functions
  265. #
  266. def findsource(obj):
  267. try:
  268. sourcelines, lineno = py.std.inspect.findsource(obj)
  269. except py.builtin._sysex:
  270. raise
  271. except:
  272. return None, -1
  273. source = Source()
  274. source.lines = [line.rstrip() for line in sourcelines]
  275. return source, lineno
  276. def getsource(obj, **kwargs):
  277. obj = py.code.getrawcode(obj)
  278. try:
  279. strsrc = inspect.getsource(obj)
  280. except IndentationError:
  281. strsrc = "\"Buggy python version consider upgrading, cannot get source\""
  282. assert isinstance(strsrc, str)
  283. return Source(strsrc, **kwargs)
  284. def deindent(lines, offset=None):
  285. if offset is None:
  286. for line in lines:
  287. line = line.expandtabs()
  288. s = line.lstrip()
  289. if s:
  290. offset = len(line)-len(s)
  291. break
  292. else:
  293. offset = 0
  294. if offset == 0:
  295. return list(lines)
  296. newlines = []
  297. def readline_generator(lines):
  298. for line in lines:
  299. yield line + '\n'
  300. while True:
  301. yield ''
  302. r = readline_generator(lines)
  303. try:
  304. readline = r.next
  305. except AttributeError:
  306. readline = r.__next__
  307. try:
  308. for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(readline):
  309. if sline > len(lines):
  310. break # End of input reached
  311. if sline > len(newlines):
  312. line = lines[sline - 1].expandtabs()
  313. if line.lstrip() and line[:offset].isspace():
  314. line = line[offset:] # Deindent
  315. newlines.append(line)
  316. for i in range(sline, eline):
  317. # Don't deindent continuing lines of
  318. # multiline tokens (i.e. multiline strings)
  319. newlines.append(lines[i])
  320. except (IndentationError, tokenize.TokenError):
  321. pass
  322. # Add any lines we didn't see. E.g. if an exception was raised.
  323. newlines.extend(lines[len(newlines):])
  324. return newlines