Coverage for /Users/Newville/Codes/xraylarch/larch/inputText.py: 80%

244 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-09 10:08 -0600

1#!/usr/bin/env python 

2# 

3# InputText for Larch 

4 

5from __future__ import print_function 

6import os 

7import sys 

8import time 

9from collections import deque 

10from copy import copy 

11import io 

12 

13from .utils import read_textfile 

14 

15OPENS = '{([' 

16CLOSES = '})]' 

17PARENS = dict(zip(OPENS, CLOSES)) 

18QUOTES = '\'"' 

19BSLASH = '\\' 

20COMMENT = '#' 

21DBSLASH = "%s%s" % (BSLASH, BSLASH) 

22 

23BLOCK_FRIENDS = {'if': ('else', 'elif'), 

24 'for': ('else',), 

25 'def': (), 

26 'try': ('else', 'except', 'finally'), 

27 'while': ('else',), 

28 None: ()} 

29 

30STARTKEYS = ['if', 'for', 'def', 'try', 'while'] 

31 

32 

33def find_eostring(txt, eos, istart): 

34 """find end of string token for a string""" 

35 while True: 

36 inext = txt[istart:].find(eos) 

37 if inext < 0: # reached end of text before match found 

38 return eos, len(txt) 

39 elif (txt[istart+inext-1] == BSLASH and 

40 txt[istart+inext-2] != BSLASH): # matched quote was escaped 

41 istart = istart+inext+len(eos) 

42 else: # real match found! skip ahead in string 

43 return '', istart+inext+len(eos)-1 

44 

45def is_complete(text): 

46 """returns whether a text of code is complete 

47 for strings quotes and open / close delimiters, 

48 including nested delimeters. 

49 """ 

50 itok = istart = 0 

51 eos = '' 

52 delims = [] 

53 while itok < len(text): 

54 c = text[itok] 

55 if c in QUOTES: 

56 eos = c 

57 if text[itok:itok+3] == c*3: 

58 eos = c*3 

59 istart = itok + len(eos) 

60 # leap ahead to matching quote, ignoring text within 

61 eos, itok = find_eostring(text, eos, istart) 

62 elif c in OPENS: 

63 delims.append(PARENS[c]) 

64 elif c in CLOSES and len(delims) > 0 and c == delims[-1]: 

65 delims.pop() 

66 elif c == COMMENT and eos == '': # comment char outside string 

67 jtok = itok 

68 if '\n' in text[itok:]: 

69 itok = itok + text[itok:].index('\n') 

70 else: 

71 itok = len(text) 

72 itok += 1 

73 return eos=='' and len(delims)==0 and not text.rstrip().endswith(BSLASH) 

74 

75def strip_comments(text, char='#'): 

76 """return text with end-of-line comments removed""" 

77 out = [] 

78 for line in text.split('\n'): 

79 if line.find(char) > 0: 

80 i = 0 

81 while i < len(line): 

82 tchar = line[i] 

83 if tchar == char: 

84 line = line[:i] 

85 break 

86 elif tchar in ('"',"'"): 

87 eos = line[i+1:].find(tchar) 

88 if eos > 0: 

89 i = i + eos 

90 i += 1 

91 out.append(line.rstrip()) 

92 return '\n'.join(out) 

93 

94def get_key(text): 

95 """return keyword: first word of text, 

96 isolating keywords followed by '(' and ':' """ 

97 t = text.replace('(', ' (').replace(':', ' :').strip() 

98 if len(t) == 0: 

99 return '' 

100 return t.split(None, 1)[0].strip() 

101 

102def block_start(text): 

103 """return whether a complete-extended-line of text 

104 starts with a block-starting keyword, one of 

105 ('if', 'for', 'try', 'while', 'def') 

106 """ 

107 txt = strip_comments(text) 

108 key = get_key(txt) 

109 if key in STARTKEYS and txt.endswith(':'): 

110 return key 

111 return False 

112 

113def block_end(text): 

