##// END OF EJS Templates
Update a few observer and .tag(config=True) to match new traitlets API.
Matthias Bussonnier -
Show More
@@ -1,311 +1,314 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 An application for managing IPython profiles.
3 An application for managing IPython profiles.
4
4
5 To be invoked as the `ipython profile` subcommand.
5 To be invoked as the `ipython profile` subcommand.
6
6
7 Authors:
7 Authors:
8
8
9 * Min RK
9 * Min RK
10
10
11 """
11 """
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Copyright (C) 2008 The IPython Development Team
15 # Copyright (C) 2008 The IPython Development Team
16 #
16 #
17 # Distributed under the terms of the BSD License. The full license is in
17 # Distributed under the terms of the BSD License. The full license is in
18 # the file COPYING, distributed as part of this software.
18 # the file COPYING, distributed as part of this software.
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20
20
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22 # Imports
22 # Imports
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24
24
25 import os
25 import os
26
26
27 from traitlets.config.application import Application
27 from traitlets.config.application import Application
28 from IPython.core.application import (
28 from IPython.core.application import (
29 BaseIPythonApplication, base_flags
29 BaseIPythonApplication, base_flags
30 )
30 )
31 from IPython.core.profiledir import ProfileDir
31 from IPython.core.profiledir import ProfileDir
32 from IPython.utils.importstring import import_item
32 from IPython.utils.importstring import import_item
33 from IPython.paths import get_ipython_dir, get_ipython_package_dir
33 from IPython.paths import get_ipython_dir, get_ipython_package_dir
34 from IPython.utils import py3compat
34 from IPython.utils import py3compat
35 from traitlets import Unicode, Bool, Dict
35 from traitlets import Unicode, Bool, Dict, observe
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Constants
38 # Constants
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40
40
41 create_help = """Create an IPython profile by name
41 create_help = """Create an IPython profile by name
42
42
43 Create an ipython profile directory by its name or
43 Create an ipython profile directory by its name or
44 profile directory path. Profile directories contain
44 profile directory path. Profile directories contain
45 configuration, log and security related files and are named
45 configuration, log and security related files and are named
46 using the convention 'profile_<name>'. By default they are
46 using the convention 'profile_<name>'. By default they are
47 located in your ipython directory. Once created, you will
47 located in your ipython directory. Once created, you will
48 can edit the configuration files in the profile
48 can edit the configuration files in the profile
49 directory to configure IPython. Most users will create a
49 directory to configure IPython. Most users will create a
50 profile directory by name,
50 profile directory by name,
51 `ipython profile create myprofile`, which will put the directory
51 `ipython profile create myprofile`, which will put the directory
52 in `<ipython_dir>/profile_myprofile`.
52 in `<ipython_dir>/profile_myprofile`.
53 """
53 """
54 list_help = """List available IPython profiles
54 list_help = """List available IPython profiles
55
55
56 List all available profiles, by profile location, that can
56 List all available profiles, by profile location, that can
57 be found in the current working directly or in the ipython
57 be found in the current working directly or in the ipython
58 directory. Profile directories are named using the convention
58 directory. Profile directories are named using the convention
59 'profile_<profile>'.
59 'profile_<profile>'.
60 """
60 """
61 profile_help = """Manage IPython profiles
61 profile_help = """Manage IPython profiles
62
62
63 Profile directories contain
63 Profile directories contain
64 configuration, log and security related files and are named
64 configuration, log and security related files and are named
65 using the convention 'profile_<name>'. By default they are
65 using the convention 'profile_<name>'. By default they are
66 located in your ipython directory. You can create profiles
66 located in your ipython directory. You can create profiles
67 with `ipython profile create <name>`, or see the profiles you
67 with `ipython profile create <name>`, or see the profiles you
68 already have with `ipython profile list`
68 already have with `ipython profile list`
69
69
70 To get started configuring IPython, simply do:
70 To get started configuring IPython, simply do:
71
71
72 $> ipython profile create
72 $> ipython profile create
73
73
74 and IPython will create the default profile in <ipython_dir>/profile_default,
74 and IPython will create the default profile in <ipython_dir>/profile_default,
75 where you can edit ipython_config.py to start configuring IPython.
75 where you can edit ipython_config.py to start configuring IPython.
76
76
77 """
77 """
78
78
79 _list_examples = "ipython profile list # list all profiles"
79 _list_examples = "ipython profile list # list all profiles"
80
80
81 _create_examples = """
81 _create_examples = """
82 ipython profile create foo # create profile foo w/ default config files
82 ipython profile create foo # create profile foo w/ default config files
83 ipython profile create foo --reset # restage default config files over current
83 ipython profile create foo --reset # restage default config files over current
84 ipython profile create foo --parallel # also stage parallel config files
84 ipython profile create foo --parallel # also stage parallel config files
85 """
85 """
86
86
87 _main_examples = """
87 _main_examples = """
88 ipython profile create -h # show the help string for the create subcommand
88 ipython profile create -h # show the help string for the create subcommand
89 ipython profile list -h # show the help string for the list subcommand
89 ipython profile list -h # show the help string for the list subcommand
90
90
91 ipython locate profile foo # print the path to the directory for profile 'foo'
91 ipython locate profile foo # print the path to the directory for profile 'foo'
92 """
92 """
93
93
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95 # Profile Application Class (for `ipython profile` subcommand)
95 # Profile Application Class (for `ipython profile` subcommand)
96 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
97
97
98
98
99 def list_profiles_in(path):
99 def list_profiles_in(path):
100 """list profiles in a given root directory"""
100 """list profiles in a given root directory"""
101 files = os.listdir(path)
101 files = os.listdir(path)
102 profiles = []
102 profiles = []
103 for f in files:
103 for f in files:
104 try:
104 try:
105 full_path = os.path.join(path, f)
105 full_path = os.path.join(path, f)
106 except UnicodeError:
106 except UnicodeError:
107 continue
107 continue
108 if os.path.isdir(full_path) and f.startswith('profile_'):
108 if os.path.isdir(full_path) and f.startswith('profile_'):
109 profiles.append(f.split('_',1)[-1])
109 profiles.append(f.split('_',1)[-1])
110 return profiles
110 return profiles
111
111
112
112
113 def list_bundled_profiles():
113 def list_bundled_profiles():
114 """list profiles that are bundled with IPython."""
114 """list profiles that are bundled with IPython."""
115 path = os.path.join(get_ipython_package_dir(), u'core', u'profile')
115 path = os.path.join(get_ipython_package_dir(), u'core', u'profile')
116 files = os.listdir(path)
116 files = os.listdir(path)
117 profiles = []
117 profiles = []
118 for profile in files:
118 for profile in files:
119 full_path = os.path.join(path, profile)
119 full_path = os.path.join(path, profile)
120 if os.path.isdir(full_path) and profile != "__pycache__":
120 if os.path.isdir(full_path) and profile != "__pycache__":
121 profiles.append(profile)
121 profiles.append(profile)
122 return profiles
122 return profiles
123
123
124
124
125 class ProfileLocate(BaseIPythonApplication):
125 class ProfileLocate(BaseIPythonApplication):
126 description = """print the path to an IPython profile dir"""
126 description = """print the path to an IPython profile dir"""
127
127
128 def parse_command_line(self, argv=None):
128 def parse_command_line(self, argv=None):
129 super(ProfileLocate, self).parse_command_line(argv)
129 super(ProfileLocate, self).parse_command_line(argv)
130 if self.extra_args:
130 if self.extra_args:
131 self.profile = self.extra_args[0]
131 self.profile = self.extra_args[0]
132
132
133 def start(self):
133 def start(self):
134 print(self.profile_dir.location)
134 print(self.profile_dir.location)
135
135
136
136
137 class ProfileList(Application):
137 class ProfileList(Application):
138 name = u'ipython-profile'
138 name = u'ipython-profile'
139 description = list_help
139 description = list_help
140 examples = _list_examples
140 examples = _list_examples
141
141
142 aliases = Dict({
142 aliases = Dict({
143 'ipython-dir' : 'ProfileList.ipython_dir',
143 'ipython-dir' : 'ProfileList.ipython_dir',
144 'log-level' : 'Application.log_level',
144 'log-level' : 'Application.log_level',
145 })
145 })
146 flags = Dict(dict(
146 flags = Dict(dict(
147 debug = ({'Application' : {'log_level' : 0}},
147 debug = ({'Application' : {'log_level' : 0}},
148 "Set Application.log_level to 0, maximizing log output."
148 "Set Application.log_level to 0, maximizing log output."
149 )
149 )
150 ))
150 ))
151
151
152 ipython_dir = Unicode(get_ipython_dir(), config=True,
152 ipython_dir = Unicode(get_ipython_dir(),
153 help="""
153 help="""
154 The name of the IPython directory. This directory is used for logging
154 The name of the IPython directory. This directory is used for logging
155 configuration (through profiles), history storage, etc. The default
155 configuration (through profiles), history storage, etc. The default
156 is usually $HOME/.ipython. This options can also be specified through
156 is usually $HOME/.ipython. This options can also be specified through
157 the environment variable IPYTHONDIR.
157 the environment variable IPYTHONDIR.
158 """
158 """
159 )
159 ).tag(config=True)
160
160
161
161
162 def _print_profiles(self, profiles):
162 def _print_profiles(self, profiles):
163 """print list of profiles, indented."""
163 """print list of profiles, indented."""
164 for profile in profiles:
164 for profile in profiles:
165 print(' %s' % profile)
165 print(' %s' % profile)
166
166
167 def list_profile_dirs(self):
167 def list_profile_dirs(self):
168 profiles = list_bundled_profiles()
168 profiles = list_bundled_profiles()
169 if profiles:
169 if profiles:
170 print()
170 print()
171 print("Available profiles in IPython:")
171 print("Available profiles in IPython:")
172 self._print_profiles(profiles)
172 self._print_profiles(profiles)
173 print()
173 print()
174 print(" The first request for a bundled profile will copy it")
174 print(" The first request for a bundled profile will copy it")
175 print(" into your IPython directory (%s)," % self.ipython_dir)
175 print(" into your IPython directory (%s)," % self.ipython_dir)
176 print(" where you can customize it.")
176 print(" where you can customize it.")
177
177
178 profiles = list_profiles_in(self.ipython_dir)
178 profiles = list_profiles_in(self.ipython_dir)
179 if profiles:
179 if profiles:
180 print()
180 print()
181 print("Available profiles in %s:" % self.ipython_dir)
181 print("Available profiles in %s:" % self.ipython_dir)
182 self._print_profiles(profiles)
182 self._print_profiles(profiles)
183
183
184 profiles = list_profiles_in(py3compat.getcwd())
184 profiles = list_profiles_in(py3compat.getcwd())
185 if profiles:
185 if profiles:
186 print()
186 print()
187 print("Available profiles in current directory (%s):" % py3compat.getcwd())
187 print("Available profiles in current directory (%s):" % py3compat.getcwd())
188 self._print_profiles(profiles)
188 self._print_profiles(profiles)
189
189
190 print()
190 print()
191 print("To use any of the above profiles, start IPython with:")
191 print("To use any of the above profiles, start IPython with:")
192 print(" ipython --profile=<name>")
192 print(" ipython --profile=<name>")
193 print()
193 print()
194
194
195 def start(self):
195 def start(self):
196 self.list_profile_dirs()
196 self.list_profile_dirs()
197
197
198
198
199 create_flags = {}
199 create_flags = {}
200 create_flags.update(base_flags)
200 create_flags.update(base_flags)
201 # don't include '--init' flag, which implies running profile create in other apps
201 # don't include '--init' flag, which implies running profile create in other apps
202 create_flags.pop('init')
202 create_flags.pop('init')
203 create_flags['reset'] = ({'ProfileCreate': {'overwrite' : True}},
203 create_flags['reset'] = ({'ProfileCreate': {'overwrite' : True}},
204 "reset config files in this profile to the defaults.")
204 "reset config files in this profile to the defaults.")
205 create_flags['parallel'] = ({'ProfileCreate': {'parallel' : True}},
205 create_flags['parallel'] = ({'ProfileCreate': {'parallel' : True}},
206 "Include the config files for parallel "
206 "Include the config files for parallel "
207 "computing apps (ipengine, ipcontroller, etc.)")
207 "computing apps (ipengine, ipcontroller, etc.)")
208
208
209
209
210 class ProfileCreate(BaseIPythonApplication):
210 class ProfileCreate(BaseIPythonApplication):
211 name = u'ipython-profile'
211 name = u'ipython-profile'
212 description = create_help
212 description = create_help
213 examples = _create_examples
213 examples = _create_examples
214 auto_create = Bool(True, config=False)
214 auto_create = Bool(True)
215 def _log_format_default(self):
215 def _log_format_default(self):
216 return "[%(name)s] %(message)s"
216 return "[%(name)s] %(message)s"
217
217
218 def _copy_config_files_default(self):
218 def _copy_config_files_default(self):
219 return True
219 return True
220
220
221 parallel = Bool(False, config=True,
221 parallel = Bool(False,
222 help="whether to include parallel computing config files")
222 help="whether to include parallel computing config files"
223 ).tag(config=True)
224
225 @observe('parallel')
223 def _parallel_changed(self, name, old, new):
226 def _parallel_changed(self, name, old, new):
224 parallel_files = [ 'ipcontroller_config.py',
227 parallel_files = [ 'ipcontroller_config.py',
225 'ipengine_config.py',
228 'ipengine_config.py',
226 'ipcluster_config.py'
229 'ipcluster_config.py'
227 ]
230 ]
228 if new:
231 if new:
229 for cf in parallel_files:
232 for cf in parallel_files:
230 self.config_files.append(cf)
233 self.config_files.append(cf)
231 else:
234 else:
232 for cf in parallel_files:
235 for cf in parallel_files:
233 if cf in self.config_files:
236 if cf in self.config_files:
234 self.config_files.remove(cf)
237 self.config_files.remove(cf)
235
238
236 def parse_command_line(self, argv):
239 def parse_command_line(self, argv):
237 super(ProfileCreate, self).parse_command_line(argv)
240 super(ProfileCreate, self).parse_command_line(argv)
238 # accept positional arg as profile name
241 # accept positional arg as profile name
239 if self.extra_args:
242 if self.extra_args:
240 self.profile = self.extra_args[0]
243 self.profile = self.extra_args[0]
241
244
242 flags = Dict(create_flags)
245 flags = Dict(create_flags)
243
246
244 classes = [ProfileDir]
247 classes = [ProfileDir]
245
248
246 def _import_app(self, app_path):
249 def _import_app(self, app_path):
247 """import an app class"""
250 """import an app class"""
248 app = None
251 app = None
249 name = app_path.rsplit('.', 1)[-1]
252 name = app_path.rsplit('.', 1)[-1]
250 try:
253 try:
251 app = import_item(app_path)
254 app = import_item(app_path)
252 except ImportError:
255 except ImportError:
253 self.log.info("Couldn't import %s, config file will be excluded", name)
256 self.log.info("Couldn't import %s, config file will be excluded", name)
254 except Exception:
257 except Exception:
255 self.log.warning('Unexpected error importing %s', name, exc_info=True)
258 self.log.warning('Unexpected error importing %s', name, exc_info=True)
256 return app
259 return app
257
260
258 def init_config_files(self):
261 def init_config_files(self):
259 super(ProfileCreate, self).init_config_files()
262 super(ProfileCreate, self).init_config_files()
260 # use local imports, since these classes may import from here
263 # use local imports, since these classes may import from here
261 from IPython.terminal.ipapp import TerminalIPythonApp
264 from IPython.terminal.ipapp import TerminalIPythonApp
262 apps = [TerminalIPythonApp]
265 apps = [TerminalIPythonApp]
263 for app_path in (
266 for app_path in (
264 'ipykernel.kernelapp.IPKernelApp',
267 'ipykernel.kernelapp.IPKernelApp',
265 ):
268 ):
266 app = self._import_app(app_path)
269 app = self._import_app(app_path)
267 if app is not None:
270 if app is not None:
268 apps.append(app)
271 apps.append(app)
269 if self.parallel:
272 if self.parallel:
270 from ipyparallel.apps.ipcontrollerapp import IPControllerApp
273 from ipyparallel.apps.ipcontrollerapp import IPControllerApp
271 from ipyparallel.apps.ipengineapp import IPEngineApp
274 from ipyparallel.apps.ipengineapp import IPEngineApp
272 from ipyparallel.apps.ipclusterapp import IPClusterStart
275 from ipyparallel.apps.ipclusterapp import IPClusterStart
273 apps.extend([
276 apps.extend([
274 IPControllerApp,
277 IPControllerApp,
275 IPEngineApp,
278 IPEngineApp,
276 IPClusterStart,
279 IPClusterStart,
277 ])
280 ])
278 for App in apps:
281 for App in apps:
279 app = App()
282 app = App()
280 app.config.update(self.config)
283 app.config.update(self.config)
281 app.log = self.log
284 app.log = self.log
282 app.overwrite = self.overwrite
285 app.overwrite = self.overwrite
283 app.copy_config_files=True
286 app.copy_config_files=True
284 app.ipython_dir=self.ipython_dir
287 app.ipython_dir=self.ipython_dir
285 app.profile_dir=self.profile_dir
288 app.profile_dir=self.profile_dir
286 app.init_config_files()
289 app.init_config_files()
287
290
288 def stage_default_config_file(self):
291 def stage_default_config_file(self):
289 pass
292 pass
290
293
291
294
292 class ProfileApp(Application):
295 class ProfileApp(Application):
293 name = u'ipython profile'
296 name = u'ipython profile'
294 description = profile_help
297 description = profile_help
295 examples = _main_examples
298 examples = _main_examples
296
299
297 subcommands = Dict(dict(
300 subcommands = Dict(dict(
298 create = (ProfileCreate, ProfileCreate.description.splitlines()[0]),
301 create = (ProfileCreate, ProfileCreate.description.splitlines()[0]),
299 list = (ProfileList, ProfileList.description.splitlines()[0]),
302 list = (ProfileList, ProfileList.description.splitlines()[0]),
300 locate = (ProfileLocate, ProfileLocate.description.splitlines()[0]),
303 locate = (ProfileLocate, ProfileLocate.description.splitlines()[0]),
301 ))
304 ))
302
305
303 def start(self):
306 def start(self):
304 if self.subapp is None:
307 if self.subapp is None:
305 print("No subcommand specified. Must specify one of: %s"%(self.subcommands.keys()))
308 print("No subcommand specified. Must specify one of: %s"%(self.subcommands.keys()))
306 print()
309 print()
307 self.print_description()
310 self.print_description()
308 self.print_subcommands()
311 self.print_subcommands()
309 self.exit(1)
312 self.exit(1)
310 else:
313 else:
311 return self.subapp.start()
314 return self.subapp.start()
@@ -1,409 +1,412 b''
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 # Copyright (c) 2001-2007 Fernando Perez <fperez@colorado.edu>
4 # Copyright (c) 2001-2007 Fernando Perez <fperez@colorado.edu>
5 # Copyright (c) IPython Development Team.
5 # Copyright (c) IPython Development Team.
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7
7
8 import os
8 import os
9 import re
9 import re
10 import socket
10 import socket
11 import sys
11 import sys
12 import time
12 import time
13
13
14 from string import Formatter
14 from string import Formatter
15
15
16 from traitlets.config.configurable import Configurable
16 from traitlets.config.configurable import Configurable
17 from IPython.core import release
17 from IPython.core import release
18 from IPython.utils import coloransi, py3compat
18 from IPython.utils import coloransi, py3compat
19 from traitlets import Unicode, Instance, Dict, Bool, Int, observe, default
19 from traitlets import Unicode, Instance, Dict, Bool, Int, observe, default
20
20
21 from IPython.utils.PyColorize import LightBGColors, LinuxColors, NoColor
21 from IPython.utils.PyColorize import LightBGColors, LinuxColors, NoColor
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Color schemes for prompts
24 # Color schemes for prompts
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 InputColors = coloransi.InputTermColors # just a shorthand
27 InputColors = coloransi.InputTermColors # just a shorthand
28 Colors = coloransi.TermColors # just a shorthand
28 Colors = coloransi.TermColors # just a shorthand
29
29
30 color_lists = dict(normal=Colors(), inp=InputColors(), nocolor=coloransi.NoColors())
30 color_lists = dict(normal=Colors(), inp=InputColors(), nocolor=coloransi.NoColors())
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Utilities
33 # Utilities
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 class LazyEvaluate(object):
36 class LazyEvaluate(object):
37 """This is used for formatting strings with values that need to be updated
37 """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."""
38 at that time, such as the current time or working directory."""
39 def __init__(self, func, *args, **kwargs):
39 def __init__(self, func, *args, **kwargs):
40 self.func = func
40 self.func = func
41 self.args = args
41 self.args = args
42 self.kwargs = kwargs
42 self.kwargs = kwargs
43
43
44 def __call__(self, **kwargs):
44 def __call__(self, **kwargs):
45 self.kwargs.update(kwargs)
45 self.kwargs.update(kwargs)
46 return self.func(*self.args, **self.kwargs)
46 return self.func(*self.args, **self.kwargs)
47
47
48 def __str__(self):
48 def __str__(self):
49 return str(self())
49 return str(self())
50
50
51 def __unicode__(self):
51 def __unicode__(self):
52 return py3compat.unicode_type(self())
52 return py3compat.unicode_type(self())
53
53
54 def __format__(self, format_spec):
54 def __format__(self, format_spec):
55 return format(self(), format_spec)
55 return format(self(), format_spec)
56
56
57 def multiple_replace(dict, text):
57 def multiple_replace(dict, text):
58 """ Replace in 'text' all occurrences of any key in the given
58 """ Replace in 'text' all occurrences of any key in the given
59 dictionary by its corresponding value. Returns the new string."""
59 dictionary by its corresponding value. Returns the new string."""
60
60
61 # Function by Xavier Defrang, originally found at:
61 # Function by Xavier Defrang, originally found at:
62 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81330
62 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81330
63
63
64 # Create a regular expression from the dictionary keys
64 # Create a regular expression from the dictionary keys
65 regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))
65 regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))
66 # For each match, look-up corresponding value in dictionary
66 # For each match, look-up corresponding value in dictionary
67 return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text)
67 return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text)
68
68
69 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
70 # Special characters that can be used in prompt templates, mainly bash-like
70 # Special characters that can be used in prompt templates, mainly bash-like
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72
72
73 # If $HOME isn't defined (Windows), make it an absurd string so that it can
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
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
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
76 # to become a no-op. We pre-compute $HOME here so it's not done on every
77 # prompt call.
77 # prompt call.
78
78
79 # FIXME:
79 # FIXME:
80
80
81 # - This should be turned into a class which does proper namespace management,
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.
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
83 # Currently it's just globals, which need to be managed manually by code
84 # below.
84 # below.
85
85
86 # - I also need to split up the color schemes from the prompt specials
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.
87 # somehow. I don't have a clean design for that quite yet.
88
88
89 HOME = py3compat.str_to_unicode(os.environ.get("HOME","//////:::::ZZZZZ,,,~~~"))
89 HOME = py3compat.str_to_unicode(os.environ.get("HOME","//////:::::ZZZZZ,,,~~~"))
90
90
91 # This is needed on FreeBSD, and maybe other systems which symlink /home to
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
92 # /usr/home, but retain the $HOME variable as pointing to /home
93 HOME = os.path.realpath(HOME)
93 HOME = os.path.realpath(HOME)
94
94
95 # We precompute a few more strings here for the prompt_specials, which are
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
96 # fixed once ipython starts. This reduces the runtime overhead of computing
97 # prompt strings.
97 # prompt strings.
98 USER = py3compat.str_to_unicode(os.environ.get("USER",''))
98 USER = py3compat.str_to_unicode(os.environ.get("USER",''))
99 HOSTNAME = py3compat.str_to_unicode(socket.gethostname())
99 HOSTNAME = py3compat.str_to_unicode(socket.gethostname())
100 HOSTNAME_SHORT = HOSTNAME.split(".")[0]
100 HOSTNAME_SHORT = HOSTNAME.split(".")[0]
101
101
102 # IronPython doesn't currently have os.getuid() even if
102 # IronPython doesn't currently have os.getuid() even if
103 # os.name == 'posix'; 2/8/2014
103 # os.name == 'posix'; 2/8/2014
104 ROOT_SYMBOL = "#" if (os.name=='nt' or sys.platform=='cli' or os.getuid()==0) else "$"
104 ROOT_SYMBOL = "#" if (os.name=='nt' or sys.platform=='cli' or os.getuid()==0) else "$"
105
105
106 prompt_abbreviations = {
106 prompt_abbreviations = {
107 # Prompt/history count
107 # Prompt/history count
108 '%n' : '{color.number}' '{count}' '{color.prompt}',
108 '%n' : '{color.number}' '{count}' '{color.prompt}',
109 r'\#': '{color.number}' '{count}' '{color.prompt}',
109 r'\#': '{color.number}' '{count}' '{color.prompt}',
110 # Just the prompt counter number, WITHOUT any coloring wrappers, so users
110 # Just the prompt counter number, WITHOUT any coloring wrappers, so users
111 # can get numbers displayed in whatever color they want.
111 # can get numbers displayed in whatever color they want.
112 r'\N': '{count}',
112 r'\N': '{count}',
113
113
114 # Prompt/history count, with the actual digits replaced by dots or
114 # Prompt/history count, with the actual digits replaced by dots or
115 # spaces. Used mainly in continuation prompts (prompt_in2).
115 # spaces. Used mainly in continuation prompts (prompt_in2).
116 r'\D': '{dots}',
116 r'\D': '{dots}',
117 r'\S': '{spaces}',
117 r'\S': '{spaces}',
118
118
119 # Current time
119 # Current time
120 r'\T' : '{time}',
120 r'\T' : '{time}',
121 # Current working directory
121 # Current working directory
122 r'\w': '{cwd}',
122 r'\w': '{cwd}',
123 # Basename of current working directory.
123 # Basename of current working directory.
124 # (use os.sep to make this portable across OSes)
124 # (use os.sep to make this portable across OSes)
125 r'\W' : '{cwd_last}',
125 r'\W' : '{cwd_last}',
126 # These X<N> are an extension to the normal bash prompts. They return
126 # These X<N> are an extension to the normal bash prompts. They return
127 # N terms of the path, after replacing $HOME with '~'
127 # N terms of the path, after replacing $HOME with '~'
128 r'\X0': '{cwd_x[0]}',
128 r'\X0': '{cwd_x[0]}',
129 r'\X1': '{cwd_x[1]}',
129 r'\X1': '{cwd_x[1]}',
130 r'\X2': '{cwd_x[2]}',
130 r'\X2': '{cwd_x[2]}',
131 r'\X3': '{cwd_x[3]}',
131 r'\X3': '{cwd_x[3]}',
132 r'\X4': '{cwd_x[4]}',
132 r'\X4': '{cwd_x[4]}',
133 r'\X5': '{cwd_x[5]}',
133 r'\X5': '{cwd_x[5]}',
134 # Y<N> are similar to X<N>, but they show '~' if it's the directory
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.
135 # N+1 in the list. Somewhat like %cN in tcsh.
136 r'\Y0': '{cwd_y[0]}',
136 r'\Y0': '{cwd_y[0]}',
137 r'\Y1': '{cwd_y[1]}',
137 r'\Y1': '{cwd_y[1]}',
138 r'\Y2': '{cwd_y[2]}',
138 r'\Y2': '{cwd_y[2]}',
139 r'\Y3': '{cwd_y[3]}',
139 r'\Y3': '{cwd_y[3]}',
140 r'\Y4': '{cwd_y[4]}',
140 r'\Y4': '{cwd_y[4]}',
141 r'\Y5': '{cwd_y[5]}',
141 r'\Y5': '{cwd_y[5]}',
142 # Hostname up to first .
142 # Hostname up to first .
143 r'\h': HOSTNAME_SHORT,
143 r'\h': HOSTNAME_SHORT,
144 # Full hostname
144 # Full hostname
145 r'\H': HOSTNAME,
145 r'\H': HOSTNAME,
146 # Username of current user
146 # Username of current user
147 r'\u': USER,
147 r'\u': USER,
148 # Escaped '\'
148 # Escaped '\'
149 '\\\\': '\\',
149 '\\\\': '\\',
150 # Newline
150 # Newline
151 r'\n': '\n',
151 r'\n': '\n',
152 # Carriage return
152 # Carriage return
153 r'\r': '\r',
153 r'\r': '\r',
154 # Release version
154 # Release version
155 r'\v': release.version,
155 r'\v': release.version,
156 # Root symbol ($ or #)
156 # Root symbol ($ or #)
157 r'\$': ROOT_SYMBOL,
157 r'\$': ROOT_SYMBOL,
158 }
158 }
159
159
160 #-----------------------------------------------------------------------------
160 #-----------------------------------------------------------------------------
161 # More utilities
161 # More utilities
162 #-----------------------------------------------------------------------------
162 #-----------------------------------------------------------------------------
163
163
164 def cwd_filt(depth):
164 def cwd_filt(depth):
165 """Return the last depth elements of the current working directory.
165 """Return the last depth elements of the current working directory.
166
166
167 $HOME is always replaced with '~'.
167 $HOME is always replaced with '~'.
168 If depth==0, the full path is returned."""
168 If depth==0, the full path is returned."""
169
169
170 cwd = py3compat.getcwd().replace(HOME,"~")
170 cwd = py3compat.getcwd().replace(HOME,"~")
171 out = os.sep.join(cwd.split(os.sep)[-depth:])
171 out = os.sep.join(cwd.split(os.sep)[-depth:])
172 return out or os.sep
172 return out or os.sep
173
173
174 def cwd_filt2(depth):
174 def cwd_filt2(depth):
175 """Return the last depth elements of the current working directory.
175 """Return the last depth elements of the current working directory.
176
176
177 $HOME is always replaced with '~'.
177 $HOME is always replaced with '~'.
178 If depth==0, the full path is returned."""
178 If depth==0, the full path is returned."""
179
179
180 full_cwd = py3compat.getcwd()
180 full_cwd = py3compat.getcwd()
181 cwd = full_cwd.replace(HOME,"~").split(os.sep)
181 cwd = full_cwd.replace(HOME,"~").split(os.sep)
182 if '~' in cwd and len(cwd) == depth+1:
182 if '~' in cwd and len(cwd) == depth+1:
183 depth += 1
183 depth += 1
184 drivepart = ''
184 drivepart = ''
185 if sys.platform == 'win32' and len(cwd) > depth:
185 if sys.platform == 'win32' and len(cwd) > depth:
186 drivepart = os.path.splitdrive(full_cwd)[0]
186 drivepart = os.path.splitdrive(full_cwd)[0]
187 out = drivepart + '/'.join(cwd[-depth:])
187 out = drivepart + '/'.join(cwd[-depth:])
188
188
189 return out or os.sep
189 return out or os.sep
190
190
191 #-----------------------------------------------------------------------------
191 #-----------------------------------------------------------------------------
192 # Prompt classes
192 # Prompt classes
193 #-----------------------------------------------------------------------------
193 #-----------------------------------------------------------------------------
194
194
195 lazily_evaluate = {'time': LazyEvaluate(time.strftime, "%H:%M:%S"),
195 lazily_evaluate = {'time': LazyEvaluate(time.strftime, "%H:%M:%S"),
196 'cwd': LazyEvaluate(py3compat.getcwd),
196 'cwd': LazyEvaluate(py3compat.getcwd),
197 'cwd_last': LazyEvaluate(lambda: py3compat.getcwd().split(os.sep)[-1]),
197 'cwd_last': LazyEvaluate(lambda: py3compat.getcwd().split(os.sep)[-1]),
198 'cwd_x': [LazyEvaluate(lambda: py3compat.getcwd().replace(HOME,"~"))] +\
198 'cwd_x': [LazyEvaluate(lambda: py3compat.getcwd().replace(HOME,"~"))] +\
199 [LazyEvaluate(cwd_filt, x) for x in range(1,6)],
199 [LazyEvaluate(cwd_filt, x) for x in range(1,6)],
200 'cwd_y': [LazyEvaluate(cwd_filt2, x) for x in range(6)]
200 'cwd_y': [LazyEvaluate(cwd_filt2, x) for x in range(6)]
201 }
201 }
202
202
203 def _lenlastline(s):
203 def _lenlastline(s):
204 """Get the length of the last line. More intelligent than
204 """Get the length of the last line. More intelligent than
205 len(s.splitlines()[-1]).
205 len(s.splitlines()[-1]).
206 """
206 """
207 if not s or s.endswith(('\n', '\r')):
207 if not s or s.endswith(('\n', '\r')):
208 return 0
208 return 0
209 return len(s.splitlines()[-1])
209 return len(s.splitlines()[-1])
210
210
211
211
212 invisible_chars_re = re.compile('\001[^\001\002]*\002')
212 invisible_chars_re = re.compile('\001[^\001\002]*\002')
213 def _invisible_characters(s):
213 def _invisible_characters(s):
214 """
214 """
215 Get the number of invisible ANSI characters in s. Invisible characters
215 Get the number of invisible ANSI characters in s. Invisible characters
216 must be delimited by \001 and \002.
216 must be delimited by \001 and \002.
217 """
217 """
218 return _lenlastline(s) - _lenlastline(invisible_chars_re.sub('', s))
218 return _lenlastline(s) - _lenlastline(invisible_chars_re.sub('', s))
219
219
220 class UserNSFormatter(Formatter):
220 class UserNSFormatter(Formatter):
221 """A Formatter that falls back on a shell's user_ns and __builtins__ for name resolution"""
221 """A Formatter that falls back on a shell's user_ns and __builtins__ for name resolution"""
222 def __init__(self, shell):
222 def __init__(self, shell):
223 self.shell = shell
223 self.shell = shell
224
224
225 def get_value(self, key, args, kwargs):
225 def get_value(self, key, args, kwargs):
226 # try regular formatting first:
226 # try regular formatting first:
227 try:
227 try:
228 return Formatter.get_value(self, key, args, kwargs)
228 return Formatter.get_value(self, key, args, kwargs)
229 except Exception:
229 except Exception:
230 pass
230 pass
231 # next, look in user_ns and builtins:
231 # next, look in user_ns and builtins:
232 for container in (self.shell.user_ns, __builtins__):
232 for container in (self.shell.user_ns, __builtins__):
233 if key in container:
233 if key in container:
234 return container[key]
234 return container[key]
235 # nothing found, put error message in its place
235 # nothing found, put error message in its place
236 return "<ERROR: '%s' not found>" % key
236 return "<ERROR: '%s' not found>" % key
237
237
238
238
239 class PromptManager(Configurable):
239 class PromptManager(Configurable):
240 """This is the primary interface for producing IPython's prompts."""
240 """This is the primary interface for producing IPython's prompts."""
241 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC', allow_none=True)
241 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC', allow_none=True)
242
242
243 color_scheme_table = Instance(coloransi.ColorSchemeTable, allow_none=True)
243 color_scheme_table = Instance(coloransi.ColorSchemeTable, allow_none=True)
244 color_scheme = Unicode('Linux').tag(config=True)
244 color_scheme = Unicode('Linux').tag(config=True)
245
245
246 @observe('color_scheme')
246 @observe('color_scheme')
247 def _color_scheme_changed(self, change):
247 def _color_scheme_changed(self, change):
248 self.color_scheme_table.set_active_scheme(change['new'])
248 self.color_scheme_table.set_active_scheme(change['new'])
249 for pname in ['in', 'in2', 'out', 'rewrite']:
249 for pname in ['in', 'in2', 'out', 'rewrite']:
250 # We need to recalculate the number of invisible characters
250 # We need to recalculate the number of invisible characters
251 self.update_prompt(pname)
251 self.update_prompt(pname)
252
252
253 lazy_evaluate_fields = Dict(help="""
253 lazy_evaluate_fields = Dict(help="""
254 This maps field names used in the prompt templates to functions which
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
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
256 things like the current time in the prompts. Functions are only called
257 if they are used in the prompt.
257 if they are used in the prompt.
258 """)
258 """)
259
259
260 in_template = Unicode('In [\\#]: ',
260 in_template = Unicode('In [\\#]: ',
261 help="Input prompt. '\\#' will be transformed to the prompt number"
261 help="Input prompt. '\\#' will be transformed to the prompt number"
262 ).tag(config=True)
262 ).tag(config=True)
263 in2_template = Unicode(' .\\D.: ',
263 in2_template = Unicode(' .\\D.: ',
264 help="Continuation prompt.").tag(config=True)
264 help="Continuation prompt.").tag(config=True)
265 out_template = Unicode('Out[\\#]: ',
265 out_template = Unicode('Out[\\#]: ',
266 help="Output prompt. '\\#' will be transformed to the prompt number"
266 help="Output prompt. '\\#' will be transformed to the prompt number"
267 ).tag(config=True)
267 ).tag(config=True)
268
268
269 @default('lazy_evaluate_fields')
269 @default('lazy_evaluate_fields')
270 def _lazy_evaluate_fields_default(self):
270 def _lazy_evaluate_fields_default(self):
271 return lazily_evaluate.copy()
271 return lazily_evaluate.copy()
272
272
273 justify = Bool(True, help="""
273 justify = Bool(True, help="""
274 If True (default), each prompt will be right-aligned with the
274 If True (default), each prompt will be right-aligned with the
275 preceding one.
275 preceding one.
276 """).tag(config=True)
276 """).tag(config=True)
277
277
278 # We actually store the expanded templates here:
278 # We actually store the expanded templates here:
279 templates = Dict()
279 templates = Dict()
280
280
281 # The number of characters in the last prompt rendered, not including
281 # The number of characters in the last prompt rendered, not including
282 # colour characters.
282 # colour characters.
283 width = Int()
283 width = Int()
284 txtwidth = Int() # Not including right-justification
284 txtwidth = Int() # Not including right-justification
285
285
286 # The number of characters in each prompt which don't contribute to width
286 # The number of characters in each prompt which don't contribute to width
287 invisible_chars = Dict()
287 invisible_chars = Dict()
288
288 @default('invisible_chars')
289 @default('invisible_chars')
289 def _invisible_chars_default(self):
290 def _invisible_chars_default(self):
290 return {'in': 0, 'in2': 0, 'out': 0, 'rewrite':0}
291 return {'in': 0, 'in2': 0, 'out': 0, 'rewrite':0}
291
292
292 def __init__(self, shell, **kwargs):
293 def __init__(self, shell, **kwargs):
293 super(PromptManager, self).__init__(shell=shell, **kwargs)
294 super(PromptManager, self).__init__(shell=shell, **kwargs)
294
295
295 # Prepare colour scheme table
296 # Prepare colour scheme table
296 self.color_scheme_table = coloransi.ColorSchemeTable([NoColor,
297 self.color_scheme_table = coloransi.ColorSchemeTable([NoColor,
297 LinuxColors, LightBGColors], self.color_scheme)
298 LinuxColors, LightBGColors], self.color_scheme)
298
299
299 self._formatter = UserNSFormatter(shell)
300 self._formatter = UserNSFormatter(shell)
300 # Prepare templates & numbers of invisible characters
301 # Prepare templates & numbers of invisible characters
301 self.update_prompt('in', self.in_template)
302 self.update_prompt('in', self.in_template)
302 self.update_prompt('in2', self.in2_template)
303 self.update_prompt('in2', self.in2_template)
303 self.update_prompt('out', self.out_template)
304 self.update_prompt('out', self.out_template)
304 self.update_prompt('rewrite')
305 self.update_prompt('rewrite')
305 self.observe(self._update_prompt_trait,
306 self.observe(self._update_prompt_trait,
306 names=['in_template', 'in2_template', 'out_template'])
307 names=['in_template', 'in2_template', 'out_template'])
307
308
308 def update_prompt(self, name, new_template=None):
309 def update_prompt(self, name, new_template=None):
309 """This is called when a prompt template is updated. It processes
310 """This is called when a prompt template is updated. It processes
310 abbreviations used in the prompt template (like \#) and calculates how
311 abbreviations used in the prompt template (like \#) and calculates how
311 many invisible characters (ANSI colour escapes) the resulting prompt
312 many invisible characters (ANSI colour escapes) the resulting prompt
312 contains.
313 contains.
313
314
314 It is also called for each prompt on changing the colour scheme. In both
315 It is also called for each prompt on changing the colour scheme. In both
315 cases, traitlets should take care of calling this automatically.
316 cases, traitlets should take care of calling this automatically.
316 """
317 """
317 if new_template is not None:
318 if new_template is not None:
318 self.templates[name] = multiple_replace(prompt_abbreviations, new_template)
319 self.templates[name] = multiple_replace(prompt_abbreviations, new_template)
319 # We count invisible characters (colour escapes) on the last line of the
320 # We count invisible characters (colour escapes) on the last line of the
320 # prompt, to calculate the width for lining up subsequent prompts.
321 # prompt, to calculate the width for lining up subsequent prompts.
321 invis_chars = _invisible_characters(self._render(name, color=True))
322 invis_chars = _invisible_characters(self._render(name, color=True))
322 self.invisible_chars[name] = invis_chars
323 self.invisible_chars[name] = invis_chars
323
324
324 def _update_prompt_trait(self, traitname, new_template):
325 def _update_prompt_trait(self, changes):
326 traitname = changes['name']
327 new_template = changes['new']
325 name = traitname[:-9] # Cut off '_template'
328 name = traitname[:-9] # Cut off '_template'
326 self.update_prompt(name, new_template)
329 self.update_prompt(name, new_template)
327
330
328 def _render(self, name, color=True, **kwargs):
331 def _render(self, name, color=True, **kwargs):
329 """Render but don't justify, or update the width or txtwidth attributes.
332 """Render but don't justify, or update the width or txtwidth attributes.
330 """
333 """
331 if name == 'rewrite':
334 if name == 'rewrite':
332 return self._render_rewrite(color=color)
335 return self._render_rewrite(color=color)
333
336
334 if color:
337 if color:
335 scheme = self.color_scheme_table.active_colors
338 scheme = self.color_scheme_table.active_colors
336 if name=='out':
339 if name=='out':
337 colors = color_lists['normal']
340 colors = color_lists['normal']
338 colors.number, colors.prompt, colors.normal = \
341 colors.number, colors.prompt, colors.normal = \
339 scheme.out_number, scheme.out_prompt, scheme.normal
342 scheme.out_number, scheme.out_prompt, scheme.normal
340 else:
343 else:
341 colors = color_lists['inp']
344 colors = color_lists['inp']
342 colors.number, colors.prompt, colors.normal = \
345 colors.number, colors.prompt, colors.normal = \
343 scheme.in_number, scheme.in_prompt, scheme.in_normal
346 scheme.in_number, scheme.in_prompt, scheme.in_normal
344 if name=='in2':
347 if name=='in2':
345 colors.prompt = scheme.in_prompt2
348 colors.prompt = scheme.in_prompt2
346 else:
349 else:
347 # No color
350 # No color
348 colors = color_lists['nocolor']
351 colors = color_lists['nocolor']
349 colors.number, colors.prompt, colors.normal = '', '', ''
352 colors.number, colors.prompt, colors.normal = '', '', ''
350
353
351 count = self.shell.execution_count # Shorthand
354 count = self.shell.execution_count # Shorthand
352 # Build the dictionary to be passed to string formatting
355 # Build the dictionary to be passed to string formatting
353 fmtargs = dict(color=colors, count=count,
356 fmtargs = dict(color=colors, count=count,
354 dots="."*len(str(count)), spaces=" "*len(str(count)),
357 dots="."*len(str(count)), spaces=" "*len(str(count)),
355 width=self.width, txtwidth=self.txtwidth)
358 width=self.width, txtwidth=self.txtwidth)
356 fmtargs.update(self.lazy_evaluate_fields)
359 fmtargs.update(self.lazy_evaluate_fields)
357 fmtargs.update(kwargs)
360 fmtargs.update(kwargs)
358
361
359 # Prepare the prompt
362 # Prepare the prompt
360 prompt = colors.prompt + self.templates[name] + colors.normal
363 prompt = colors.prompt + self.templates[name] + colors.normal
361
364
362 # Fill in required fields
365 # Fill in required fields
363 return self._formatter.format(prompt, **fmtargs)
366 return self._formatter.format(prompt, **fmtargs)
364
367
365 def _render_rewrite(self, color=True):
368 def _render_rewrite(self, color=True):
366 """Render the ---> rewrite prompt."""
369 """Render the ---> rewrite prompt."""
367 if color:
370 if color:
368 scheme = self.color_scheme_table.active_colors
371 scheme = self.color_scheme_table.active_colors
369 # We need a non-input version of these escapes
372 # We need a non-input version of these escapes
370 color_prompt = scheme.in_prompt.replace("\001","").replace("\002","")
373 color_prompt = scheme.in_prompt.replace("\001","").replace("\002","")
371 color_normal = scheme.normal
374 color_normal = scheme.normal
372 else:
375 else:
373 color_prompt, color_normal = '', ''
376 color_prompt, color_normal = '', ''
374
377
375 return color_prompt + "-> ".rjust(self.txtwidth, "-") + color_normal
378 return color_prompt + "-> ".rjust(self.txtwidth, "-") + color_normal
376
379
377 def render(self, name, color=True, just=None, **kwargs):
380 def render(self, name, color=True, just=None, **kwargs):
378 """
381 """
379 Render the selected prompt.
382 Render the selected prompt.
380
383
381 Parameters
384 Parameters
382 ----------
385 ----------
383 name : str
386 name : str
384 Which prompt to render. One of 'in', 'in2', 'out', 'rewrite'
387 Which prompt to render. One of 'in', 'in2', 'out', 'rewrite'
385 color : bool
388 color : bool
386 If True (default), include ANSI escape sequences for a coloured prompt.
389 If True (default), include ANSI escape sequences for a coloured prompt.
387 just : bool
390 just : bool
388 If True, justify the prompt to the width of the last prompt. The
391 If True, justify the prompt to the width of the last prompt. The
389 default is stored in self.justify.
392 default is stored in self.justify.
390 **kwargs :
393 **kwargs :
391 Additional arguments will be passed to the string formatting operation,
394 Additional arguments will be passed to the string formatting operation,
392 so they can override the values that would otherwise fill in the
395 so they can override the values that would otherwise fill in the
393 template.
396 template.
394
397
395 Returns
398 Returns
396 -------
399 -------
397 A string containing the rendered prompt.
400 A string containing the rendered prompt.
398 """
401 """
399 res = self._render(name, color=color, **kwargs)
402 res = self._render(name, color=color, **kwargs)
400
403
401 # Handle justification of prompt
404 # Handle justification of prompt
402 invis_chars = self.invisible_chars[name] if color else 0
405 invis_chars = self.invisible_chars[name] if color else 0
403 self.txtwidth = _lenlastline(res) - invis_chars
406 self.txtwidth = _lenlastline(res) - invis_chars
404 just = self.justify if (just is None) else just
407 just = self.justify if (just is None) else just
405 # If the prompt spans more than one line, don't try to justify it:
408 # If the prompt spans more than one line, don't try to justify it:
406 if just and name != 'in' and ('\n' not in res) and ('\r' not in res):
409 if just and name != 'in' and ('\n' not in res) and ('\r' not in res):
407 res = res.rjust(self.width + invis_chars)
410 res = res.rjust(self.width + invis_chars)
408 self.width = _lenlastline(res) - invis_chars
411 self.width = _lenlastline(res) - invis_chars
409 return res
412 return res
General Comments 0
You need to be logged in to leave comments. Login now