Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief @see cl FrameFunction 

5""" 

6import sys 

7import os 

8import inspect 

9import threading 

10import tkinter 

11import tkinter.font as tkFont 

12from io import StringIO 

13from pyquickhelper.loghelper.flog import fLOG, GetLogFile 

14from .tk_window import create_tk 

15from .function_helper import has_unknown_parameters, extract_function_information, private_adjust_parameters, private_get_function 

16from .storing_functions import _private_restore, _private_store, interpret_parameter 

17 

18 

19class FrameFunction(tkinter.Frame): 

20 """ 

21 Creating a Frame window for a function. 

22 It will create an entry control for every parameter. 

23 If one of the parameter is 'password', the window will show only stars. 

24 The windows proposes to store the value and to restore them on the 

25 next call. This functionality is disable when 'password' is present 

26 in the list of parameters. 

27 """ 

28 

29 def __init__(self, parent, function, restore=True, width=100, raise_exception=False, 

30 overwrite=None, hide=False, command_leave=None, key_save="e"): 

31 """ 

32 @param parent window parent 

33 @param function function object (can be a string) 

34 @param restore if True, check if existing saved parameters are present 

35 @param width number of characters in every Entry field 

36 @param raise_exception raise an exception instead of catching it 

37 @param overwrite parameters to overwrite 

38 @param hide if True, hide the window after clicking on OK 

39 @param command_leave if not None, this function will be called when clicking on Cancel or Leave 

40 @param key_save suffix to add to the filename used to store parameters 

