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