Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/core/page.py: 16%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

173 statements  

1""" 

2Paging capabilities for IPython.core 

3 

4Notes 

5----- 

6 

7For now this uses IPython hooks, so it can't be in IPython.utils. If we can get 

8rid of that dependency, we could move it there. 

9----- 

10""" 

11 

12# Copyright (c) IPython Development Team. 

13# Distributed under the terms of the Modified BSD License. 

14 

15 

16import os 

17import io 

18import re 

19import sys 

20import tempfile 

21import subprocess 

22 

23from io import UnsupportedOperation 

24from pathlib import Path 

25 

26from IPython.core.getipython import get_ipython 

27from IPython.display import display 

28from IPython.core.error import TryNext 

29from IPython.utils.data import chop 

30from IPython.utils.process import system 

31from IPython.utils.terminal import get_terminal_size 

32 

33 

34def display_page(strng, start=0, screen_lines=25): 

35 """Just display, no paging. screen_lines is ignored.""" 

36 if isinstance(strng, dict): 

37 data = strng 

38 else: 

39 if start: 

40 strng = '\n'.join(strng.splitlines()[start:]) 

41 data = { 'text/plain': strng } 

42 display(data, raw=True) 

43 

44 

45def as_hook(page_func): 

46 """Wrap a pager func to strip the `self` arg 

47 

48 so it can be called as a hook. 

49 """ 

50 return lambda self, *args, **kwargs: page_func(*args, **kwargs) 

51 

52 

53esc_re = re.compile(r"(\x1b[^m]+m)") 

54 

55def page_dumb(strng, start=0, screen_lines=25): 

56 """Very dumb 'pager' in Python, for when nothing else works. 

57 

58 Only moves forward, same interface as page(), except for pager_cmd and 

59 mode. 

60 """ 

61 if isinstance(strng, dict): 

62 strng = strng.get('text/plain', '') 

63 out_ln = strng.splitlines()[start:] 

64 screens = chop(out_ln,screen_lines-1) 

65 if len(screens) == 1: 

66 print(os.linesep.join(screens[0])) 

67 else: 

68 last_escape = "" 

69 for scr in screens[0:-1]: 

70 hunk = os.linesep.join(scr) 

71 print(last_escape + hunk) 

72 if not page_more(): 

73 return 

74 esc_list = esc_re.findall(hunk) 

75 if len(esc_list) > 0: 

76 last_escape = esc_list[-1] 

77 print(last_escape + os.linesep.join(screens[-1])) 

78 

79def _detect_screen_size(screen_lines_def): 

80 """Attempt to work out the number of lines on the screen. 

81 

82 This is called by page(). It can raise an error (e.g. when run in the 

83 test suite), so it's separated out so it can easily be called in a try block. 

84 """ 

85 TERM = os.environ.get('TERM',None) 

86 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'): 

87 # curses causes problems on many terminals other than xterm, and 

88 # some termios calls lock up on Sun OS5. 

89 return screen_lines_def 

90 

91 try: 

92 import termios 

93 import curses 

94 except ImportError: 

95 return screen_lines_def 

96 

97 # There is a bug in curses, where *sometimes* it fails to properly 

98 # initialize, and then after the endwin() call is made, the 

99 # terminal is left in an unusable state. Rather than trying to 

100 # check every time for this (by requesting and comparing termios 

101 # flags each time), we just save the initial terminal state and 

102 # unconditionally reset it every time. It's cheaper than making 

103 # the checks. 

104 try: 

105 term_flags = termios.tcgetattr(sys.stdout) 

106 except termios.error as err: 

107 # can fail on Linux 2.6, pager_page will catch the TypeError 

108 raise TypeError(f'termios error: {err}') from err 

109 

110 try: 

111 scr = curses.initscr() 

112 except AttributeError: 

113 # Curses on Solaris may not be complete, so we can't use it there 

114 return screen_lines_def 

115 

116 screen_lines_real,screen_cols = scr.getmaxyx() 

117 curses.endwin() 

118 

119 # Restore terminal state in case endwin() didn't. 

120 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags) 

