Coverage for help.py: 9%

187 statements  

« 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. 

3 

4 

5Help => About IDLE: display About Idle dialog 

6 

7<to be moved here from help_about.py> 

8 

9 

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 

13 

14HelpParser - Parse help.html and render to tk Text. 

15 

16HelpText - Display formatted help.html. 

17 

18HelpFrame - Contain text, scrollbar, and table-of-contents. 

19(This will be needed for display in a future tabbed window.) 

20 

21HelpWindow - Display HelpFrame in a standalone window. 

22 

23copy_strip - Copy idle.html to help.html, rstripping each line. 

24 

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 

30 

31from tkinter import Toplevel, Text, Menu 

32from tkinter.ttk import Frame, Menubutton, Scrollbar, Style 

33from tkinter import font as tkfont 

34 

35from idlelib.config import idleConf 

36 

37## About IDLE ## 

38 

39 

40## IDLE Help ## 

41 

42class HelpParser(HTMLParser): 

43 """Render help.html into a text widget. 

44 

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). 

66 

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) 

71 

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) 

129 

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) 

150 

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)) 

166 

167 

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) 

177 

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) 

192 

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' 

198 

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 

208 

209 

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 

221 

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') 

227 

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 

236 

237 

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) 

247 

248 

249def copy_strip(): 

250 """Copy idle.html to idlelib/help.html, stripping trailing whitespace. 

251 

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. 

259 

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. 

264 

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}') 

280 

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()) 

288 

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) 

292 

293 from idlelib.idle_test.htest import run 

294 run(show_idlehelp)