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