##// END OF EJS Templates
Use pathlib in page.py
Inception95 -
Show More
@@ -1,345 +1,345 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Paging capabilities for IPython.core
3 Paging capabilities for IPython.core
4
4
5 Notes
5 Notes
6 -----
6 -----
7
7
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
8 For now this uses IPython hooks, so it can't be in IPython.utils. If we can get
9 rid of that dependency, we could move it there.
9 rid of that dependency, we could move it there.
10 -----
10 -----
11 """
11 """
12
12
13 # Copyright (c) IPython Development Team.
13 # Copyright (c) IPython Development Team.
14 # Distributed under the terms of the Modified BSD License.
14 # Distributed under the terms of the Modified BSD License.
15
15
16
16
17 import os
17 import os
18 import io
18 import io
19 import re
19 import re
20 import sys
20 import sys
21 import tempfile
21 import tempfile
22 import subprocess
22 import subprocess
23
23
24 from io import UnsupportedOperation
24 from io import UnsupportedOperation
25 from pathlib import Path
25 from pathlib import Path
26
26
27 from IPython import get_ipython
27 from IPython import get_ipython
28 from IPython.display import display
28 from IPython.display import display
29 from IPython.core.error import TryNext
29 from IPython.core.error import TryNext
30 from IPython.utils.data import chop
30 from IPython.utils.data import chop
31 from IPython.utils.process import system
31 from IPython.utils.process import system
32 from IPython.utils.terminal import get_terminal_size
32 from IPython.utils.terminal import get_terminal_size
33 from IPython.utils import py3compat
33 from IPython.utils import py3compat
34
34
35
35
36 def display_page(strng, start=0, screen_lines=25):
36 def display_page(strng, start=0, screen_lines=25):
37 """Just display, no paging. screen_lines is ignored."""
37 """Just display, no paging. screen_lines is ignored."""
38 if isinstance(strng, dict):
38 if isinstance(strng, dict):
39 data = strng
39 data = strng
40 else:
40 else:
41 if start:
41 if start:
42 strng = u'\n'.join(strng.splitlines()[start:])
42 strng = u'\n'.join(strng.splitlines()[start:])
43 data = { 'text/plain': strng }
43 data = { 'text/plain': strng }
44 display(data, raw=True)
44 display(data, raw=True)
45
45
46
46
47 def as_hook(page_func):
47 def as_hook(page_func):
48 """Wrap a pager func to strip the `self` arg
48 """Wrap a pager func to strip the `self` arg
49
49
50 so it can be called as a hook.
50 so it can be called as a hook.
51 """
51 """
52 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
52 return lambda self, *args, **kwargs: page_func(*args, **kwargs)
53
53
54
54
55 esc_re = re.compile(r"(\x1b[^m]+m)")
55 esc_re = re.compile(r"(\x1b[^m]+m)")
56
56
57 def page_dumb(strng, start=0, screen_lines=25):
57 def page_dumb(strng, start=0, screen_lines=25):
58 """Very dumb 'pager' in Python, for when nothing else works.
58 """Very dumb 'pager' in Python, for when nothing else works.
59
59
60 Only moves forward, same interface as page(), except for pager_cmd and
60 Only moves forward, same interface as page(), except for pager_cmd and
61 mode.
61 mode.
62 """
62 """
63 if isinstance(strng, dict):
63 if isinstance(strng, dict):
64 strng = strng.get('text/plain', '')
64 strng = strng.get('text/plain', '')
65 out_ln = strng.splitlines()[start:]
65 out_ln = strng.splitlines()[start:]
66 screens = chop(out_ln,screen_lines-1)
66 screens = chop(out_ln,screen_lines-1)
67 if len(screens) == 1:
67 if len(screens) == 1:
68 print(os.linesep.join(screens[0]))
68 print(os.linesep.join(screens[0]))
69 else:
69 else:
70 last_escape = ""
70 last_escape = ""
71 for scr in screens[0:-1]:
71 for scr in screens[0:-1]:
72 hunk = os.linesep.join(scr)
72 hunk = os.linesep.join(scr)
73 print(last_escape + hunk)
73 print(last_escape + hunk)
74 if not page_more():
74 if not page_more():
75 return
75 return
76 esc_list = esc_re.findall(hunk)
76 esc_list = esc_re.findall(hunk)
77 if len(esc_list) > 0:
77 if len(esc_list) > 0:
78 last_escape = esc_list[-1]
78 last_escape = esc_list[-1]
79 print(last_escape + os.linesep.join(screens[-1]))
79 print(last_escape + os.linesep.join(screens[-1]))
80
80
81 def _detect_screen_size(screen_lines_def):
81 def _detect_screen_size(screen_lines_def):
82 """Attempt to work out the number of lines on the screen.
82 """Attempt to work out the number of lines on the screen.
83
83
84 This is called by page(). It can raise an error (e.g. when run in the
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.
85 test suite), so it's separated out so it can easily be called in a try block.
86 """
86 """
87 TERM = os.environ.get('TERM',None)
87 TERM = os.environ.get('TERM',None)
88 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
88 if not((TERM=='xterm' or TERM=='xterm-color') and sys.platform != 'sunos5'):
89 # curses causes problems on many terminals other than xterm, and
89 # curses causes problems on many terminals other than xterm, and
90 # some termios calls lock up on Sun OS5.
90 # some termios calls lock up on Sun OS5.
91 return screen_lines_def
91 return screen_lines_def
92
92
93 try:
93 try:
94 import termios
94 import termios
95 import curses
95 import curses
96 except ImportError:
96 except ImportError:
97 return screen_lines_def
97 return screen_lines_def
98
98
99 # There is a bug in curses, where *sometimes* it fails to properly
99 # There is a bug in curses, where *sometimes* it fails to properly
100 # initialize, and then after the endwin() call is made, the
100 # initialize, and then after the endwin() call is made, the
101 # terminal is left in an unusable state. Rather than trying to
101 # terminal is left in an unusable state. Rather than trying to
102 # check every time for this (by requesting and comparing termios
102 # check every time for this (by requesting and comparing termios
103 # flags each time), we just save the initial terminal state and
103 # flags each time), we just save the initial terminal state and
104 # unconditionally reset it every time. It's cheaper than making
104 # unconditionally reset it every time. It's cheaper than making
105 # the checks.
105 # the checks.
106 try:
106 try:
107 term_flags = termios.tcgetattr(sys.stdout)
107 term_flags = termios.tcgetattr(sys.stdout)
108 except termios.error as err:
108 except termios.error as err:
109 # can fail on Linux 2.6, pager_page will catch the TypeError
109 # can fail on Linux 2.6, pager_page will catch the TypeError
110 raise TypeError('termios error: {0}'.format(err)) from err
110 raise TypeError('termios error: {0}'.format(err)) from err
111
111
112 try:
112 try:
113 scr = curses.initscr()
113 scr = curses.initscr()
114 except AttributeError:
114 except AttributeError:
115 # Curses on Solaris may not be complete, so we can't use it there
115 # Curses on Solaris may not be complete, so we can't use it there
116 return screen_lines_def
116 return screen_lines_def
117
117
118 screen_lines_real,screen_cols = scr.getmaxyx()
118 screen_lines_real,screen_cols = scr.getmaxyx()
119 curses.endwin()
119 curses.endwin()
120
120
121 # Restore terminal state in case endwin() didn't.
121 # Restore terminal state in case endwin() didn't.
122 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
122 termios.tcsetattr(sys.stdout,termios.TCSANOW,term_flags)
123 # Now we have what we needed: the screen size in rows/columns
123 # Now we have what we needed: the screen size in rows/columns
124 return screen_lines_real
124 return screen_lines_real
125 #print '***Screen size:',screen_lines_real,'lines x',\
125 #print '***Screen size:',screen_lines_real,'lines x',\
126 #screen_cols,'columns.' # dbg
126 #screen_cols,'columns.' # dbg
127
127
128 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
128 def pager_page(strng, start=0, screen_lines=0, pager_cmd=None):
129 """Display a string, piping through a pager after a certain length.
129 """Display a string, piping through a pager after a certain length.
130
130
131 strng can be a mime-bundle dict, supplying multiple representations,
131 strng can be a mime-bundle dict, supplying multiple representations,
132 keyed by mime-type.
132 keyed by mime-type.
133
133
134 The screen_lines parameter specifies the number of *usable* lines of your
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
135 terminal screen (total lines minus lines you need to reserve to show other
136 information).
136 information).
137
137
138 If you set screen_lines to a number <=0, page() will try to auto-determine
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
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
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
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.
142 auto-detection without any lines reserved simply use screen_lines = 0.
143
143
144 If a string won't fit in the allowed lines, it is sent through the
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,
145 specified pager command. If none given, look for PAGER in the environment,
146 and ultimately default to less.
146 and ultimately default to less.
147
147
148 If no system pager works, the string is sent through a 'dumb pager'
148 If no system pager works, the string is sent through a 'dumb pager'
149 written in python, very simplistic.
149 written in python, very simplistic.
150 """
150 """
151
151
152 # for compatibility with mime-bundle form:
152 # for compatibility with mime-bundle form:
153 if isinstance(strng, dict):
153 if isinstance(strng, dict):
154 strng = strng['text/plain']
154 strng = strng['text/plain']
155
155
156 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
156 # Ugly kludge, but calling curses.initscr() flat out crashes in emacs
157 TERM = os.environ.get('TERM','dumb')
157 TERM = os.environ.get('TERM','dumb')
158 if TERM in ['dumb','emacs'] and os.name != 'nt':
158 if TERM in ['dumb','emacs'] and os.name != 'nt':
159 print(strng)
159 print(strng)
160 return
160 return
161 # chop off the topmost part of the string we don't want to see
161 # chop off the topmost part of the string we don't want to see
162 str_lines = strng.splitlines()[start:]
162 str_lines = strng.splitlines()[start:]
163 str_toprint = os.linesep.join(str_lines)
163 str_toprint = os.linesep.join(str_lines)
164 num_newlines = len(str_lines)
164 num_newlines = len(str_lines)
165 len_str = len(str_toprint)
165 len_str = len(str_toprint)
166
166
167 # Dumb heuristics to guesstimate number of on-screen lines the string
167 # Dumb heuristics to guesstimate number of on-screen lines the string
168 # takes. Very basic, but good enough for docstrings in reasonable
168 # takes. Very basic, but good enough for docstrings in reasonable
169 # terminals. If someone later feels like refining it, it's not hard.
169 # terminals. If someone later feels like refining it, it's not hard.
170 numlines = max(num_newlines,int(len_str/80)+1)
170 numlines = max(num_newlines,int(len_str/80)+1)
171
171
172 screen_lines_def = get_terminal_size()[1]
172 screen_lines_def = get_terminal_size()[1]
173
173
174 # auto-determine screen size
174 # auto-determine screen size
175 if screen_lines <= 0:
175 if screen_lines <= 0:
176 try:
176 try:
177 screen_lines += _detect_screen_size(screen_lines_def)
177 screen_lines += _detect_screen_size(screen_lines_def)
178 except (TypeError, UnsupportedOperation):
178 except (TypeError, UnsupportedOperation):
179 print(str_toprint)
179 print(str_toprint)
180 return
180 return
181
181
182 #print 'numlines',numlines,'screenlines',screen_lines # dbg
182 #print 'numlines',numlines,'screenlines',screen_lines # dbg
183 if numlines <= screen_lines :
183 if numlines <= screen_lines :
184 #print '*** normal print' # dbg
184 #print '*** normal print' # dbg
185 print(str_toprint)
185 print(str_toprint)
186 else:
186 else:
187 # Try to open pager and default to internal one if that fails.
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
188 # All failure modes are tagged as 'retval=1', to match the return
189 # value of a failed system command. If any intermediate attempt
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.
190 # sets retval to 1, at the end we resort to our own page_dumb() pager.
191 pager_cmd = get_pager_cmd(pager_cmd)
191 pager_cmd = get_pager_cmd(pager_cmd)
192 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
192 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
193 if os.name == 'nt':
193 if os.name == 'nt':
194 if pager_cmd.startswith('type'):
194 if pager_cmd.startswith('type'):
195 # The default WinXP 'type' command is failing on complex strings.
195 # The default WinXP 'type' command is failing on complex strings.
196 retval = 1
196 retval = 1
197 else:
197 else:
198 fd, tmpname = tempfile.mkstemp('.txt')
198 fd, tmpname = tempfile.mkstemp('.txt')
199 tmppath =
199 tmppath = Path(tmpname)
200 try:
200 try:
201 os.close(fd)
201 os.close(fd)
202 with open(tmpname, 'wt') as tmpfile:
202 with tmppath.open('wt') as tmpfile:
203 tmpfile.write(strng)
203 tmpfile.write(strng)
204 cmd = "%s < %s" % (pager_cmd, tmpname)
204 cmd = "%s < %s" % (pager_cmd, tmppath)
205 # tmpfile needs to be closed for windows
205 # tmpfile needs to be closed for windows
206 if os.system(cmd):
206 if os.system(cmd):
207 retval = 1
207 retval = 1
208 else:
208 else:
209 retval = None
209 retval = None
210 finally:
210 finally:
211 os.remove(tmpname)
211 Path.unlink(tmppath)
212 else:
212 else:
213 try:
213 try:
214 retval = None
214 retval = None
215 # Emulate os.popen, but redirect stderr
215 # Emulate os.popen, but redirect stderr
216 proc = subprocess.Popen(pager_cmd,
216 proc = subprocess.Popen(pager_cmd,
217 shell=True,
217 shell=True,
218 stdin=subprocess.PIPE,
218 stdin=subprocess.PIPE,
219 stderr=subprocess.DEVNULL
219 stderr=subprocess.DEVNULL
220 )
220 )
221 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc)
221 pager = os._wrap_close(io.TextIOWrapper(proc.stdin), proc)
222 try:
222 try:
223 pager_encoding = pager.encoding or sys.stdout.encoding
223 pager_encoding = pager.encoding or sys.stdout.encoding
224 pager.write(strng)
224 pager.write(strng)
225 finally:
225 finally:
226 retval = pager.close()
226 retval = pager.close()
227 except IOError as msg: # broken pipe when user quits
227 except IOError as msg: # broken pipe when user quits
228 if msg.args == (32, 'Broken pipe'):
228 if msg.args == (32, 'Broken pipe'):
229 retval = None
229 retval = None
230 else:
230 else:
231 retval = 1
231 retval = 1
232 except OSError:
232 except OSError:
233 # Other strange problems, sometimes seen in Win2k/cygwin
233 # Other strange problems, sometimes seen in Win2k/cygwin
234 retval = 1
234 retval = 1
235 if retval is not None:
235 if retval is not None:
236 page_dumb(strng,screen_lines=screen_lines)
236 page_dumb(strng,screen_lines=screen_lines)
237
237
238
238
239 def page(data, start=0, screen_lines=0, pager_cmd=None):
239 def page(data, start=0, screen_lines=0, pager_cmd=None):
240 """Display content in a pager, piping through a pager after a certain length.
240 """Display content in a pager, piping through a pager after a certain length.
241
241
242 data can be a mime-bundle dict, supplying multiple representations,
242 data can be a mime-bundle dict, supplying multiple representations,
243 keyed by mime-type, or text.
243 keyed by mime-type, or text.
244
244
245 Pager is dispatched via the `show_in_pager` IPython hook.
245 Pager is dispatched via the `show_in_pager` IPython hook.
246 If no hook is registered, `pager_page` will be used.
246 If no hook is registered, `pager_page` will be used.
247 """
247 """
248 # Some routines may auto-compute start offsets incorrectly and pass a
248 # Some routines may auto-compute start offsets incorrectly and pass a
249 # negative value. Offset to 0 for robustness.
249 # negative value. Offset to 0 for robustness.
250 start = max(0, start)
250 start = max(0, start)
251
251
252 # first, try the hook
252 # first, try the hook
253 ip = get_ipython()
253 ip = get_ipython()
254 if ip:
254 if ip:
255 try:
255 try:
256 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
256 ip.hooks.show_in_pager(data, start=start, screen_lines=screen_lines)
257 return
257 return
258 except TryNext:
258 except TryNext:
259 pass
259 pass
260
260
261 # fallback on default pager
261 # fallback on default pager
262 return pager_page(data, start, screen_lines, pager_cmd)
262 return pager_page(data, start, screen_lines, pager_cmd)
263
263
264
264
265 def page_file(fname, start=0, pager_cmd=None):
265 def page_file(fname, start=0, pager_cmd=None):
266 """Page a file, using an optional pager command and starting line.
266 """Page a file, using an optional pager command and starting line.
267 """
267 """
268
268
269 pager_cmd = get_pager_cmd(pager_cmd)
269 pager_cmd = get_pager_cmd(pager_cmd)
270 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
270 pager_cmd += ' ' + get_pager_start(pager_cmd,start)
271
271
272 try:
272 try:
273 if os.environ['TERM'] in ['emacs','dumb']:
273 if os.environ['TERM'] in ['emacs','dumb']:
274 raise EnvironmentError
274 raise EnvironmentError
275 system(pager_cmd + ' ' + fname)
275 system(pager_cmd + ' ' + fname)
276 except:
276 except:
277 try:
277 try:
278 if start > 0:
278 if start > 0:
279 start -= 1
279 start -= 1
280 page(open(fname).read(),start)
280 page(open(fname).read(),start)
281 except:
281 except:
282 print('Unable to show file',repr(fname))
282 print('Unable to show file',repr(fname))
283
283
284
284
285 def get_pager_cmd(pager_cmd=None):
285 def get_pager_cmd(pager_cmd=None):
286 """Return a pager command.
286 """Return a pager command.
287
287
288 Makes some attempts at finding an OS-correct one.
288 Makes some attempts at finding an OS-correct one.
289 """
289 """
290 if os.name == 'posix':
290 if os.name == 'posix':
291 default_pager_cmd = 'less -R' # -R for color control sequences
291 default_pager_cmd = 'less -R' # -R for color control sequences
292 elif os.name in ['nt','dos']:
292 elif os.name in ['nt','dos']:
293 default_pager_cmd = 'type'
293 default_pager_cmd = 'type'
294
294
295 if pager_cmd is None:
295 if pager_cmd is None:
296 try:
296 try:
297 pager_cmd = os.environ['PAGER']
297 pager_cmd = os.environ['PAGER']
298 except:
298 except:
299 pager_cmd = default_pager_cmd
299 pager_cmd = default_pager_cmd
300
300
301 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
301 if pager_cmd == 'less' and '-r' not in os.environ.get('LESS', '').lower():
302 pager_cmd += ' -R'
302 pager_cmd += ' -R'
303
303
304 return pager_cmd
304 return pager_cmd
305
305
306
306
307 def get_pager_start(pager, start):
307 def get_pager_start(pager, start):
308 """Return the string for paging files with an offset.
308 """Return the string for paging files with an offset.
309
309
310 This is the '+N' argument which less and more (under Unix) accept.
310 This is the '+N' argument which less and more (under Unix) accept.
311 """
311 """
312
312
313 if pager in ['less','more']:
313 if pager in ['less','more']:
314 if start:
314 if start:
315 start_string = '+' + str(start)
315 start_string = '+' + str(start)
316 else:
316 else:
317 start_string = ''
317 start_string = ''
318 else:
318 else:
319 start_string = ''
319 start_string = ''
320 return start_string
320 return start_string
321
321
322
322
323 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
323 # (X)emacs on win32 doesn't like to be bypassed with msvcrt.getch()
324 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
324 if os.name == 'nt' and os.environ.get('TERM','dumb') != 'emacs':
325 import msvcrt
325 import msvcrt
326 def page_more():
326 def page_more():
327 """ Smart pausing between pages
327 """ Smart pausing between pages
328
328
329 @return: True if need print more lines, False if quit
329 @return: True if need print more lines, False if quit
330 """
330 """
331 sys.stdout.write('---Return to continue, q to quit--- ')
331 sys.stdout.write('---Return to continue, q to quit--- ')
332 ans = msvcrt.getwch()
332 ans = msvcrt.getwch()
333 if ans in ("q", "Q"):
333 if ans in ("q", "Q"):
334 result = False
334 result = False
335 else:
335 else:
336 result = True
336 result = True
337 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
337 sys.stdout.write("\b"*37 + " "*37 + "\b"*37)
338 return result
338 return result
339 else:
339 else:
340 def page_more():
340 def page_more():
341 ans = py3compat.input('---Return to continue, q to quit--- ')
341 ans = py3compat.input('---Return to continue, q to quit--- ')
342 if ans.lower().startswith('q'):
342 if ans.lower().startswith('q'):
343 return False
343 return False
344 else:
344 else:
345 return True
345 return True
General Comments 0
You need to be logged in to leave comments. Login now