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