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