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

176 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1# encoding: utf-8 

2""" 

3Paging capabilities for IPython.core 

4 

5Notes 

6----- 

7 

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

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

10----- 

11""" 

12 

13# Copyright (c) IPython Development Team. 

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

15 

16 

17import os 

18import io 

19import re 

20import sys 

21import tempfile 

22import subprocess 

23 

24from io import UnsupportedOperation 

25from pathlib import Path 

26 

27from IPython import get_ipython 

28from IPython.display import display 

29from IPython.core.error import TryNext 

30from IPython.utils.data import chop 

31from IPython.utils.process import system 

32from IPython.utils.terminal import get_terminal_size 

33from IPython.utils import py3compat 

34 

35 

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

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

38 if isinstance(strng, dict): 

39 data = strng 

40 else: 

41 if start: 

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

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

44 display(data, raw=True) 

45 

46 

47def as_hook(page_func): 

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

49 

50 so it can be called as a hook. 

51 """ 

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

53 

54 

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

56 

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

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

59 

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

61 mode. 

62 """ 

63 if isinstance(strng, dict): 

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

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

66 screens = chop(out_ln,screen_lines-1) 

67 if len(screens) == 1: 

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

69 else: 

70 last_escape = "" 

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

72 hunk = os.linesep.join(scr) 

73 print(last_escape + hunk) 

74 if not page_more(): 

75 return 

76 esc_list = esc_re.findall(hunk) 

77 if len(esc_list) > 0: 

78 last_escape = esc_list[-1] 

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

80 

81def _detect_screen_size(screen_lines_def): 

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

83 

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

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

86 """ 

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

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

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

90 # some termios calls lock up on Sun OS5. 

91 return screen_lines_def 

92 

93 try: 

94 import termios 

95 import curses 

96 except ImportError: 

97 return screen_lines_def 

98 

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

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

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

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

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

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

105 # the checks. 

106 try: 

107 term_flags = termios.tcgetattr(sys.stdout) 

108 except termios.error as err: 

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

110 raise TypeError('termios error: {0}'.format(err)) from err 

111 

112 try: 

113 scr = curses.initscr() 

114 except AttributeError: 

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

116 return screen_lines_def 

117 

118 screen_lines_real,screen_cols = scr.getmaxyx() 

119 curses.endwin() 

120 

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

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

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

124 return screen_lines_real 

125 #print '***Screen size:',screen_lines_real,'lines x',\ 

126 #screen_cols,'columns.' # dbg 

127 

128def pager_page(strng, start=0, screen_lines=0, pager_cmd=None): 

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

130 

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

132 keyed by mime-type. 

133 

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

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

136 information). 

137 

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

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

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

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

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

143 

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

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

146 and ultimately default to less. 

147 

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

149 written in python, very simplistic. 

150 """ 

151 

152 # for compatibility with mime-bundle form: 

153 if isinstance(strng, dict): 

154 strng = strng['text/plain'] 

155 

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

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

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

159 print(strng) 

160 return 

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

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

163 str_toprint = os.linesep.join(str_lines) 

164 num_newlines = len(str_lines) 

165 len_str = len(str_toprint) 

166 

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

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

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

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

171 

172 screen_lines_def = get_terminal_size()[1] 

173 

174 # auto-determine screen size 

175 if screen_lines <= 0: 

176 try: 

177 screen_lines += _detect_screen_size(screen_lines_def) 

178 except (TypeError, UnsupportedOperation): 

179 print(str_toprint) 

180 return 

181 

182 #print 'numlines',numlines,'screenlines',screen_lines # dbg 

183 if numlines <= screen_lines : 

184 #print '*** normal print' # dbg 

185 print(str_toprint) 

186 else: 

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

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

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

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

191 pager_cmd = get_pager_cmd(pager_cmd) 

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

193 if os.name == 'nt': 

194 if pager_cmd.startswith('type'): 

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

196 retval = 1 

197 else: 

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

199 tmppath = Path(tmpname) 

200 try: 

201 os.close(fd) 

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

203 tmpfile.write(strng) 

204 cmd = "%s < %s" % (pager_cmd, tmppath) 

205 # tmpfile needs to be closed for windows 

206 if os.system(cmd): 

207 retval = 1 

208 else: 

209 retval = None 

210 finally: 

211 Path.unlink(tmppath) 

212 else: 

213 try: 

214 retval = None 

215 # Emulate os.popen, but redirect stderr 

216 proc = subprocess.Popen( 

217 pager_cmd, 

218 shell=True, 

219 stdin=subprocess.PIPE, 

220 stderr=subprocess.DEVNULL, 

221 ) 

222 pager = os._wrap_close( 

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

224 ) 

225 try: 

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

227 pager.write(strng) 

228 finally: 

229 retval = pager.close() 

230 except IOError as msg: # broken pipe when user quits 

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

232 retval = None 

233 else: 

234 retval = 1 

235 except OSError: 

236 # Other strange problems, sometimes seen in Win2k/cygwin 

237 retval = 1 

238 if retval is not None: 

239 page_dumb(strng,screen_lines=screen_lines) 

240 

241 

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

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

244 

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

246 keyed by mime-type, or text. 

247 

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

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

250 """ 

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

252 # negative value. Offset to 0 for robustness. 

253 start = max(0, start) 

254 

255 # first, try the hook 

256 ip = get_ipython() 

257 if ip: 

258 try: 

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

260 return 

261 except TryNext: 

262 pass 

263 

264 # fallback on default pager 

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

266 

267 

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

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

270 """ 

271 

272 pager_cmd = get_pager_cmd(pager_cmd) 

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

274 

275 try: 

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

277 raise EnvironmentError 

278 system(pager_cmd + ' ' + fname) 

279 except: 

280 try: 

281 if start > 0: 

282 start -= 1 

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

284 except: 

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

286 

287 

288def get_pager_cmd(pager_cmd=None): 

289 """Return a pager command. 

290 

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

292 """ 

293 if os.name == 'posix': 

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

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

296 default_pager_cmd = 'type' 

297 

298 if pager_cmd is None: 

299 try: 

300 pager_cmd = os.environ['PAGER'] 

301 except: 

302 pager_cmd = default_pager_cmd 

303 

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

305 pager_cmd += ' -R' 

306 

307 return pager_cmd 

308 

309 

310def get_pager_start(pager, start): 

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

312 

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

314 """ 

315 

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

317 if start: 

318 start_string = '+' + str(start) 

319 else: 

320 start_string = '' 

321 else: 

322 start_string = '' 

323 return start_string 

324 

325 

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

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

328 import msvcrt 

329 def page_more(): 

330 """ Smart pausing between pages 

331 

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

333 """ 

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

335 ans = msvcrt.getwch() 

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

337 result = False 

338 else: 

339 result = True 

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

341 return result 

342else: 

343 def page_more(): 

344 ans = py3compat.input('---Return to continue, q to quit--- ') 

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

346 return False 

347 else: 

348 return True