import importlib.util, importlib.abc
import os
import shlex
from sys import executable, platform
from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
from tkinter import filedialog
from tkinter.font import Font
from tkinter.simpledialog import _setup_dialog
class Query(Toplevel):
def __init__(self, parent, title, message, *, text0='', used_names={},
_htest=False, _utest=False):
self.parent = parent self.message = message
self.text0 = text0
self.used_names = used_names
Toplevel.__init__(self, parent)
self.withdraw() self.title(title)
self.transient(parent)
if not _utest: self.grab_set()
_setup_dialog(self)
if self._windowingsystem == 'aqua':
self.bind("<Command-.>", self.cancel)
self.bind('<Key-Escape>', self.cancel)
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.bind('<Key-Return>', self.ok)
self.bind("<KP_Enter>", self.ok)
self.create_widgets()
self.update_idletasks() self.geometry( "+%d+%d" % (
parent.winfo_rootx() +
(parent.winfo_width()/2 - self.winfo_reqwidth()/2),
parent.winfo_rooty() +
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
if not _htest else 150)
) )
self.resizable(height=False, width=False)
if not _utest:
self.deiconify() self.entry.focus_set()
self.wait_window()
def create_widgets(self, ok_text='OK'):
self.frame = frame = Frame(self, padding=10)
frame.grid(column=0, row=0, sticky='news')
frame.grid_columnconfigure(0, weight=1)
entrylabel = Label(frame, anchor='w', justify='left',
text=self.message)
self.entryvar = StringVar(self, self.text0)
self.entry = Entry(frame, width=30, textvariable=self.entryvar)
self.error_font = Font(name='TkCaptionFont',
exists=True, root=self.parent)
self.entry_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
pady=[10,0])
self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
sticky=W+E)
self.create_extra()
self.button_ok = Button(
frame, text=ok_text, default='active', command=self.ok)
self.button_cancel = Button(
frame, text='Cancel', command=self.cancel)
self.button_ok.grid(column=1, row=99, padx=5)
self.button_cancel.grid(column=2, row=99, padx=5)
def create_extra(self): pass
def showerror(self, message, widget=None):
(widget or self.entry_error)['text'] = 'ERROR: ' + message
def entry_ok(self): "Return non-blank entry or None."
entry = self.entry.get().strip()
if not entry:
self.showerror('blank line.')
return None
return entry
def ok(self, event=None):
self.entry_error['text'] = ''
entry = self.entry_ok()
if entry is not None:
self.result = entry
self.destroy()
else:
self.entry.focus_set()
def cancel(self, event=None): "Set dialog result to None and destroy tk widget."
self.result = None
self.destroy()
def destroy(self):
self.grab_release()
super().destroy()
class SectionName(Query):
"Get a name for a config file section name."
def __init__(self, parent, title, message, used_names,
*, _htest=False, _utest=False):
super().__init__(parent, title, message, used_names=used_names,
_htest=_htest, _utest=_utest)
def entry_ok(self):
"Return sensible ConfigParser section name or None."
name = self.entry.get().strip()
if not name:
self.showerror('no name specified.')
return None
elif len(name)>30:
self.showerror('name is longer than 30 characters.')
return None
elif name in self.used_names:
self.showerror('name is already in use.')
return None
return name
class ModuleName(Query):
"Get a module name for Open Module menu entry."
def __init__(self, parent, title, message, text0,
*, _htest=False, _utest=False):
super().__init__(parent, title, message, text0=text0,
_htest=_htest, _utest=_utest)
def entry_ok(self):
"Return entered module name as file path or None."
name = self.entry.get().strip()
if not name:
self.showerror('no name specified.')
return None
try:
spec = importlib.util.find_spec(name)
except (ValueError, ImportError) as msg:
self.showerror(str(msg))
return None
if spec is None:
self.showerror("module not found.")
return None
if not isinstance(spec.loader, importlib.abc.SourceLoader):
self.showerror("not a source-based module.")
return None
try:
file_path = spec.loader.get_filename(name)
except AttributeError:
self.showerror("loader does not support get_filename.")
return None
except ImportError:
try:
file_path = spec.loader.get_filename()
except TypeError:
self.showerror("loader failed to get filename.")
return None
return file_path
class Goto(Query):
"Get a positive line number for editor Go To Line."
def entry_ok(self):
try:
lineno = int(self.entry.get())
except ValueError:
self.showerror('not a base 10 integer.')
return None
if lineno <= 0:
self.showerror('not a positive integer.')
return None
return lineno
class HelpSource(Query):
"Get menu name and help source for Help menu."
def __init__(self, parent, title, *, menuitem='', filepath='',
used_names={}, _htest=False, _utest=False):
self.filepath = filepath
message = 'Name for item on Help menu:'
super().__init__(
parent, title, message, text0=menuitem,
used_names=used_names, _htest=_htest, _utest=_utest)
def create_extra(self):
"Add path widjets to rows 10-12."
frame = self.frame
pathlabel = Label(frame, anchor='w', justify='left',
text='Help File Path: Enter URL or browse for file')
self.pathvar = StringVar(self, self.filepath)
self.path = Entry(frame, textvariable=self.pathvar, width=40)
browse = Button(frame, text='Browse', width=8,
command=self.browse_file)
self.path_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
pathlabel.grid(column=0, row=10, columnspan=3, padx=5, pady=[10,0],
sticky=W)
self.path.grid(column=0, row=11, columnspan=2, padx=5, sticky=W+E,
pady=[10,0])
browse.grid(column=2, row=11, padx=5, sticky=W+S)
self.path_error.grid(column=0, row=12, columnspan=3, padx=5,
sticky=W+E)
def askfilename(self, filetypes, initdir, initfile): return filedialog.Open(parent=self, filetypes=filetypes)\
.show(initialdir=initdir, initialfile=initfile)
def browse_file(self):
filetypes = [
("HTML Files", "*.htm *.html", "TEXT"),
("Text Files", "*.txt", "TEXT"),
("All Files", "*")]
path = self.pathvar.get()
if path:
dir, base = os.path.split(path)
else:
base = None
if platform[:3] == 'win':
dir = os.path.join(os.path.dirname(executable), 'Doc')
if not os.path.isdir(dir):
dir = os.getcwd()
else:
dir = os.getcwd()
file = self.askfilename(filetypes, dir, base)
if file:
self.pathvar.set(file)
item_ok = SectionName.entry_ok
def path_ok(self):
"Simple validity check for menu file path"
path = self.path.get().strip()
if not path: self.showerror('no help file path specified.', self.path_error)
return None
elif not path.startswith(('www.', 'http')):
if path[:5] == 'file:':
path = path[5:]
if not os.path.exists(path):
self.showerror('help file path does not exist.',
self.path_error)
return None
if platform == 'darwin': path = "file://" + path
return path
def entry_ok(self):
"Return apparently valid (name, path) or None"
self.path_error['text'] = ''
name = self.item_ok()
path = self.path_ok()
return None if name is None or path is None else (name, path)
class CustomRun(Query):
def __init__(self, parent, title, *, cli_args=[],
_htest=False, _utest=False):
message = 'Command Line Arguments for sys.argv:'
super().__init__(
parent, title, message, text0=cli_args,
_htest=_htest, _utest=_utest)
def create_extra(self):
"Add run mode on rows 10-12."
frame = self.frame
self.restartvar = BooleanVar(self, value=True)
restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
offvalue=False, text='Restart shell')
self.args_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
sticky='we')
def cli_args_ok(self):
"Return command line arg list or None if error."
cli_string = self.entry.get().strip()
try:
cli_args = shlex.split(cli_string, posix=True)
except ValueError as err:
self.showerror(str(err))
return None
return cli_args
def entry_ok(self):
"Return apparently valid (cli_args, restart) or None."
cli_args = self.cli_args_ok()
restart = self.restartvar.get()
return None if cli_args is None else (cli_args, restart)
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_query', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
run(Query, HelpSource, CustomRun)