114 """return whether a complete-extended-line of text 

115 starts wih block-ending keyword, 

116 '#end' + ('if', 'for', 'try', 'while', 'def') 

117 """ 

118 txt = text.strip() 

119 if txt.startswith('#end') or txt.startswith('end'): 

120 n = 3 

121 if txt.startswith('#end'): 

122 n = 4 

123 key = txt[n:].split(None, 1)[0].strip() 

124 if key in STARTKEYS: 

125 return key 

126 return False 

127 

128BLANK_TEXT = ('', '<incomplete input>', -1) 

129 

130 

131class HistoryBuffer(object): 

132 """ 

133 command history buffer 

134 """ 

135 def __init__(self, filename=None, maxlines=5000, title='larch history'): 

136 self.filename = filename 

137 self.maxlines = maxlines 

138 self.title = title 

139 self.session_start = 0 

140 self.buffer = [] 

141 if filename is not None: 

142 self.load(filename=filename) 

143 

144 def add(self, text=''): 

145 if len(text.strip()) > 0: 

146 self.buffer.append(text) 

147 

148 def clear(self): 

149 self.buffer = [] 

150 self.session_start = 0 

151 

152 def load(self, filename=None): 

153 if filename is not None: 

154 self.filename = filename 

155 if os.path.exists(self.filename): 

156 self.clear() 

157 text = read_textfile(filename).split('\n') 

158 for hline in text: 

159 if not hline.startswith("# larch history"): 

160 self.add(text=hline) 

161 self.session_start = len(self.buffer) 

162 

163 def get(self, session_only=False, trim_last=False, maxlines=None): 

164 if maxlines is None: 

165 maxlines = self.maxlines 

166 start_ = -maxlines 

167 if session_only: 

168 start_ = self.session_start 

169 end_ = None 

170 if trim_last: 

171 end_ = -1 

172 

173 comment = "# %s saved" % (self.title) 

174 out = ["%s %s" % (comment, time.ctime())] 

175 for bline in self.buffer[start_:end_]: 

176 if not (bline.startswith(comment) or len(bline) < 0): 

177 out.append(str(bline)) 

178 out.append('') 

179 return out 

180 

181 def save(self, filename=None, session_only=False, 

182 trim_last=False, maxlines=None): 

183 if filename is None: 

184 filename = self.filename 

185 out = self.get(session_only=session_only, 

186 trim_last=trim_last, 

187 maxlines=maxlines) 

188 out.append('') 

189 

190 with open(filename, 'w', encoding=sys.getdefaultencoding()) as fh: 

191 fh.write('\n'.join(out)) 

192 

193class InputText: 

194 """input text for larch, with history""" 

195 def __init__(self, _larch=None, historyfile=None, maxhistory=5000, 

196 prompt='larch> ',prompt2 = ".....> "): 

197 self.deque = deque() 

198 self.filename = '<stdin>' 

199 self.lineno = 0 

200 self.curline = 0 

201 self.curtext = '' 

202 self.blocks = [] 

203 self.buffer = [] 

204 self.larch = _larch 

205 self.prompt = prompt 

206 self.prompt2 = prompt2 

207 self.saved_text = BLANK_TEXT 

208 self.history = HistoryBuffer(filename=historyfile, 

209 maxlines=maxhistory) 

210 

211 def __len__(self): 

212 return len(self.deque) 

213 

214 def get(self): 

215 """get compile-able block of python code""" 

216 out = [] 

217 filename, linenumber = None, None 

218 if self.saved_text != BLANK_TEXT: 

219 txt, filename, lineno = self.saved_text 

220 out.append(txt) 

221 text, fn, ln, done = self.deque.popleft() 

222 out.append(text) 

223 if filename is None: 

224 filename = fn 

225 if linenumber is None: 

226 linenumber = ln 

227 

228 while not done: 

229 if len(self.deque) == 0: 

230 self.saved_text = ("\n".join(out), filename, linenumber) 

231 return BLANK_TEXT 

232 text, fn, ln, done = self.deque.popleft() 