41 """ 

42 if overwrite is None: 

43 overwrite = {} 

44 

45 tkinter.Frame.__init__(self, parent) 

46 self.fdoc = tkinter.Frame(self) 

47 self.fpar = tkinter.Frame(self) 

48 self.flog = tkinter.Frame(self) 

49 self.fbut = tkinter.Frame(self) 

50 self.fpar.pack() 

51 self.fbut.pack() 

52 self.flog.pack() 

53 self.fdoc.pack() 

54 self.restore = restore 

55 self.parent = parent 

56 self.input = {} 

57 self.types = {} 

58 self.hide = hide 

59 self.raise_exception = raise_exception 

60 self._added = {} 

61 self.command_leave = command_leave 

62 self._suffix = key_save 

63 

64 # retieve previous answers 

65 self._history = [] 

66 self._hpos = -1 

67 

68 if isinstance(function, str): 

69 if self.hide: 

70 fLOG(__file__, function) # , OutputPrint = False) 

71 else: 

72 fLOG(__file__, function, # , OutputPrint = False, 

73 LogFile=StringIO()) 

74 

75 function = private_get_function(function) 

76 self.function = function 

77 self.info = extract_function_information(function) 

78 

79 else: 

80 self.function = function 

81 self.info = extract_function_information(function) 

82 

83 if self.hide: 

84 fLOG(__file__, self.info["name"], # , OutputPrint = False, 

85 LogFile=StringIO()) 

86 else: 

87 fLOG(__file__, self.info["name"], # , OutputPrint = False, 

88 LogFile=StringIO()) 

89 

90 if restore: 

91 self._history = _private_restore( 

92 self.info["name"] + "." + self._suffix) 

93 if len(self._history) > 0: 

94 self.info["param"].update(self._history[-1]) 

95 self._hpos = len(self._history) - 1 

96 

97 for k in self.info["param"]: 

98 self.types[k] = self.info["types"][k] 

99 if self.types[k] in [None, None.__class__]: 

100 self.types[k] = str 

101 

102 tlab = tkinter.Label(self.fdoc, text="Help") 

103 tlab.pack(side=tkinter.LEFT) 

104 lab = tkinter.Text(self.fdoc, width=width, height=7) 

105 lab.pack(side=tkinter.LEFT) 

106 lab.insert("0.0", self.info["help"]) 

107 

108 objs = [lab, tlab] 

109 

110 scroll = tkinter.Scrollbar(self.fdoc) 

111 scroll.pack(side=tkinter.RIGHT, fill=tkinter.Y) 

112 scroll.config(command=lab.yview, width=5) 

113 lab.config(yscrollcommand=scroll.set) 

114 

115 self.fdoc.bind('<Return>', self.run_function) 

116 self.fdoc.bind('<Escape>', self.run_cancel) 

117 

118 # overwrite parameters 

119 if overwrite is not None: 

120 for k in self.info["param"]: 

121 if k in overwrite: 

122 self.info["param"][k] = overwrite[k] 

123 

124 # optional parameters in **params 

125 has = has_unknown_parameters(function) 

126 if has and overwrite is not None: 

127 add = [] 

128 for a, b in overwrite.items(): 

129 if a not in self.info["param"]: 

130 add.append((a, b)) 

131 for a, b in add: 

132 self.info["param"][a] = b 

133 self.types[a] = type(b) 

134 

135 sig = inspect.signature(function) 

136 params = list(_ for _ in sig.parameters.keys() if _ not in ('fLOG',)) 

137 if len(params) == 0: 

138 raise ValueError( 

139 "Unable to extract any parameter from function '{0}'.".format(function)) 

140 if params == ['args']: 

141 raise ValueError("Extracted weird parameters from function '{0}' (only {1})\nSIG={2}.".format( 

142 function, params, sig)) 

143 self._added = [_ for _ in self.info["param"] if _ not in params] 

144 

145 self.fpar.bind('<Return>', self.run_function) 

146 self.fpar.bind('<Escape>', self.run_cancel) 

147 

148 # next 

149 line = 0 

150 for k in sorted(self.info["param"]): 

151 if k in self._added: 

152 continue 

153 lab = tkinter.Label(self.fpar, text=k) 

154 lab.grid(row=line, column=0) 

155 

156 if k in ["password", "password1", "password2", "password3"]: 

157 lab = tkinter.Entry(self.fpar, width=width, show="*") 

158 else: 

159 lab = tkinter.Entry(self.fpar, width=width) 

160 

161 lab.grid(row=line, column=1) 

162 if self.info["param"][k] is not None: 

163 lab.insert("0", str(self.info["param"][k])) 

164 self.input[k] = lab 

165 objs.append(lab) 

166 line += 1 

167 

168 # optional 

169 for k in sorted(self.info["param"]): 

170 if k not in self._added: 

171 continue 

172 lab = tkinter.Label(self.fpar, text=k) 

173 lab.grid(row=line, column=0) 

174 

175 if k in ["password", "password1", "password2", "password3"]: 

176 lab = tkinter.Entry(self.fpar, width=width, show="*") 

177 else: 

178 lab = tkinter.Entry(self.fpar, width=width) 

179 

180 lab.grid(row=line, column=1) 

181 if self.info["param"][k] is not None: 

182 lab.insert("0", str(self.info["param"][k])) 

183 self.input[k] = lab 

184 objs.append(lab) 

185 line += 1 

186 

187 # next 

188 tex = tkinter.Text(self.flog, width=int(width * 3 / 2), height=15) 

189 tex.pack() 

190 self.LOG = tex 

191 tex.config(font=tkFont.Font(family="Courrier New", size=8)) 

192 

193 self.cancel = tkinter.Button(self.fbut, text="cancel or leave") 

194 self.run = tkinter.Button(self.fbut, text=" run ") 

195 self.cancel.pack(side=tkinter.LEFT) 

196 self.run.pack(side=tkinter.LEFT) 

197 

198 self.cancel.config(command=self.run_cancel) 

199 self.run.config(command=self.run_function) 

200 private_adjust_parameters(self.info["param"]) 

201 self._already = False 

202 

203 # up, down 

204 self.bup = tkinter.Button(self.fbut, text="up") 

205 self.bdown = tkinter.Button(self.fbut, text="down") 

206 self.bup.pack(side=tkinter.LEFT) 

207 self.bdown.pack(side=tkinter.LEFT) 

208 self.bup.config(command=self.history_up) 

209 self.bdown.config(command=self.history_down) 

210 

211 # keys 

212 for obj in objs + [tex, parent, self, self.bup, 

213 self.bdown, self.run, self.cancel, self.fdoc]: 

214 obj.bind("<Up>", self.history_up) 

215 obj.bind("<Down>", self.history_down) 

216 obj.bind("<Return>", self.run_function) 

217 obj.bind("<Escape>", self.run_cancel) 

218 

219 def update(self): 

220 """ 

221 Updates the parameters (ie ``self.info``). 

222 """ 

223 for k in self.input: # pylint: disable=C0206 

224 self.input[k].delete(0, tkinter.END) 

225 self.input[k].insert("0", str(self.info["param"].get(k, ""))) 

226 

227 def history_up(self, *args): 

228 """ 

229 Looks back in the history of used parameters and 

230 change the parameters. 

