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