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