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