231 """ 

232 if len(self._history) > 0: 

233 self._hpos = (self._hpos + 1) % len(self._history) 

234 self.info["param"].update(self._history[self._hpos]) 

235 self.update() 

236 

237 def history_down(self, *args): 

238 """ 

239 Looks forward in the history of used parameters 

240 and change the parameters. 

241 """ 

242 if len(self._history) > 0: 

243 self._hpos = ( 

244 self._hpos + len(self._history) - 1) % len(self._history) 

245 self.info["param"].update(self._history[self._hpos]) 

246 self.update() 

247 

248 def stop_thread(self): 

249 """ 

250 Stops the function execution. 

251 """ 

252 if "thread_started" in self.__dict__: 

253 for th in self.thread_started: 

254 if th.is_alive(): 

255 if not th.daemon: 

256 raise Exception( 

257 "the thread is not daemon, this case should not happen") 

258 th._stop() 

259 

260 def destroy(self): 

261 """ 

262 Stops the thread and destroy the function. 

263 The behaviour of method 

264 `Thread._stop <http://hg.python.org/cpython/file/3.4/Lib/threading.py>`_ 

265 changed in Python 3.4, 

266 See the `discussion <https://groups.google.com/forum/#!topic/comp.lang.python/sXXwTh9EHsI>`_. 

267 """ 

268 self.stop_thread() 

269 if self.command_leave is not None: 

270 f = self.command_leave 

271 self.command_leave = None 

272 f() 

273 else: 

274 try: 

275 tkinter.Frame.destroy(self) 

276 except Exception as e: 

277 if "application has been destroyed" in str(e): 

278 os._exit(0) 

279 

280 def run_cancel(self, *args): 

281 """ 

282 cancel 

283 """ 

284 if self.command_leave is not None: 

285 f = self.command_leave 

286 self.command_leave = None 

287 f() 

288 else: 

289 self.parent.destroy() 

290 

291 def get_parameters(self): 

292 """ 

293 Returns the parameters in a dictionary. 

294 

295 @return dictionary 

296 """ 

297 res = {} 

298 for k, v in self.input.items(): 

299 s = v.get() 

300 s = s.strip() 

301 if len(s) == 0: 

302 s = None 

303 ty = self.types[k] 

304 res[k] = interpret_parameter(ty, s) 

305 return res 

306 

307 def get_title(self): 

308 """ 

309 @return self.info ["name"] 

310 """ 

311 return self.info["name"] 

312 

313 def refresh(self): 

314 """ 

315 Refreshes the screen. 

316 """ 

317 temp = self.LOG.get("0.0", "end") 

318 log = GetLogFile().getvalue() 

319 log = log[len(temp):] 

320 

321 try: 

322 self.LOG.insert("end", log) 

323 except Exception: 

324 self.LOG.insert("end", "\n".join(repr(log).split("\n"))) 

325 self.LOG.see("end") 

326 

327 if self._already: 

328 self.after(1000, self.refresh) 

329 else: 

330 self.run.config(state=tkinter.NORMAL) 

331 if self.hide: 

332 self.parent.destroy() 

333 

334 def run_function(self, *args): 

335 """ 

336 Runs the function. 

337 """ 

338 if self.hide: 

339 self.parent.withdraw() 

340 else: 

341 self.run.config(state=tkinter.DISABLED) 

342 self._already = True 

343 

344 res = self.get_parameters() 

345 if self.restore: 

346 _private_store(self.info["name"] + "." + self._suffix, res) 

347 

348 self._history.append(res) 

349 

350 for k in sorted(res): 

351 if k in ["password", "password1", "password2", "password3"]: 

352 fLOG("parameter ", k, " = ****") 

353 else: 

354 fLOG("parameter ", k, " = ", res[k], " type ", type(res[k])) 

355 fLOG("------------------------") 

356 

357 self.after(100, self.refresh) 

358 th = FrameFunction_ThreadFunction(self, res) 

359 th.daemon = True 

360 th.start() 

361 if "thread_started" not in self.__dict__: 

362 self.thread_started = [] 

363 self.thread_started.append(th) 

364 

365 @staticmethod 

366 def open_window(func, top_level_window=None, params=None, 

367 key_save="f", do_not_open=False): 

368 """ 

369 Opens a :epkg:`tkinter` window to run a function. It adds entries for the parameters, 

370 it displays the help associated to this function, 

371 and it allows use to run the function in a window frame. 

372 Logs are also displayed. 

