Coverage for help.py: 9%
187 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1""" help.py: Implement the Idle help menu.
2Contents are subject to revision at any time, without notice.
5Help => About IDLE: display About Idle dialog
7<to be moved here from help_about.py>
10Help => IDLE Help: Display help.html with proper formatting.
11Doc/library/idle.rst (Sphinx)=> Doc/build/html/library/idle.html
12(help.copy_strip)=> Lib/idlelib/help.html
14HelpParser - Parse help.html and render to tk Text.
16HelpText - Display formatted help.html.
18HelpFrame - Contain text, scrollbar, and table-of-contents.
19(This will be needed for display in a future tabbed window.)
21HelpWindow - Display HelpFrame in a standalone window.
23copy_strip - Copy idle.html to help.html, rstripping each line.
25show_idlehelp - Create HelpWindow. Called in EditorWindow.help_dialog.
26"""
27from html.parser import HTMLParser
28from os.path import abspath, dirname, isfile, join
29from platform import python_version
31from tkinter import Toplevel, Text, Menu
32from tkinter.ttk import Frame, Menubutton, Scrollbar, Style
33from tkinter import font as tkfont
35from idlelib.config import idleConf
37## About IDLE ##
40## IDLE Help ##
42class HelpParser(HTMLParser):
43 """Render help.html into a text widget.
45 The overridden handle_xyz methods handle a subset of html tags.
46 The supplied text should have the needed tag configurations.
47 The behavior for unsupported tags, such as table, is undefined.
48 If the tags generated by Sphinx change, this class, especially
49 the handle_starttag and handle_endtags methods, might have to also.
50 """
51 def __init__(self, text):
52 HTMLParser.__init__(self, convert_charrefs=True)
53 self.text = text # Text widget we're rendering into.
54 self.tags = '' # Current block level text tags to apply.
55 self.chartags = '' # Current character level text tags.
56 self.show = False # Exclude html page navigation.
57 self.hdrlink = False # Exclude html header links.
58 self.level = 0 # Track indentation level.
59 self.pre = False # Displaying preformatted text?
60 self.hprefix = '' # Heading prefix (like '25.5'?) to remove.
61 self.nested_dl = False # In a nested <dl>?
62 self.simplelist = False # In a simple list (no double spacing)?
63 self.toc = [] # Pair headers with text indexes for toc.
64 self.header = '' # Text within header tags for toc.
65 self.prevtag = None # Previous tag info (opener?, tag).
67 def indent(self, amt=1):
68 "Change indent (+1, 0, -1) and tags."
69 self.level += amt
70 self.tags = '' if self.level == 0 else 'l'+str(self.level)
72 def handle_starttag(self, tag, attrs):
73 "Handle starttags in help.html."
74 class_ = ''
75 for a, v in attrs:
76 if a == 'class':
77 class_ = v
78 s = ''
79 if tag == 'section' and attrs == [('id', 'idle')]:
80 self.show = True # Start main content.
81 elif tag == 'div' and class_ == 'clearer':
82 self.show = False # End main content.
83 elif tag == 'p' and self.prevtag and not self.prevtag[0]:
84 # Begin a new block for <p> tags after a closed tag.
85 # Avoid extra lines, e.g. after <pre> tags.
86 lastline = self.text.get('end-1c linestart', 'end-1c')
87 s = '\n\n' if lastline and not lastline.isspace() else '\n'
88 elif tag == 'span' and class_ == 'pre':
89 self.chartags = 'pre'
90 elif tag == 'span' and class_ == 'versionmodified':
91 self.chartags = 'em'
92 elif tag == 'em':
93 self.chartags = 'em'
94 elif tag in ['ul', 'ol']:
95 if class_.find('simple') != -1:
96 s = '\n'
97 self.simplelist = True
98 else:
99 self.simplelist = False
100 self.indent()
101 elif tag == 'dl':
102 if self.level > 0:
103 self.nested_dl = True
104 elif tag == 'li':
105 s = '\n* ' if self.simplelist else '\n\n* '
106 elif tag == 'dt':
107 s = '\n\n' if not self.nested_dl else '\n' # Avoid extra line.
108 self.nested_dl = False
109 elif tag == 'dd':
110 self.indent()
111 s = '\n'
112 elif tag == 'pre':
113 self.pre = True
114 if self.show:
115 self.text.insert('end', '\n\n')
116 self.tags = 'preblock'
117 elif tag == 'a' and class_ == 'headerlink':
118 self.hdrlink = True
119 elif tag == 'h1':
120 self.tags = tag
121 elif tag in ['h2', 'h3']:
122 if self.show:
123 self.header = ''
124 self.text.insert('end', '\n\n')
125 self.tags = tag
126 if self.show:
127 self.text.insert('end', s, (self.tags, self.chartags))
128 self.prevtag = (True, tag)
130 def handle_endtag(self, tag):
131 "Handle endtags in help.html."
132 if tag in ['h1', 'h2', 'h3']:
133 assert self.level == 0
134 if self.show:
135 indent = (' ' if tag == 'h3' else
136 ' ' if tag == 'h2' else
137 '')
138 self.toc.append((indent+self.header, self.text.index('insert')))
139 self.tags = ''
140 elif tag in ['span', 'em']:
141 self.chartags = ''
142 elif tag == 'a':
143 self.hdrlink = False
144 elif tag == 'pre':
145 self.pre = False
146 self.tags = ''
147 elif tag in ['ul', 'dd', 'ol']:
148 self.indent(-1)
149 self.prevtag = (False, tag)
151 def handle_data(self, data):
152 "Handle date segments in help.html."
153 if self.show and not self.hdrlink:
154 d = data if self.pre else data.replace('\n', ' ')
155 if self.tags == 'h1':
156 try:
157 self.hprefix = d[0:d.index(' ')]
158 except ValueError:
159 self.hprefix = ''
160 if self.tags in ['h1', 'h2', 'h3']:
161 if (self.hprefix != '' and
162 d[0:len(self.hprefix)] == self.hprefix):
163 d = d[len(self.hprefix):]
164 self.header += d.strip()
165 self.text.insert('end', d, (self.tags, self.chartags))
168class HelpText(Text):
169 "Display help.html."
170 def __init__(self, parent, filename):
171 "Configure tags and feed file to parser."
172 uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
173 uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int')
174 uhigh = 3 * uhigh // 4 # Lines average 4/3 of editor line height.
175 Text.__init__(self, parent, wrap='word', highlightthickness=0,
176 padx=5, borderwidth=0, width=uwide, height=uhigh)
178 normalfont = self.findfont(['TkDefaultFont', 'arial', 'helvetica'])
179 fixedfont = self.findfont(['TkFixedFont', 'monaco', 'courier'])
180 self['font'] = (normalfont, 12)
181 self.tag_configure('em', font=(normalfont, 12, 'italic'))
182 self.tag_configure('h1', font=(normalfont, 20, 'bold'))
183 self.tag_configure('h2', font=(normalfont, 18, 'bold'))
184 self.tag_configure('h3', font=(normalfont, 15, 'bold'))
185 self.tag_configure('pre', font=(fixedfont, 12), background='#f6f6ff')
186 self.tag_configure('preblock', font=(fixedfont, 10), lmargin1=25,
187 borderwidth=1, relief='solid', background='#eeffcc')
188 self.tag_configure('l1', lmargin1=25, lmargin2=25)
189 self.tag_configure('l2', lmargin1=50, lmargin2=50)
190 self.tag_configure('l3', lmargin1=75, lmargin2=75)
191 self.tag_configure('l4', lmargin1=100, lmargin2=100)
193 self.parser = HelpParser(self)
194 with open(filename, encoding='utf-8') as f:
195 contents = f.read()
196 self.parser.feed(contents)
197 self['state'] = 'disabled'
199 def findfont(self, names):
200 "Return name of first font family derived from names."
201 for name in names:
202 if name.lower() in (x.lower() for x in tkfont.names(root=self)):
203 font = tkfont.Font(name=name, exists=True, root=self)
204 return font.actual()['family']
205 elif name.lower() in (x.lower()
206 for x in tkfont.families(root=self)):
207 return name
210class HelpFrame(Frame):
211 "Display html text, scrollbar, and toc."
212 def __init__(self, parent, filename):
213 Frame.__init__(self, parent)
214 self.text = text = HelpText(self, filename)
215 self.style = Style(parent)
216 self['style'] = 'helpframe.TFrame'
217 self.style.configure('helpframe.TFrame', background=text['background'])
218 self.toc = toc = self.toc_menu(text)
219 self.scroll = scroll = Scrollbar(self, command=text.yview)
220 text['yscrollcommand'] = scroll.set
222 self.rowconfigure(0, weight=1)
223 self.columnconfigure(1, weight=1) # Only expand the text widget.
224 toc.grid(row=0, column=0, sticky='nw')
225 text.grid(row=0, column=1, sticky='nsew')
226 scroll.grid(row=0, column=2, sticky='ns')
228 def toc_menu(self, text):
229 "Create table of contents as drop-down menu."
230 toc = Menubutton(self, text='TOC')
231 drop = Menu(toc, tearoff=False)
232 for lbl, dex in text.parser.toc:
233 drop.add_command(label=lbl, command=lambda dex=dex:text.yview(dex))
234 toc['menu'] = drop
235 return toc
238class HelpWindow(Toplevel):
239 "Display frame with rendered html."
240 def __init__(self, parent, filename, title):
241 Toplevel.__init__(self, parent)
242 self.wm_title(title)
243 self.protocol("WM_DELETE_WINDOW", self.destroy)
244 HelpFrame(self, filename).grid(column=0, row=0, sticky='nsew')
245 self.grid_columnconfigure(0, weight=1)
246 self.grid_rowconfigure(0, weight=1)
249def copy_strip():
250 """Copy idle.html to idlelib/help.html, stripping trailing whitespace.
252 Files with trailing whitespace cannot be pushed to the git cpython
253 repository. For 3.x (on Windows), help.html is generated, after
254 editing idle.rst on the master branch, with
255 sphinx-build -bhtml . build/html
256 python_d.exe -c "from idlelib.help import copy_strip; copy_strip()"
257 Check build/html/library/idle.html, the help.html diff, and the text
258 displayed by Help => IDLE Help. Add a blurb and create a PR.
260 It can be worthwhile to occasionally generate help.html without
261 touching idle.rst. Changes to the master version and to the doc
262 build system may result in changes that should not changed
263 the displayed text, but might break HelpParser.
265 As long as master and maintenance versions of idle.rst remain the
266 same, help.html can be backported. The internal Python version
267 number is not displayed. If maintenance idle.rst diverges from
268 the master version, then instead of backporting help.html from
269 master, repeat the procedure above to generate a maintenance
270 version.
271 """
272 src = join(abspath(dirname(dirname(dirname(__file__)))),
273 'Doc', 'build', 'html', 'library', 'idle.html')
274 dst = join(abspath(dirname(__file__)), 'help.html')
275 with open(src, 'rb') as inn,\
276 open(dst, 'wb') as out:
277 for line in inn:
278 out.write(line.rstrip() + b'\n')
279 print(f'{src} copied to {dst}')
281def show_idlehelp(parent):
282 "Create HelpWindow; called from Idle Help event handler."
283 filename = join(abspath(dirname(__file__)), 'help.html')
284 if not isfile(filename):
285 # Try copy_strip, present message.
286 return
287 HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version())
289if __name__ == '__main__': 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true
290 from unittest import main
291 main('idlelib.idle_test.test_help', verbosity=2, exit=False)
293 from idlelib.idle_test.htest import run
294 run(show_idlehelp)