233 out.append(text) 

234 self.saved_text = BLANK_TEXT 

235 return ("\n".join(out), filename, linenumber) 

236 

237 def clear(self): 

238 self.deque.clear() 

239 self.saved_text = BLANK_TEXT 

240 self.curtext = '' 

241 self.blocks = [] 

242 

243 def putfile(self, filename): 

244 """add the content of a file at the top of the stack 

245 that is, to be run next, as for run('myscript.lar') 

246 

247 Parameters 

248 ---------- 

249 filename : file object or string of filename 

250 

251 Returns 

252 ------- 

253 None on success, 

254 (exception, message) on failure 

255 """ 

256 

257 text = None 

258 try: 

259 text = read_textfile(filename) 

260 except: 

261 errtype, errmsg, errtb = sys.exc_info() 

262 return (errtype, errmsg) 

263 

264 if isinstance(filename, io.IOBase): 

265 filename = filename.name 

266 

267 if text is None: 

268 return (IOError, 'cannot read %s' % filename) 

269 

270 current = None 

271 if len(self.deque) > 0: 

272 current = copy(self.deque) 

273 self.deque.clear() 

274 self.put(text, filename=filename, lineno=0, add_history=False) 

275 

276 if current is not None: 

277 self.deque.extend(current) 

278 

279 def put(self, text, filename=None, lineno=None, add_history=True): 

280 """add a line of input code text""" 

281 if filename is not None: 

282 self.filename = filename 

283 if lineno is not None: 

284 self.lineno = lineno 

285 

286 if self.larch is not None: 

287 getsym = self.larch.symtable.get_symbol 

288 self.valid_commands = getsym('_sys.valid_commands', create=True) 

289 

290 if self.history is not None and add_history: 

291 self.history.add(text) 

292 

293 for txt in text.split('\n'): 

294 self.lineno += 1 

295 if len(self.curtext) == 0: 

296 self.curtext = txt 

297 self.curline = self.lineno 

298 else: 

299 self.curtext = "%s\n%s" % (self.curtext, txt) 

300 

301 blk_start = False 

302 if is_complete(self.curtext) and len(self.curtext)>0: 

303 blk_start = block_start(self.curtext) 

304 if blk_start: 

305 self.blocks.append((blk_start, self.lineno, txt)) 

306 else: 

307 blk_end = block_end(self.curtext) 

308 if (blk_end and len(self.blocks) > 0 and 

309 blk_end == self.blocks[-1][0]): 

310 self.blocks.pop() 

311 if self.curtext.strip().startswith('end'): 

312 nblank = self.curtext.find(self.curtext.strip()) 

313 self.curtext = '%s#%s' % (' '*nblank, 

314 self.curtext.strip()) 

315 

316 _delim = None 

317 if len(self.blocks) > 0: 

318 _delim = self.blocks[-1][0] 

319 

320 key = get_key(self.curtext) 

321 ilevel = len(self.blocks) 

322 if ilevel > 0 and (blk_start or 

323 key in BLOCK_FRIENDS[_delim]): 

324 ilevel = ilevel - 1 

325 

326 sindent = ' '*4*ilevel 

327 pytext = "%s%s" % (sindent, self.curtext.strip()) 

328 # look for valid commands 

329 if key in self.valid_commands and '\n' not in self.curtext: 

330 argtext = self.curtext.strip()[len(key):].strip() 

331 if not (argtext.startswith('(') and 

332 argtext.endswith(')') ): 

333 pytext = "%s%s(%s)" % (sindent, key, argtext) 

334 

335 self.deque.append((pytext, self.filename, 

336 self.curline, 0==len(self.blocks))) 

337 

338 self.curtext = '' 

339 

340 @property 

341 def complete(self): 

342 return len(self.curtext)==0 and len(self.blocks)==0 

343 

344 @property 

345 def next_prompt(self): 

346 if len(self.curtext)==0 and len(self.blocks)==0: 

347 return self.prompt 

348 return self.prompt2