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

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 

20 

21from io import UnsupportedOperation 

22from pathlib import Path 

23 

24from IPython.core.getipython import get_ipython 

25from IPython.core.error import TryNext 

26from IPython.utils.data import chop 

27from IPython.utils.terminal import get_terminal_size 

28 

29 

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

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

32 if isinstance(strng, dict): 

33 data = strng 

34 else: 

35 if start: 

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

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

38 from IPython.display import display 

39 display(data, raw=True) 

40 

41 

42def as_hook(page_func): 

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

44 

45 so it can be called as a hook. 

46 """ 

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

48 

49 

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

51 

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

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

54 

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

56 mode. 

57 """ 

58 if isinstance(strng, dict): 

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

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

61 screens = chop(out_ln,screen_lines-1) 

62 if len(screens) == 1: 

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

64 else: 

65 last_escape = "" 

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

67 hunk = os.linesep.join(scr) 

68 print(last_escape + hunk) 

69 if not page_more(): 

70 return 

71 esc_list = esc_re.findall(hunk) 

72 if len(esc_list) > 0: 

73 last_escape = esc_list[-1] 

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

75 

76def _detect_screen_size(screen_lines_def): 

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

78 

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

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

81 """ 

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

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

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

85 # some termios calls lock up on Sun OS5. 

86 return screen_lines_def 

87 

88 try: 

89 import termios 

90 import curses 

91 except ImportError: 

92 return screen_lines_def 

93 

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

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

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

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

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

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

100 # the checks. 

101 try: 

102 term_flags = termios.tcgetattr(sys.stdout) 

103 except termios.error as err: 

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

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

106 

107 try: 

108 scr = curses.initscr() 

109 except AttributeError: 

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

111 return screen_lines_def 

112 

113 screen_lines_real,screen_cols = scr.getmaxyx() 

114 curses.endwin() 

115 

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

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

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

119 return screen_lines_real 

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

121 # screen_cols,'columns.') # dbg 

122 

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

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

125 

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

127 keyed by mime-type. 

128 

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

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

131 information). 

132 

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

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

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

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

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

138 

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

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

141 and ultimately default to less. 

142 

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

144 written in python, very simplistic. 

145 """ 

146 

147 # for compatibility with mime-bundle form: 

148 if isinstance(strng, dict): 

149 strng = strng['text/plain'] 

150 

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

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

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

154 print(strng) 

155 return 

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

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

158 str_toprint = os.linesep.join(str_lines) 

159 num_newlines = len(str_lines) 

160 len_str = len(str_toprint) 

161 

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

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

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

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

166 

167 screen_lines_def = get_terminal_size()[1] 

168 

169 # auto-determine screen size 

170 if screen_lines <= 0: 

171 try: 

172 screen_lines += _detect_screen_size(screen_lines_def) 

173 except (TypeError, UnsupportedOperation): 

174 print(str_toprint) 

175 return 

176 

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

178 if numlines <= screen_lines : 

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

180 print(str_toprint) 

181 else: 

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

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

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

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

186 pager_cmd = get_pager_cmd(pager_cmd) 

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

188 if os.name == 'nt': 

189 if pager_cmd.startswith('type'): 

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

191 retval = 1 

192 else: 

193 import tempfile 

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

195 tmppath = Path(tmpname) 

196 try: 

197 os.close(fd) 

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

199 tmpfile.write(strng) 

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

201 # tmpfile needs to be closed for windows 

202 if os.system(cmd): 

203 retval = 1 

204 else: 

205 retval = None 

206 finally: 

207 Path.unlink(tmppath) 

208 else: 

209 try: 

210 retval = None 

211 # Emulate os.popen, but redirect stderr 

212 import subprocess 

213 proc = subprocess.Popen( 

214 pager_cmd, 

215 shell=True, 

216 stdin=subprocess.PIPE, 

217 stderr=subprocess.DEVNULL, 

218 ) 

219 pager = os._wrap_close( 

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

221 ) 

222 try: 

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

224 pager.write(strng) 

225 finally: 

226 retval = pager.close() 

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

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

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

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

231 retval = None 

232 else: 

233 retval = 1 

234 if retval is not None: 

235 page_dumb(strng,screen_lines=screen_lines) 

236 

237 

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

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

240 

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

242 keyed by mime-type, or text. 

243 

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

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

246 """ 

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

248 # negative value. Offset to 0 for robustness. 

249 start = max(0, start) 

250 

251 # first, try the hook 

252 ip = get_ipython() 

253 if ip: 

254 try: 

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

256 return 

257 except TryNext: 

258 pass 

259 

260 # fallback on default pager 

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

262 

263 

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

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

266 """ 

267 

268 pager_cmd = get_pager_cmd(pager_cmd) 

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

270 

271 try: 

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

273 raise OSError 

274 from IPython.utils.process import system 

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