121 # Now we have what we needed: the screen size in rows/columns 

122 return screen_lines_real 

123 # print('***Screen size:',screen_lines_real,'lines x', 

124 # screen_cols,'columns.') # dbg 

125 

126def pager_page(strng, start=0, screen_lines=0, pager_cmd=None) -> None: 

127 """Display a string, piping through a pager after a certain length. 

128 

129 strng can be a mime-bundle dict, supplying multiple representations, 

130 keyed by mime-type. 

131 

132 The screen_lines parameter specifies the number of *usable* lines of your 

133 terminal screen (total lines minus lines you need to reserve to show other 

134 information). 

135 

136 If you set screen_lines to a number <=0, page() will try to auto-determine 

137 your screen size and will only use up to (screen_size+screen_lines) for 

138 printing, paging after that. That is, if you want auto-detection but need 

139 to reserve the bottom 3 lines of the screen, use screen_lines = -3, and for 

140 auto-detection without any lines reserved simply use screen_lines = 0. 

141 

142 If a string won't fit in the allowed lines, it is sent through the 

143 specified pager command. If none given, look for PAGER in the environment, 

144 and ultimately default to less. 

145 

146 If no system pager works, the string is sent through a 'dumb pager' 

147 written in python, very simplistic. 

148 """ 

149 

150 # for compatibility with mime-bundle form: 

151 if isinstance(strng, dict): 

152 strng = strng['text/plain'] 

153 

154 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs 

155 TERM = os.environ.get('TERM','dumb') 

156 if TERM in ['dumb','emacs'] and os.name != 'nt': 

157 print(strng) 

158 return 

159 # chop off the topmost part of the string we don't want to see 

160 str_lines = strng.splitlines()[start:] 

161 str_toprint = os.linesep.join(str_lines) 

162 num_newlines = len(str_lines) 

163 len_str = len(str_toprint) 

164 

165 # Dumb heuristics to guesstimate number of on-screen lines the string 

166 # takes. Very basic, but good enough for docstrings in reasonable 

167 # terminals. If someone later feels like refining it, it's not hard. 

168 numlines = max(num_newlines,int(len_str/80)+1) 

169 

170 screen_lines_def = get_terminal_size()[1] 

171 

172 # auto-determine screen size 

173 if screen_lines <= 0: 

174 try: 

175 screen_lines += _detect_screen_size(screen_lines_def) 

176 except (TypeError, UnsupportedOperation): 

177 print(str_toprint) 

178 return 

179 

180 # print('numlines',numlines,'screenlines',screen_lines) # dbg 

181 if numlines <= screen_lines : 

182 # print('*** normal print') # dbg 

183 print(str_toprint) 

184 else: 

185 # Try to open pager and default to internal one if that fails. 

186 # All failure modes are tagged as 'retval=1', to match the return 

187 # value of a failed system command. If any intermediate attempt 

188 # sets retval to 1, at the end we resort to our own page_dumb() pager. 

189 pager_cmd = get_pager_cmd(pager_cmd) 

190 pager_cmd += ' ' + get_pager_start(pager_cmd,start) 

191 if os.name == 'nt': 

192 if pager_cmd.startswith('type'): 

193 # The default WinXP 'type' command is failing on complex strings. 

194 retval = 1 

195 else: 

196 fd, tmpname = tempfile.mkstemp('.txt') 

197 tmppath = Path(tmpname) 

198 try: 

199 os.close(fd) 

200 with tmppath.open("wt", encoding="utf-8") as tmpfile: 

201 tmpfile.write(strng) 

202 cmd = "{} < {}".format(pager_cmd, tmppath) 

203 # tmpfile needs to be closed for windows 

204 if os.system(cmd): 

205 retval = 1 

206 else: 

207 retval = None 

208 finally: 

209 Path.unlink(tmppath) 

210 else: 

211 try: 

212 retval = None 

213 # Emulate os.popen, but redirect stderr 

214 proc = subprocess.Popen( 

215 pager_cmd, 

216 shell=True, 

217 stdin=subprocess.PIPE, 

218 stderr=subprocess.DEVNULL, 

219 ) 

