##// END OF EJS Templates
Add test for unicode in logging.
Thomas Kluyver -
Show More
@@ -1,220 +1,220 b''
1 """Logger class for IPython's logging facilities.
1 """Logger class for IPython's logging facilities.
2 """
2 """
3
3
4 #*****************************************************************************
4 #*****************************************************************************
5 # Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and
5 # Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and
6 # Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu>
6 # Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu>
7 #
7 #
8 # Distributed under the terms of the BSD License. The full license is in
8 # Distributed under the terms of the BSD License. The full license is in
9 # the file COPYING, distributed as part of this software.
9 # the file COPYING, distributed as part of this software.
10 #*****************************************************************************
10 #*****************************************************************************
11
11
12 #****************************************************************************
12 #****************************************************************************
13 # Modules and globals
13 # Modules and globals
14
14
15 # Python standard modules
15 # Python standard modules
16 import glob
16 import glob
17 import io
17 import io
18 import os
18 import os
19 import time
19 import time
20
20
21 from IPython.utils.py3compat import str_to_unicode
21 from IPython.utils.py3compat import str_to_unicode
22
22
23 #****************************************************************************
23 #****************************************************************************
24 # FIXME: This class isn't a mixin anymore, but it still needs attributes from
24 # FIXME: This class isn't a mixin anymore, but it still needs attributes from
25 # ipython and does input cache management. Finish cleanup later...
25 # ipython and does input cache management. Finish cleanup later...
26
26
27 class Logger(object):
27 class Logger(object):
28 """A Logfile class with different policies for file creation"""
28 """A Logfile class with different policies for file creation"""
29
29
30 def __init__(self, home_dir, logfname='Logger.log', loghead=u'',
30 def __init__(self, home_dir, logfname='Logger.log', loghead=u'',
31 logmode='over'):
31 logmode='over'):
32
32
33 # this is the full ipython instance, we need some attributes from it
33 # this is the full ipython instance, we need some attributes from it
34 # which won't exist until later. What a mess, clean up later...
34 # which won't exist until later. What a mess, clean up later...
35 self.home_dir = home_dir
35 self.home_dir = home_dir
36
36
37 self.logfname = logfname
37 self.logfname = logfname
38 self.loghead = loghead
38 self.loghead = loghead
39 self.logmode = logmode
39 self.logmode = logmode
40 self.logfile = None
40 self.logfile = None
41
41
42 # Whether to log raw or processed input
42 # Whether to log raw or processed input
43 self.log_raw_input = False
43 self.log_raw_input = False
44
44
45 # whether to also log output
45 # whether to also log output
46 self.log_output = False
46 self.log_output = False
47
47
48 # whether to put timestamps before each log entry
48 # whether to put timestamps before each log entry
49 self.timestamp = False
49 self.timestamp = False
50
50
51 # activity control flags
51 # activity control flags
52 self.log_active = False
52 self.log_active = False
53
53
54 # logmode is a validated property
54 # logmode is a validated property
55 def _set_mode(self,mode):
55 def _set_mode(self,mode):
56 if mode not in ['append','backup','global','over','rotate']:
56 if mode not in ['append','backup','global','over','rotate']:
57 raise ValueError,'invalid log mode %s given' % mode
57 raise ValueError,'invalid log mode %s given' % mode
58 self._logmode = mode
58 self._logmode = mode
59
59
60 def _get_mode(self):
60 def _get_mode(self):
61 return self._logmode
61 return self._logmode
62
62
63 logmode = property(_get_mode,_set_mode)
63 logmode = property(_get_mode,_set_mode)
64
64
65 def logstart(self,logfname=None,loghead=None,logmode=None,
65 def logstart(self, logfname=None, loghead=None, logmode=None,
66 log_output=False,timestamp=False,log_raw_input=False):
66 log_output=False, timestamp=False, log_raw_input=False):
67 """Generate a new log-file with a default header.
67 """Generate a new log-file with a default header.
68
68
69 Raises RuntimeError if the log has already been started"""
69 Raises RuntimeError if the log has already been started"""
70
70
71 if self.logfile is not None:
71 if self.logfile is not None:
72 raise RuntimeError('Log file is already active: %s' %
72 raise RuntimeError('Log file is already active: %s' %
73 self.logfname)
73 self.logfname)
74
74
75 # The parameters can override constructor defaults
75 # The parameters can override constructor defaults
76 if logfname is not None: self.logfname = logfname
76 if logfname is not None: self.logfname = logfname
77 if loghead is not None: self.loghead = loghead
77 if loghead is not None: self.loghead = loghead
78 if logmode is not None: self.logmode = logmode
78 if logmode is not None: self.logmode = logmode
79
79
80 # Parameters not part of the constructor
80 # Parameters not part of the constructor
81 self.timestamp = timestamp
81 self.timestamp = timestamp
82 self.log_output = log_output
82 self.log_output = log_output
83 self.log_raw_input = log_raw_input
83 self.log_raw_input = log_raw_input
84
84
85 # init depending on the log mode requested
85 # init depending on the log mode requested
86 isfile = os.path.isfile
86 isfile = os.path.isfile
87 logmode = self.logmode
87 logmode = self.logmode
88
88
89 if logmode == 'append':
89 if logmode == 'append':
90 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
90 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
91
91
92 elif logmode == 'backup':
92 elif logmode == 'backup':
93 if isfile(self.logfname):
93 if isfile(self.logfname):
94 backup_logname = self.logfname+'~'
94 backup_logname = self.logfname+'~'
95 # Manually remove any old backup, since os.rename may fail
95 # Manually remove any old backup, since os.rename may fail
96 # under Windows.
96 # under Windows.
97 if isfile(backup_logname):
97 if isfile(backup_logname):
98 os.remove(backup_logname)
98 os.remove(backup_logname)
99 os.rename(self.logfname,backup_logname)
99 os.rename(self.logfname,backup_logname)
100 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
100 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
101
101
102 elif logmode == 'global':
102 elif logmode == 'global':
103 self.logfname = os.path.join(self.home_dir,self.logfname)
103 self.logfname = os.path.join(self.home_dir,self.logfname)
104 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
104 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
105
105
106 elif logmode == 'over':
106 elif logmode == 'over':
107 if isfile(self.logfname):
107 if isfile(self.logfname):
108 os.remove(self.logfname)
108 os.remove(self.logfname)
109 self.logfile = io.open(self.logfname,'w', encoding='utf-8')
109 self.logfile = io.open(self.logfname,'w', encoding='utf-8')
110
110
111 elif logmode == 'rotate':
111 elif logmode == 'rotate':
112 if isfile(self.logfname):
112 if isfile(self.logfname):
113 if isfile(self.logfname+'.001~'):
113 if isfile(self.logfname+'.001~'):
114 old = glob.glob(self.logfname+'.*~')
114 old = glob.glob(self.logfname+'.*~')
115 old.sort()
115 old.sort()
116 old.reverse()
116 old.reverse()
117 for f in old:
117 for f in old:
118 root, ext = os.path.splitext(f)
118 root, ext = os.path.splitext(f)
119 num = int(ext[1:-1])+1
119 num = int(ext[1:-1])+1
120 os.rename(f, root+'.'+`num`.zfill(3)+'~')
120 os.rename(f, root+'.'+`num`.zfill(3)+'~')
121 os.rename(self.logfname, self.logfname+'.001~')
121 os.rename(self.logfname, self.logfname+'.001~')
122 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
122 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
123
123
124 if logmode != 'append':
124 if logmode != 'append':
125 self.logfile.write(self.loghead)
125 self.logfile.write(self.loghead)
126
126
127 self.logfile.flush()
127 self.logfile.flush()
128 self.log_active = True
128 self.log_active = True
129
129
130 def switch_log(self,val):
130 def switch_log(self,val):
131 """Switch logging on/off. val should be ONLY a boolean."""
131 """Switch logging on/off. val should be ONLY a boolean."""
132
132
133 if val not in [False,True,0,1]:
133 if val not in [False,True,0,1]:
134 raise ValueError, \
134 raise ValueError, \
135 'Call switch_log ONLY with a boolean argument, not with:',val
135 'Call switch_log ONLY with a boolean argument, not with:',val
136
136
137 label = {0:'OFF',1:'ON',False:'OFF',True:'ON'}
137 label = {0:'OFF',1:'ON',False:'OFF',True:'ON'}
138
138
139 if self.logfile is None:
139 if self.logfile is None:
140 print """
140 print """
141 Logging hasn't been started yet (use logstart for that).
141 Logging hasn't been started yet (use logstart for that).
142
142
143 %logon/%logoff are for temporarily starting and stopping logging for a logfile
143 %logon/%logoff are for temporarily starting and stopping logging for a logfile
144 which already exists. But you must first start the logging process with
144 which already exists. But you must first start the logging process with
145 %logstart (optionally giving a logfile name)."""
145 %logstart (optionally giving a logfile name)."""
146
146
147 else:
147 else:
148 if self.log_active == val:
148 if self.log_active == val:
149 print 'Logging is already',label[val]
149 print 'Logging is already',label[val]
150 else:
150 else:
151 print 'Switching logging',label[val]
151 print 'Switching logging',label[val]
152 self.log_active = not self.log_active
152 self.log_active = not self.log_active
153 self.log_active_out = self.log_active
153 self.log_active_out = self.log_active
154
154
155 def logstate(self):
155 def logstate(self):
156 """Print a status message about the logger."""
156 """Print a status message about the logger."""
157 if self.logfile is None:
157 if self.logfile is None:
158 print 'Logging has not been activated.'
158 print 'Logging has not been activated.'
159 else:
159 else:
160 state = self.log_active and 'active' or 'temporarily suspended'
160 state = self.log_active and 'active' or 'temporarily suspended'
161 print 'Filename :',self.logfname
161 print 'Filename :',self.logfname
162 print 'Mode :',self.logmode
162 print 'Mode :',self.logmode
163 print 'Output logging :',self.log_output
163 print 'Output logging :',self.log_output
164 print 'Raw input log :',self.log_raw_input
164 print 'Raw input log :',self.log_raw_input
165 print 'Timestamping :',self.timestamp
165 print 'Timestamping :',self.timestamp
166 print 'State :',state
166 print 'State :',state
167
167
168 def log(self, line_mod, line_ori):
168 def log(self, line_mod, line_ori):
169 """Write the sources to a log.
169 """Write the sources to a log.
170
170
171 Inputs:
171 Inputs:
172
172
173 - line_mod: possibly modified input, such as the transformations made
173 - line_mod: possibly modified input, such as the transformations made
174 by input prefilters or input handlers of various kinds. This should
174 by input prefilters or input handlers of various kinds. This should
175 always be valid Python.
175 always be valid Python.
176
176
177 - line_ori: unmodified input line from the user. This is not
177 - line_ori: unmodified input line from the user. This is not
178 necessarily valid Python.
178 necessarily valid Python.
179 """
179 """
180
180
181 # Write the log line, but decide which one according to the
181 # Write the log line, but decide which one according to the
182 # log_raw_input flag, set when the log is started.
182 # log_raw_input flag, set when the log is started.
183 if self.log_raw_input:
183 if self.log_raw_input:
184 self.log_write(line_ori)
184 self.log_write(line_ori)
185 else:
185 else:
186 self.log_write(line_mod)
186 self.log_write(line_mod)
187
187
188 def log_write(self, data, kind='input'):
188 def log_write(self, data, kind='input'):
189 """Write data to the log file, if active"""
189 """Write data to the log file, if active"""
190
190
191 #print 'data: %r' % data # dbg
191 #print 'data: %r' % data # dbg
192 if self.log_active and data:
192 if self.log_active and data:
193 write = self.logfile.write
193 write = self.logfile.write
194 if kind=='input':
194 if kind=='input':
195 if self.timestamp:
195 if self.timestamp:
196 write(str_to_unicode(time.strftime('# %a, %d %b %Y %H:%M:%S\n',
196 write(str_to_unicode(time.strftime('# %a, %d %b %Y %H:%M:%S\n',
197 time.localtime())))
197 time.localtime())))
198 write(data)
198 write(data)
199 elif kind=='output' and self.log_output:
199 elif kind=='output' and self.log_output:
200 odata = u'\n'.join([u'#[Out]# %s' % s
200 odata = u'\n'.join([u'#[Out]# %s' % s
201 for s in data.splitlines()])
201 for s in data.splitlines()])
202 write(u'%s\n' % odata)
202 write(u'%s\n' % odata)
203 self.logfile.flush()
203 self.logfile.flush()
204
204
205 def logstop(self):
205 def logstop(self):
206 """Fully stop logging and close log file.
206 """Fully stop logging and close log file.
207
207
208 In order to start logging again, a new logstart() call needs to be
208 In order to start logging again, a new logstart() call needs to be
209 made, possibly (though not necessarily) with a new filename, mode and
209 made, possibly (though not necessarily) with a new filename, mode and
210 other options."""
210 other options."""
211
211
212 if self.logfile is not None:
212 if self.logfile is not None:
213 self.logfile.close()
213 self.logfile.close()
214 self.logfile = None
214 self.logfile = None
215 else:
215 else:
216 print "Logging hadn't been started."
216 print "Logging hadn't been started."
217 self.log_active = False
217 self.log_active = False
218
218
219 # For backwards compatibility, in case anyone was using this.
219 # For backwards compatibility, in case anyone was using this.
220 close_log = logstop
220 close_log = logstop
@@ -1,19 +1,31 b''
1 """Test IPython.core.logger"""
1 """Test IPython.core.logger"""
2
2
3 import os.path
4
3 import nose.tools as nt
5 import nose.tools as nt
6 from IPython.utils.tempdir import TemporaryDirectory
4
7
5 _ip = get_ipython()
8 _ip = get_ipython()
6
9
7 def test_logstart_inaccessible_file():
10 def test_logstart_inaccessible_file():
8 try:
11 try:
9 _ip.logger.logstart(logfname="/") # Opening that filename will fail.
12 _ip.logger.logstart(logfname="/") # Opening that filename will fail.
10 except IOError:
13 except IOError:
11 pass
14 pass
12 else:
15 else:
13 nt.assert_true(False) # The try block should never pass.
16 nt.assert_true(False) # The try block should never pass.
14
17
15 try:
18 try:
16 _ip.run_cell("a=1") # Check it doesn't try to log this
19 _ip.run_cell("a=1") # Check it doesn't try to log this
17 finally:
20 finally:
18 _ip.logger.log_active = False # If this fails, don't let later tests fail
21 _ip.logger.log_active = False # If this fails, don't let later tests fail
19
22
23 def test_logstart_unicode():
24 with TemporaryDirectory() as tdir:
25 logfname = os.path.join(tdir, "test_unicode.log")
26 _ip.run_cell("'abc€'")
27 try:
28 _ip.magic("logstart -to %s" % logfname)
29 _ip.run_cell("'abc€'")
30 finally:
31 _ip.logger.logstop()
General Comments 0
You need to be logged in to leave comments. Login now