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