373 It also memorizes the latest values used (stored in <user>/TEMP folder). 

374 

375 @param func function (function object) 

376 @param top_level_window if you want this window to depend on a top level window from tkinter 

377 @param params if not None, overwrite values for some parameters 

378 @param key_save suffix added to the file used to store the parameters 

379 @param do_not_open if True, the function do not open the window but returns it 

380 @return None or windows if *do_not_open* is True 

381 

382 The window looks like: 

383 @image images/open_window_function.png 

384 

385 Example:: 

386 

387 FrameFunction.open_window (file_head) 

388 

389 .. versionadded:: 1.0 

390 Parameter *do_not_open* was added. 

391 """ 

392 param = params if params is not None else {} 

393 

394 root = top_level_window if top_level_window is not None else create_tk() 

395 ico = os.path.realpath( 

396 os.path.join(os.path.split(__file__)[0], "project_ico.ico")) 

397 fr = FrameFunction(root, func, overwrite=param, hide=False) 

398 fr.pack() 

399 root.title(fr.get_title()) 

400 if ico is not None and top_level_window is None and sys.platform.startswith( 

401 "win"): 

402 root.wm_iconbitmap(ico) 

403 if top_level_window is None: 

404 fr.focus_set() 

405 root.focus_set() 

406 

407 if do_not_open: 

408 return fr 

409 else: 

410 fr.mainloop() 

411 return None 

412 

413 

414class FrameFunction_ThreadFunction (threading.Thread): 

415 

416 """ 

417 Class associated to FrameFunction, it runs the function 

418 in a separate thread (in order to be able to stop its execution 

419 from the interface). 

420 """ 

421 

422 def __init__(self, framewindow, parameter): 

423 """ 

424 constructor 

425 """ 

426 threading.Thread.__init__(self) # ne pas oublier cette ligne 

427 self.framewindow = framewindow 

428 self.parameter = parameter 

429 

430 def run(self): 

431 """ 

432 run the thread 

433 """ 

434 if "function" in self.framewindow.__dict__: 

435 function = self.framewindow.function 

436 else: 

437 function = None 

438 param = self.parameter # self.framewindow.info ["param"] 

439 

440 if function is not None: 

441 if not self.framewindow.raise_exception: 

442 try: 

443 ret = function(**param) 

444 except Exception as e: 

445 fLOG("------------------------------ END with exception") 

446 fLOG(type(e)) 

447 fLOG(str(e)) 

448 ret = None 

449 fLOG("------------------------------ details") 

450 import traceback 

451 st = traceback.format_exc() 

452 fLOG(st) 

453 else: 

454 ret = function(**param) 

455 else: 

456 ret = None 

457 

458 fLOG("END") 

459 fLOG("result:") 

460 fLOG(ret) 

461 self.framewindow._already = False 

462 

463 

464def open_window_function(func, top_level_window=None, params=None, 

465 key_save="f", do_not_open=False): 

466 """ 

467 Opens a :epkg:`tkinter` window to run a function. 

468 It adds entries for the parameters, 

469 it displays the help associated to this function, 

470 and it allows use to run the function in a window frame. 

471 Logs are also displayed. 

472 It also memorizes the latest values used 

473 (stored in ``<user>/TEMP folder``). 

474 

475 @param func function (function object) 

476 @param top_level_window if you want this window to depend on a top level window from tkinter 

477 @param params if not None, overwrite values for some parameters 

478 @param key_save suffix added to the file used to store the parameters 

479 @param do_not_open if True, the function do not open the window but returns it 

480 @return None or windows if *do_not_open* is True 

481 

482 The window looks like: 

483 @image images/open_window_function.png 

484 

485 .. exref:: 

486 :title: Open a tkinter windows to run a function 

487 

488 @code 

489 open_window_function (test_regular_expression) 

490 @endcode 

491 

492 The functions opens a window which looks like the following one: 

493 

494 @image images/open_function.png 

495 

496 The parameters ``key_save`` can be ignored but if you use this function 

497 with different parameters, they should all appear after a couple of runs. 

498 That is because the function uses ``key_save`` ot unique the file uses 

499 to store the values for the parameters used in previous execution. 

500 

501 .. versionadded:: 1.0 

502 Parameter *do_not_open* was added. 

503 """ 

504 return FrameFunction.open_window(func=func, top_level_window=top_level_window, params=params, 

505 key_save=key_save, do_not_open=do_not_open)