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
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
1"""
2Paging capabilities for IPython.core
4Notes
5-----
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"""
12# Copyright (c) IPython Development Team.
13# Distributed under the terms of the Modified BSD License.
16import os
17import io
18import re
19import sys
21from io import UnsupportedOperation
22from pathlib import Path
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
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)
42def as_hook(page_func):
43 """Wrap a pager func to strip the `self` arg
45 so it can be called as a hook.
46 """
47 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
50esc_re = re.compile(r"(\x1b[^m]+m)")
52def page_dumb(strng, start=0, screen_lines=25):
53 """Very dumb 'pager' in Python, for when nothing else works.
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]))
76def _detect_screen_size(screen_lines_def):
77 """Attempt to work out the number of lines on the screen.
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
88 try:
89 import termios
90 import curses
91 except ImportError:
92 return screen_lines_def
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
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
113 screen_lines_real,screen_cols = scr.getmaxyx()
114 curses.endwin()
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
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.
126 strng can be a mime-bundle dict, supplying multiple representations,
127 keyed by mime-type.
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).
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.
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.
143 If no system pager works, the string is sent through a 'dumb pager'
144 written in python, very simplistic.
145 """
147 # for compatibility with mime-bundle form:
148 if isinstance(strng, dict):
149 strng = strng['text/plain']
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)
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)
167 screen_lines_def = get_terminal_size()[1]
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
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)
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.
241 data can be a mime-bundle dict, supplying multiple representations,
242 keyed by mime-type, or text.
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)
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
260 # fallback on default pager
261 return pager_page(data, start, screen_lines, pager_cmd)
264def page_file(fname, start=0, pager_cmd=None):
265 """Page a file, using an optional pager command and starting line.
266 """
268 pager_cmd = get_pager_cmd(pager_cmd)
269 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
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))
285def get_pager_cmd(pager_cmd=None):
286 """Return a pager command.
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'
295 if pager_cmd is None:
296 try:
297 pager_cmd = os.environ['PAGER']
298 except KeyError:
299 pager_cmd = default_pager_cmd
301 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
302 pager_cmd += ' -R'
304 return pager_cmd
307def get_pager_start(pager, start):
308 """Return the string for paging files with an offset.
310 This is the '+N' argument which less and more (under Unix) accept.
311 """
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
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
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