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
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 """
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 = {}
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
64 # retieve previous answers
65 self._history = []
66 self._hpos = -1
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())
75 function = private_get_function(function)
76 self.function = function
77 self.info = extract_function_information(function)
79 else:
80 self.function = function
81 self.info = extract_function_information(function)
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())
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
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
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"])
108 objs = [lab, tlab]
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)
115 self.fdoc.bind('<Return>', self.run_function)
116 self.fdoc.bind('<Escape>', self.run_cancel)
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]
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)
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]
145 self.fpar.bind('<Return>', self.run_function)
146 self.fpar.bind('<Escape>', self.run_cancel)
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)
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)
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
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)
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)
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
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))
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)
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
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)
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)
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, "")))
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()
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()
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()
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)
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()
291 def get_parameters(self):
292 """
293 Returns the parameters in a dictionary.
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
307 def get_title(self):
308 """
309 @return self.info ["name"]
310 """
311 return self.info["name"]
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):]
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")
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()
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
344 res = self.get_parameters()
345 if self.restore:
346 _private_store(self.info["name"] + "." + self._suffix, res)
348 self._history.append(res)
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("------------------------")
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)
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).
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
382 The window looks like:
383 @image images/open_window_function.png
385 Example::
387 FrameFunction.open_window (file_head)
389 .. versionadded:: 1.0
390 Parameter *do_not_open* was added.
391 """
392 param = params if params is not None else {}
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()
407 if do_not_open:
408 return fr
409 else:
410 fr.mainloop()
411 return None
414class FrameFunction_ThreadFunction (threading.Thread):
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 """
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
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"]
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
458 fLOG("END")
459 fLOG("result:")
460 fLOG(ret)
461 self.framewindow._already = False
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``).
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
482 The window looks like:
483 @image images/open_window_function.png
485 .. exref::
486 :title: Open a tkinter windows to run a function
488 @code
489 open_window_function (test_regular_expression)
490 @endcode
492 The functions opens a window which looks like the following one:
494 @image images/open_function.png
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.
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)