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
« 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
5Notes
6-----
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"""
13# Copyright (c) IPython Development Team.
14# Distributed under the terms of the Modified BSD License.
17import os
18import io
19import re
20import sys
21import tempfile
22import subprocess
24from io import UnsupportedOperation
25from pathlib import Path
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
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)
47def as_hook(page_func):
48 """Wrap a pager func to strip the `self` arg
50 so it can be called as a hook.
51 """
52 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
55esc_re = re.compile(r"(\x1b[^m]+m)")
57def page_dumb(strng, start=0, screen_lines=25):
58 """Very dumb 'pager' in Python, for when nothing else works.
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]))
81def _detect_screen_size(screen_lines_def):
82 """Attempt to work out the number of lines on the screen.
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
93 try:
94 import termios
95 import curses
96 except ImportError:
97 return screen_lines_def
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
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
118 screen_lines_real,screen_cols = scr.getmaxyx()
119 curses.endwin()
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
128def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
129 """Display a string, piping through a pager after a certain length.
131 strng can be a mime-bundle dict, supplying multiple representations,
132 keyed by mime-type.
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).
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.
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.
148 If no system pager works, the string is sent through a 'dumb pager'
149 written in python, very simplistic.
150 """
152 # for compatibility with mime-bundle form:
153 if isinstance(strng, dict):
154 strng = strng['text/plain']
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)
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)
172 screen_lines_def = get_terminal_size()[1]
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
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)
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.
245 data can be a mime-bundle dict, supplying multiple representations,
246 keyed by mime-type, or text.
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)
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
264 # fallback on default pager
265 return pager_page(data, start, screen_lines, pager_cmd)
268def page_file(fname, start=0, pager_cmd=None):
269 """Page a file, using an optional pager command and starting line.
270 """
272 pager_cmd = get_pager_cmd(pager_cmd)
273 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
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))
288def get_pager_cmd(pager_cmd=None):
289 """Return a pager command.
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'
298 if pager_cmd is None:
299 try:
300 pager_cmd = os.environ['PAGER']
301 except:
302 pager_cmd = default_pager_cmd
304 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
305 pager_cmd += ' -R'
307 return pager_cmd
310def get_pager_start(pager, start):
311 """Return the string for paging files with an offset.
313 This is the '+N' argument which less and more (under Unix) accept.
314 """
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
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
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