220 pager = os._wrap_close( 

221 io.TextIOWrapper(proc.stdin, encoding="utf-8"), proc 

222 ) 

223 try: 

224 pager_encoding = pager.encoding or sys.stdout.encoding 

225 pager.write(strng) 

226 finally: 

227 retval = pager.close() 

228 except OSError as msg: # broken pipe when user quits 

229 # msg.args == (32, 'Broken pipe') for that case; other 

230 # OSErrors are strange problems, sometimes seen in Win2k/cygwin 

231 if msg.args == (32, 'Broken pipe'): 

232 retval = None 

233 else: 

234 retval = 1 

235 if retval is not None: 

236 page_dumb(strng,screen_lines=screen_lines) 

237 

238 

239def page(data, start: int = 0, screen_lines: int = 0, pager_cmd=None): 

240 """Display content in a pager, piping through a pager after a certain length. 

241 

242 data can be a mime-bundle dict, supplying multiple representations, 

243 keyed by mime-type, or text. 

244 

245 Pager is dispatched via the `show_in_pager` IPython hook. 

246 If no hook is registered, `pager_page` will be used. 

247 """ 

248 # Some routines may auto-compute start offsets incorrectly and pass a 

249 # negative value. Offset to 0 for robustness. 

250 start = max(0, start) 

251 

252 # first, try the hook 

253 ip = get_ipython() 

254 if ip: 

255 try: 

256 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines) 

257 return 

258 except TryNext: 

259 pass 

260 

261 # fallback on default pager 

262 return pager_page(data, start, screen_lines, pager_cmd) 

263 

264 

265def page_file(fname, start=0, pager_cmd=None): 

266 """Page a file, using an optional pager command and starting line. 

267 """ 

268 

269 pager_cmd = get_pager_cmd(pager_cmd) 

270 pager_cmd += ' ' + get_pager_start(pager_cmd,start) 

271 

272 try: 

273 if os.environ['TERM'] in ['emacs','dumb']: 

274 raise OSError 

275 system(pager_cmd + ' ' + fname) 

276 except Exception: 

277 try: 

278 if start > 0: 

279 start -= 1 

280 page(open(fname, encoding="utf-8").read(), start) 

281 except Exception: 

282 print('Unable to show file',repr(fname)) 

283 

284 

285def get_pager_cmd(pager_cmd=None): 

286 """Return a pager command. 

287 

288 Makes some attempts at finding an OS-correct one. 

289 """ 

290 if os.name == 'posix': 

291 default_pager_cmd = 'less -R' # -R for color control sequences 

292 elif os.name in ['nt','dos']: 

293 default_pager_cmd = 'type' 

294 

295 if pager_cmd is None: 

296 try: 

297 pager_cmd = os.environ['PAGER'] 

298 except KeyError: 

299 pager_cmd = default_pager_cmd 

300 

301 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower(): 

302 pager_cmd += ' -R' 

303 

304 return pager_cmd 

305 

306 

307def get_pager_start(pager, start): 

308 """Return the string for paging files with an offset. 

309 

310 This is the '+N' argument which less and more (under Unix) accept. 

311 """ 

312 

313 if pager in ['less','more']: 

314 if start: 

315 start_string = '+' + str(start) 

316 else: 

317 start_string = '' 

318 else: 

319 start_string = '' 

320 return start_string 

321 

322 

323# (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch() 

324if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs': 

325 import msvcrt 

326 def page_more(): 

327 """ Smart pausing between pages 

328 

329 @return: True if need print more lines, False if quit 

330 """ 

331 sys.stdout.write('---Return to continue, q to quit--- ') 

332 ans = msvcrt.getwch() 

333 if ans in ("q", "Q"): 

334 result = False 

335 else: 

336 result = True 

337 sys.stdout.write("\b"*37 + " "*37 + "\b"*37) 

338 return result 

339else: 

340 def page_more(): 

341 ans = input('---Return to continue, q to quit--- ') 

342 if ans.lower().startswith('q'): 

343 return False 

344 else: 

345 return True