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