##// END OF EJS Templates
allow access to user_ns in prompt_manager...
MinRK -
Show More
@@ -1,406 +1,429 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Classes for handling input/output prompts.
3 3
4 4 Authors:
5 5
6 6 * Fernando Perez
7 7 * Brian Granger
8 8 * Thomas Kluyver
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2011 The IPython Development Team
13 13 # Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu>
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import os
24 24 import re
25 25 import socket
26 26 import sys
27 27 import time
28 28
29 from string import Formatter
30
29 31 from IPython.config.configurable import Configurable
30 32 from IPython.core import release
31 33 from IPython.utils import coloransi
32 34 from IPython.utils.traitlets import (Unicode, Instance, Dict, Bool, Int)
33 35
34 36 #-----------------------------------------------------------------------------
35 37 # Color schemes for prompts
36 38 #-----------------------------------------------------------------------------
37 39
38 40 InputColors = coloransi.InputTermColors # just a shorthand
39 41 Colors = coloransi.TermColors # just a shorthand
40 42
41 43 color_lists = dict(normal=Colors(), inp=InputColors(), nocolor=coloransi.NoColors())
42 44
43 45 PColNoColors = coloransi.ColorScheme(
44 46 'NoColor',
45 47 in_prompt = InputColors.NoColor, # Input prompt
46 48 in_number = InputColors.NoColor, # Input prompt number
47 49 in_prompt2 = InputColors.NoColor, # Continuation prompt
48 50 in_normal = InputColors.NoColor, # color off (usu. Colors.Normal)
49 51
50 52 out_prompt = Colors.NoColor, # Output prompt
51 53 out_number = Colors.NoColor, # Output prompt number
52 54
53 55 normal = Colors.NoColor # color off (usu. Colors.Normal)
54 56 )
55 57
56 58 # make some schemes as instances so we can copy them for modification easily:
57 59 PColLinux = coloransi.ColorScheme(
58 60 'Linux',
59 61 in_prompt = InputColors.Green,
60 62 in_number = InputColors.LightGreen,
61 63 in_prompt2 = InputColors.Green,
62 64 in_normal = InputColors.Normal, # color off (usu. Colors.Normal)
63 65
64 66 out_prompt = Colors.Red,
65 67 out_number = Colors.LightRed,
66 68
67 69 normal = Colors.Normal
68 70 )
69 71
70 72 # Slightly modified Linux for light backgrounds
71 73 PColLightBG = PColLinux.copy('LightBG')
72 74
73 75 PColLightBG.colors.update(
74 76 in_prompt = InputColors.Blue,
75 77 in_number = InputColors.LightBlue,
76 78 in_prompt2 = InputColors.Blue
77 79 )
78 80
79 81 #-----------------------------------------------------------------------------
80 82 # Utilities
81 83 #-----------------------------------------------------------------------------
82 84
83 85 class LazyEvaluate(object):
84 86 """This is used for formatting strings with values that need to be updated
85 87 at that time, such as the current time or working directory."""
86 88 def __init__(self, func, *args, **kwargs):
87 89 self.func = func
88 90 self.args = args
89 91 self.kwargs = kwargs
90 92
91 93 def __call__(self, **kwargs):
92 94 self.kwargs.update(kwargs)
93 95 return self.func(*self.args, **self.kwargs)
94 96
95 97 def __str__(self):
96 98 return str(self())
97 99
98 100 def multiple_replace(dict, text):
99 101 """ Replace in 'text' all occurences of any key in the given
100 102 dictionary by its corresponding value. Returns the new string."""
101 103
102 104 # Function by Xavier Defrang, originally found at:
103 105 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81330
104 106
105 107 # Create a regular expression from the dictionary keys
106 108 regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))
107 109 # For each match, look-up corresponding value in dictionary
108 110 return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text)
109 111
110 112 #-----------------------------------------------------------------------------
111 113 # Special characters that can be used in prompt templates, mainly bash-like
112 114 #-----------------------------------------------------------------------------
113 115
114 116 # If $HOME isn't defined (Windows), make it an absurd string so that it can
115 117 # never be expanded out into '~'. Basically anything which can never be a
116 118 # reasonable directory name will do, we just want the $HOME -> '~' operation
117 119 # to become a no-op. We pre-compute $HOME here so it's not done on every
118 120 # prompt call.
119 121
120 122 # FIXME:
121 123
122 124 # - This should be turned into a class which does proper namespace management,
123 125 # since the prompt specials need to be evaluated in a certain namespace.
124 126 # Currently it's just globals, which need to be managed manually by code
125 127 # below.
126 128
127 129 # - I also need to split up the color schemes from the prompt specials
128 130 # somehow. I don't have a clean design for that quite yet.
129 131
130 132 HOME = os.environ.get("HOME","//////:::::ZZZZZ,,,~~~")
131 133
132 134 # We precompute a few more strings here for the prompt_specials, which are
133 135 # fixed once ipython starts. This reduces the runtime overhead of computing
134 136 # prompt strings.
135 137 USER = os.environ.get("USER")
136 138 HOSTNAME = socket.gethostname()
137 139 HOSTNAME_SHORT = HOSTNAME.split(".")[0]
138 140 ROOT_SYMBOL = "#" if (os.name=='nt' or os.getuid()==0) else "$"
139 141
140 142 prompt_abbreviations = {
141 143 # Prompt/history count
142 144 '%n' : '{color.number}' '{count}' '{color.prompt}',
143 145 r'\#': '{color.number}' '{count}' '{color.prompt}',
144 146 # Just the prompt counter number, WITHOUT any coloring wrappers, so users
145 147 # can get numbers displayed in whatever color they want.
146 148 r'\N': '{count}',
147 149
148 150 # Prompt/history count, with the actual digits replaced by dots. Used
149 151 # mainly in continuation prompts (prompt_in2)
150 152 r'\D': '{dots}',
151 153
152 154 # Current time
153 155 r'\T' : '{time}',
154 156 # Current working directory
155 157 r'\w': '{cwd}',
156 158 # Basename of current working directory.
157 159 # (use os.sep to make this portable across OSes)
158 160 r'\W' : '{cwd_last}',
159 161 # These X<N> are an extension to the normal bash prompts. They return
160 162 # N terms of the path, after replacing $HOME with '~'
161 163 r'\X0': '{cwd_x[0]}',
162 164 r'\X1': '{cwd_x[1]}',
163 165 r'\X2': '{cwd_x[2]}',
164 166 r'\X3': '{cwd_x[3]}',
165 167 r'\X4': '{cwd_x[4]}',
166 168 r'\X5': '{cwd_x[5]}',
167 169 # Y<N> are similar to X<N>, but they show '~' if it's the directory
168 170 # N+1 in the list. Somewhat like %cN in tcsh.
169 171 r'\Y0': '{cwd_y[0]}',
170 172 r'\Y1': '{cwd_y[1]}',
171 173 r'\Y2': '{cwd_y[2]}',
172 174 r'\Y3': '{cwd_y[3]}',
173 175 r'\Y4': '{cwd_y[4]}',
174 176 r'\Y5': '{cwd_y[5]}',
175 177 # Hostname up to first .
176 178 r'\h': HOSTNAME_SHORT,
177 179 # Full hostname
178 180 r'\H': HOSTNAME,
179 181 # Username of current user
180 182 r'\u': USER,
181 183 # Escaped '\'
182 184 '\\\\': '\\',
183 185 # Newline
184 186 r'\n': '\n',
185 187 # Carriage return
186 188 r'\r': '\r',
187 189 # Release version
188 190 r'\v': release.version,
189 191 # Root symbol ($ or #)
190 192 r'\$': ROOT_SYMBOL,
191 193 }
192 194
193 195 #-----------------------------------------------------------------------------
194 196 # More utilities
195 197 #-----------------------------------------------------------------------------
196 198
197 199 def cwd_filt(depth):
198 200 """Return the last depth elements of the current working directory.
199 201
200 202 $HOME is always replaced with '~'.
201 203 If depth==0, the full path is returned."""
202 204
203 205 cwd = os.getcwd().replace(HOME,"~")
204 206 out = os.sep.join(cwd.split(os.sep)[-depth:])
205 207 return out or os.sep
206 208
207 209 def cwd_filt2(depth):
208 210 """Return the last depth elements of the current working directory.
209 211
210 212 $HOME is always replaced with '~'.
211 213 If depth==0, the full path is returned."""
212 214
213 215 full_cwd = os.getcwd()
214 216 cwd = full_cwd.replace(HOME,"~").split(os.sep)
215 217 if '~' in cwd and len(cwd) == depth+1:
216 218 depth += 1
217 219 drivepart = ''
218 220 if sys.platform == 'win32' and len(cwd) > depth:
219 221 drivepart = os.path.splitdrive(full_cwd)[0]
220 222 out = drivepart + '/'.join(cwd[-depth:])
221 223
222 224 return out or os.sep
223 225
224 226 #-----------------------------------------------------------------------------
225 227 # Prompt classes
226 228 #-----------------------------------------------------------------------------
227 229
228 230 lazily_evaluate = {'time': LazyEvaluate(time.strftime, "%H:%M:%S"),
229 231 'cwd': LazyEvaluate(os.getcwd),
230 232 'cwd_last': LazyEvaluate(lambda: os.getcwd().split(os.sep)[-1]),
231 233 'cwd_x': [LazyEvaluate(lambda: os.getcwd().replace("%s","~"))] +\
232 234 [LazyEvaluate(cwd_filt, x) for x in range(1,6)],
233 235 'cwd_y': [LazyEvaluate(cwd_filt2, x) for x in range(6)]
234 236 }
235 237
236 238 def _lenlastline(s):
237 239 """Get the length of the last line. More intelligent than
238 240 len(s.splitlines()[-1]).
239 241 """
240 242 if not s or s.endswith(('\n', '\r')):
241 243 return 0
242 244 return len(s.splitlines()[-1])
243 245
246
247 class UserNSFormatter(Formatter):
248 """A Formatter that falls back on a shell's user_ns and __builtins__ for name resolution"""
249 def __init__(self, shell):
250 self.shell = shell
251
252 def get_value(self, key, args, kwargs):
253 # try regular formatting first:
254 try:
255 return Formatter.get_value(self, key, args, kwargs)
256 except Exception:
257 pass
258 # next, look in user_ns and builtins:
259 for container in (self.shell.user_ns, __builtins__):
260 if key in container:
261 return container[key]
262 # nothing found, put error message in its place
263 return "<ERROR: '%s' not found>" % key
264
265
244 266 class PromptManager(Configurable):
245 267 """This is the primary interface for producing IPython's prompts."""
246 268 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
247 269
248 270 color_scheme_table = Instance(coloransi.ColorSchemeTable)
249 271 color_scheme = Unicode('Linux', config=True)
250 272 def _color_scheme_changed(self, name, new_value):
251 273 self.color_scheme_table.set_active_scheme(new_value)
252 274 for pname in ['in', 'in2', 'out', 'rewrite']:
253 275 # We need to recalculate the number of invisible characters
254 276 self.update_prompt(pname)
255 277
256 278 lazy_evaluate_fields = Dict(help="""
257 279 This maps field names used in the prompt templates to functions which
258 280 will be called when the prompt is rendered. This allows us to include
259 281 things like the current time in the prompts. Functions are only called
260 282 if they are used in the prompt.
261 283 """)
262 284 def _lazy_evaluate_fields_default(self): return lazily_evaluate.copy()
263 285
264 286 in_template = Unicode('In [\\#]: ', config=True,
265 287 help="Input prompt. '\\#' will be transformed to the prompt number")
266 288 in2_template = Unicode(' .\\D.: ', config=True,
267 289 help="Continuation prompt.")
268 290 out_template = Unicode('Out[\\#]: ', config=True,
269 291 help="Output prompt. '\\#' will be transformed to the prompt number")
270 292
271 293 justify = Bool(True, config=True, help="""
272 294 If True (default), each prompt will be right-aligned with the
273 295 preceding one.
274 296 """)
275 297
276 298 # We actually store the expanded templates here:
277 299 templates = Dict()
278 300
279 301 # The number of characters in the last prompt rendered, not including
280 302 # colour characters.
281 303 width = Int()
282 304 txtwidth = Int() # Not including right-justification
283 305
284 306 # The number of characters in each prompt which don't contribute to width
285 307 invisible_chars = Dict()
286 308 def _invisible_chars_default(self):
287 309 return {'in': 0, 'in2': 0, 'out': 0, 'rewrite':0}
288 310
289 311 def __init__(self, shell, config=None):
290 312 super(PromptManager, self).__init__(shell=shell, config=config)
291 313
292 314 # Prepare colour scheme table
293 315 self.color_scheme_table = coloransi.ColorSchemeTable([PColNoColors,
294 316 PColLinux, PColLightBG], self.color_scheme)
295 317
318 self._formatter = UserNSFormatter(shell)
296 319 # Prepare templates & numbers of invisible characters
297 320 self.update_prompt('in', self.in_template)
298 321 self.update_prompt('in2', self.in2_template)
299 322 self.update_prompt('out', self.out_template)
300 323 self.update_prompt('rewrite')
301 324 self.on_trait_change(self._update_prompt_trait, ['in_template',
302 325 'in2_template', 'out_template'])
303 326
304 327 def update_prompt(self, name, new_template=None):
305 328 """This is called when a prompt template is updated. It processes
306 329 abbreviations used in the prompt template (like \#) and calculates how
307 330 many invisible characters (ANSI colour escapes) the resulting prompt
308 331 contains.
309 332
310 333 It is also called for each prompt on changing the colour scheme. In both
311 334 cases, traitlets should take care of calling this automatically.
312 335 """
313 336 if new_template is not None:
314 337 self.templates[name] = multiple_replace(prompt_abbreviations, new_template)
315 338 # We count invisible characters (colour escapes) on the last line of the
316 339 # prompt, to calculate the width for lining up subsequent prompts.
317 340 invis_chars = _lenlastline(self._render(name, color=True)) - \
318 341 _lenlastline(self._render(name, color=False))
319 342 self.invisible_chars[name] = invis_chars
320 343
321 344 def _update_prompt_trait(self, traitname, new_template):
322 345 name = traitname[:-9] # Cut off '_template'
323 346 self.update_prompt(name, new_template)
324 347
325 348 def _render(self, name, color=True, **kwargs):
326 349 """Render but don't justify, or update the width or txtwidth attributes.
327 350 """
328 351 if name == 'rewrite':
329 352 return self._render_rewrite(color=color)
330 353
331 354 if color:
332 355 scheme = self.color_scheme_table.active_colors
333 356 if name=='out':
334 357 colors = color_lists['normal']
335 358 colors.number, colors.prompt, colors.normal = \
336 359 scheme.out_number, scheme.out_prompt, scheme.normal
337 360 else:
338 361 colors = color_lists['inp']
339 362 colors.number, colors.prompt, colors.normal = \
340 363 scheme.in_number, scheme.in_prompt, scheme.in_normal
341 364 if name=='in2':
342 365 colors.prompt = scheme.in_prompt2
343 366 else:
344 367 # No color
345 368 colors = color_lists['nocolor']
346 369 colors.number, colors.prompt, colors.normal = '', '', ''
347 370
348 371 count = self.shell.execution_count # Shorthand
349 372 # Build the dictionary to be passed to string formatting
350 373 fmtargs = dict(color=colors, count=count,
351 374 dots="."*len(str(count)),
352 375 width=self.width, txtwidth=self.txtwidth )
353 376 fmtargs.update(self.lazy_evaluate_fields)
354 377 fmtargs.update(kwargs)
355 378
356 379 # Prepare the prompt
357 380 prompt = colors.prompt + self.templates[name] + colors.normal
358 381
359 382 # Fill in required fields
360 return prompt.format(**fmtargs)
383 return self._formatter.format(prompt, **fmtargs)
361 384
362 385 def _render_rewrite(self, color=True):
363 386 """Render the ---> rewrite prompt."""
364 387 if color:
365 388 scheme = self.color_scheme_table.active_colors
366 389 # We need a non-input version of these escapes
367 390 color_prompt = scheme.in_prompt.replace("\001","").replace("\002","")
368 391 color_normal = scheme.normal
369 392 else:
370 393 color_prompt, color_normal = '', ''
371 394
372 395 return color_prompt + "-> ".rjust(self.txtwidth, "-") + color_normal
373 396
374 397 def render(self, name, color=True, just=None, **kwargs):
375 398 """
376 399 Render the selected prompt.
377 400
378 401 Parameters
379 402 ----------
380 403 name : str
381 404 Which prompt to render. One of 'in', 'in2', 'out', 'rewrite'
382 405 color : bool
383 406 If True (default), include ANSI escape sequences for a coloured prompt.
384 407 just : bool
385 408 If True, justify the prompt to the width of the last prompt. The
386 409 default is stored in self.justify.
387 410 **kwargs :
388 411 Additional arguments will be passed to the string formatting operation,
389 412 so they can override the values that would otherwise fill in the
390 413 template.
391 414
392 415 Returns
393 416 -------
394 417 A string containing the rendered prompt.
395 418 """
396 419 res = self._render(name, color=color, **kwargs)
397 420
398 421 # Handle justification of prompt
399 422 invis_chars = self.invisible_chars[name] if color else 0
400 423 self.txtwidth = _lenlastline(res) - invis_chars
401 424 just = self.justify if (just is None) else just
402 425 # If the prompt spans more than one line, don't try to justify it:
403 426 if just and ('\n' not in res) and ('\r' not in res):
404 427 res = res.rjust(self.width + invis_chars)
405 428 self.width = _lenlastline(res) - invis_chars
406 429 return res
@@ -1,43 +1,62 b''
1 1 """Tests for prompt generation."""
2 2
3 3 import unittest
4 4
5 5 import nose.tools as nt
6 6
7 7 from IPython.testing import tools as tt, decorators as dec
8 8 from IPython.core.prompts import PromptManager
9 9 from IPython.testing.globalipapp import get_ipython
10 10
11 11 ip = get_ipython()
12 12
13 13
14 14 class PromptTests(unittest.TestCase):
15 15 def setUp(self):
16 16 self.pm = PromptManager(shell=ip, config=ip.config)
17 17
18 18 def test_multiline_prompt(self):
19 19 self.pm.in_template = "[In]\n>>>"
20 20 self.pm.render('in')
21 21 self.assertEqual(self.pm.width, 3)
22 22 self.assertEqual(self.pm.txtwidth, 3)
23 23
24 24 self.pm.in_template = '[In]\n'
25 25 self.pm.render('in')
26 26 self.assertEqual(self.pm.width, 0)
27 27 self.assertEqual(self.pm.txtwidth, 0)
28 28
29 29 def test_translate_abbreviations(self):
30 30 def do_translate(template):
31 31 self.pm.in_template = template
32 32 return self.pm.templates['in']
33 33
34 34 pairs = [(r'%n>', '{color.number}{count}{color.prompt}>'),
35 35 (r'\T', '{time}'),
36 36 (r'\n', '\n')
37 37 ]
38 38
39 39 tt.check_pairs(do_translate, pairs)
40 40
41 def test_user_ns(self):
42 self.pm.color_scheme = 'NoColor'
43 ip.ex("foo='bar'")
44 self.pm.in_template = "In [{foo}]"
45 prompt = self.pm.render('in')
46 self.assertEquals(prompt, u'In [bar]')
47
48 def test_builtins(self):
49 self.pm.color_scheme = 'NoColor'
50 self.pm.in_template = "In [{int}]"
51 prompt = self.pm.render('in')
52 self.assertEquals(prompt, u"In [<type 'int'>]")
53
54 def test_undefined(self):
55 self.pm.color_scheme = 'NoColor'
56 self.pm.in_template = "In [{foo_dne}]"
57 prompt = self.pm.render('in')
58 self.assertEquals(prompt, u"In [<ERROR: 'foo_dne' not found>]")
59
41 60 def test_render(self):
42 61 self.pm.in_template = r'\#>'
43 62 self.assertEqual(self.pm.render('in',color=False), '%d>' % ip.execution_count)
General Comments 0
You need to be logged in to leave comments. Login now