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