##// END OF EJS Templates
merge with crew-stable
Dirkjan Ochtman -
r9316:23cf7b52 merge default
parent child Browse files
Show More
@@ -1,293 +1,298 b''
1 # notify.py - email notifications for mercurial
1 # notify.py - email notifications for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''hooks for sending email notifications at commit/push time
8 '''hooks for sending email notifications at commit/push time
9
9
10 Subscriptions can be managed through a hgrc file. Default mode is to
10 Subscriptions can be managed through a hgrc file. Default mode is to
11 print messages to stdout, for testing and configuring.
11 print messages to stdout, for testing and configuring.
12
12
13 To use, configure the notify extension and enable it in hgrc like
13 To use, configure the notify extension and enable it in hgrc like
14 this::
14 this::
15
15
16 [extensions]
16 [extensions]
17 hgext.notify =
17 hgext.notify =
18
18
19 [hooks]
19 [hooks]
20 # one email for each incoming changeset
20 # one email for each incoming changeset
21 incoming.notify = python:hgext.notify.hook
21 incoming.notify = python:hgext.notify.hook
22 # batch emails when many changesets incoming at one time
22 # batch emails when many changesets incoming at one time
23 changegroup.notify = python:hgext.notify.hook
23 changegroup.notify = python:hgext.notify.hook
24
24
25 [notify]
25 [notify]
26 # config items go here
26 # config items go here
27
27
28 Required configuration items::
28 Required configuration items::
29
29
30 config = /path/to/file # file containing subscriptions
30 config = /path/to/file # file containing subscriptions
31
31
32 Optional configuration items::
32 Optional configuration items::
33
33
34 test = True # print messages to stdout for testing
34 test = True # print messages to stdout for testing
35 strip = 3 # number of slashes to strip for url paths
35 strip = 3 # number of slashes to strip for url paths
36 domain = example.com # domain to use if committer missing domain
36 domain = example.com # domain to use if committer missing domain
37 style = ... # style file to use when formatting email
37 style = ... # style file to use when formatting email
38 template = ... # template to use when formatting email
38 template = ... # template to use when formatting email
39 incoming = ... # template to use when run as incoming hook
39 incoming = ... # template to use when run as incoming hook
40 changegroup = ... # template when run as changegroup hook
40 changegroup = ... # template when run as changegroup hook
41 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 maxsubject = 67 # truncate subject line longer than this
42 maxsubject = 67 # truncate subject line longer than this
43 diffstat = True # add a diffstat before the diff content
43 diffstat = True # add a diffstat before the diff content
44 sources = serve # notify if source of incoming changes in this list
44 sources = serve # notify if source of incoming changes in this list
45 # (serve == ssh or http, push, pull, bundle)
45 # (serve == ssh or http, push, pull, bundle)
46 [email]
46 [email]
47 from = user@host.com # email address to send as if none given
47 from = user@host.com # email address to send as if none given
48 [web]
48 [web]
49 baseurl = http://hgserver/... # root of hg web site for browsing commits
49 baseurl = http://hgserver/... # root of hg web site for browsing commits
50
50
51 The notify config file has same format as a regular hgrc file. It has
51 The notify config file has same format as a regular hgrc file. It has
52 two sections so you can express subscriptions in whatever way is
52 two sections so you can express subscriptions in whatever way is
53 handier for you.
53 handier for you.
54
54
55 ::
55 ::
56
56
57 [usersubs]
57 [usersubs]
58 # key is subscriber email, value is ","-separated list of glob patterns
58 # key is subscriber email, value is ","-separated list of glob patterns
59 user@host = pattern
59 user@host = pattern
60
60
61 [reposubs]
61 [reposubs]
62 # key is glob pattern, value is ","-separated list of subscriber emails
62 # key is glob pattern, value is ","-separated list of subscriber emails
63 pattern = user@host
63 pattern = user@host
64
64
65 Glob patterns are matched against path to repository root.
65 Glob patterns are matched against path to repository root.
66
66
67 If you like, you can put notify config file in repository that users
67 If you like, you can put notify config file in repository that users
68 can push changes to, they can manage their own subscriptions.
68 can push changes to, they can manage their own subscriptions.
69 '''
69 '''
70
70
71 from mercurial.i18n import _
71 from mercurial.i18n import _
72 from mercurial import patch, cmdutil, templater, util, mail
72 from mercurial import patch, cmdutil, templater, util, mail
73 import email.Parser, fnmatch, socket, time
73 import email.Parser, email.Errors, fnmatch, socket, time
74
74
75 # template for single changeset can include email headers.
75 # template for single changeset can include email headers.
76 single_template = '''
76 single_template = '''
77 Subject: changeset in {webroot}: {desc|firstline|strip}
77 Subject: changeset in {webroot}: {desc|firstline|strip}
78 From: {author}
78 From: {author}
79
79
80 changeset {node|short} in {root}
80 changeset {node|short} in {root}
81 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
81 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
82 description:
82 description:
83 \t{desc|tabindent|strip}
83 \t{desc|tabindent|strip}
84 '''.lstrip()
84 '''.lstrip()
85
85
86 # template for multiple changesets should not contain email headers,
86 # template for multiple changesets should not contain email headers,
87 # because only first set of headers will be used and result will look
87 # because only first set of headers will be used and result will look
88 # strange.
88 # strange.
89 multiple_template = '''
89 multiple_template = '''
90 changeset {node|short} in {root}
90 changeset {node|short} in {root}
91 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
91 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
92 summary: {desc|firstline}
92 summary: {desc|firstline}
93 '''
93 '''
94
94
95 deftemplates = {
95 deftemplates = {
96 'changegroup': multiple_template,
96 'changegroup': multiple_template,
97 }
97 }
98
98
99 class notifier(object):
99 class notifier(object):
100 '''email notification class.'''
100 '''email notification class.'''
101
101
102 def __init__(self, ui, repo, hooktype):
102 def __init__(self, ui, repo, hooktype):
103 self.ui = ui
103 self.ui = ui
104 cfg = self.ui.config('notify', 'config')
104 cfg = self.ui.config('notify', 'config')
105 if cfg:
105 if cfg:
106 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
106 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
107 self.repo = repo
107 self.repo = repo
108 self.stripcount = int(self.ui.config('notify', 'strip', 0))
108 self.stripcount = int(self.ui.config('notify', 'strip', 0))
109 self.root = self.strip(self.repo.root)
109 self.root = self.strip(self.repo.root)
110 self.domain = self.ui.config('notify', 'domain')
110 self.domain = self.ui.config('notify', 'domain')
111 self.test = self.ui.configbool('notify', 'test', True)
111 self.test = self.ui.configbool('notify', 'test', True)
112 self.charsets = mail._charsets(self.ui)
112 self.charsets = mail._charsets(self.ui)
113 self.subs = self.subscribers()
113 self.subs = self.subscribers()
114
114
115 mapfile = self.ui.config('notify', 'style')
115 mapfile = self.ui.config('notify', 'style')
116 template = (self.ui.config('notify', hooktype) or
116 template = (self.ui.config('notify', hooktype) or
117 self.ui.config('notify', 'template'))
117 self.ui.config('notify', 'template'))
118 self.t = cmdutil.changeset_templater(self.ui, self.repo,
118 self.t = cmdutil.changeset_templater(self.ui, self.repo,
119 False, None, mapfile, False)
119 False, None, mapfile, False)
120 if not mapfile and not template:
120 if not mapfile and not template:
121 template = deftemplates.get(hooktype) or single_template
121 template = deftemplates.get(hooktype) or single_template
122 if template:
122 if template:
123 template = templater.parsestring(template, quoted=False)
123 template = templater.parsestring(template, quoted=False)
124 self.t.use_template(template)
124 self.t.use_template(template)
125
125
126 def strip(self, path):
126 def strip(self, path):
127 '''strip leading slashes from local path, turn into web-safe path.'''
127 '''strip leading slashes from local path, turn into web-safe path.'''
128
128
129 path = util.pconvert(path)
129 path = util.pconvert(path)
130 count = self.stripcount
130 count = self.stripcount
131 while count > 0:
131 while count > 0:
132 c = path.find('/')
132 c = path.find('/')
133 if c == -1:
133 if c == -1:
134 break
134 break
135 path = path[c+1:]
135 path = path[c+1:]
136 count -= 1
136 count -= 1
137 return path
137 return path
138
138
139 def fixmail(self, addr):
139 def fixmail(self, addr):
140 '''try to clean up email addresses.'''
140 '''try to clean up email addresses.'''
141
141
142 addr = util.email(addr.strip())
142 addr = util.email(addr.strip())
143 if self.domain:
143 if self.domain:
144 a = addr.find('@localhost')
144 a = addr.find('@localhost')
145 if a != -1:
145 if a != -1:
146 addr = addr[:a]
146 addr = addr[:a]
147 if '@' not in addr:
147 if '@' not in addr:
148 return addr + '@' + self.domain
148 return addr + '@' + self.domain
149 return addr
149 return addr
150
150
151 def subscribers(self):
151 def subscribers(self):
152 '''return list of email addresses of subscribers to this repo.'''
152 '''return list of email addresses of subscribers to this repo.'''
153 subs = set()
153 subs = set()
154 for user, pats in self.ui.configitems('usersubs'):
154 for user, pats in self.ui.configitems('usersubs'):
155 for pat in pats.split(','):
155 for pat in pats.split(','):
156 if fnmatch.fnmatch(self.repo.root, pat.strip()):
156 if fnmatch.fnmatch(self.repo.root, pat.strip()):
157 subs.add(self.fixmail(user))
157 subs.add(self.fixmail(user))
158 for pat, users in self.ui.configitems('reposubs'):
158 for pat, users in self.ui.configitems('reposubs'):
159 if fnmatch.fnmatch(self.repo.root, pat):
159 if fnmatch.fnmatch(self.repo.root, pat):
160 for user in users.split(','):
160 for user in users.split(','):
161 subs.add(self.fixmail(user))
161 subs.add(self.fixmail(user))
162 return [mail.addressencode(self.ui, s, self.charsets, self.test)
162 return [mail.addressencode(self.ui, s, self.charsets, self.test)
163 for s in sorted(subs)]
163 for s in sorted(subs)]
164
164
165 def url(self, path=None):
165 def url(self, path=None):
166 return self.ui.config('web', 'baseurl') + (path or self.root)
166 return self.ui.config('web', 'baseurl') + (path or self.root)
167
167
168 def node(self, ctx):
168 def node(self, ctx):
169 '''format one changeset.'''
169 '''format one changeset.'''
170 self.t.show(ctx, changes=ctx.changeset(),
170 self.t.show(ctx, changes=ctx.changeset(),
171 baseurl=self.ui.config('web', 'baseurl'),
171 baseurl=self.ui.config('web', 'baseurl'),
172 root=self.repo.root, webroot=self.root)
172 root=self.repo.root, webroot=self.root)
173
173
174 def skipsource(self, source):
174 def skipsource(self, source):
175 '''true if incoming changes from this source should be skipped.'''
175 '''true if incoming changes from this source should be skipped.'''
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 return source not in ok_sources
177 return source not in ok_sources
178
178
179 def send(self, ctx, count, data):
179 def send(self, ctx, count, data):
180 '''send message.'''
180 '''send message.'''
181
181
182 p = email.Parser.Parser()
182 p = email.Parser.Parser()
183 msg = p.parsestr(data)
183 try:
184 msg = p.parsestr(data)
185 except email.Errors.MessageParseError, inst:
186 raise util.Abort(inst)
184
187
185 # store sender and subject
188 # store sender and subject
186 sender, subject = msg['From'], msg['Subject']
189 sender, subject = msg['From'], msg['Subject']
187 del msg['From'], msg['Subject']
190 del msg['From'], msg['Subject']
188 # store remaining headers
191
189 headers = msg.items()
192 if not msg.is_multipart():
190 # create fresh mime message from msg body
193 # create fresh mime message from scratch
191 text = msg.get_payload()
194 # (multipart templates must take care of this themselves)
192 # for notification prefer readability over data precision
195 headers = msg.items()
193 msg = mail.mimeencode(self.ui, text, self.charsets, self.test)
196 payload = msg.get_payload()
194 # reinstate custom headers
197 # for notification prefer readability over data precision
195 for k, v in headers:
198 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
196 msg[k] = v
199 # reinstate custom headers
200 for k, v in headers:
201 msg[k] = v
197
202
198 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
199
204
200 # try to make subject line exist and be useful
205 # try to make subject line exist and be useful
201 if not subject:
206 if not subject:
202 if count > 1:
207 if count > 1:
203 subject = _('%s: %d new changesets') % (self.root, count)
208 subject = _('%s: %d new changesets') % (self.root, count)
204 else:
209 else:
205 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
210 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
206 subject = '%s: %s' % (self.root, s)
211 subject = '%s: %s' % (self.root, s)
207 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
212 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
208 if maxsubject and len(subject) > maxsubject:
213 if maxsubject and len(subject) > maxsubject:
209 subject = subject[:maxsubject-3] + '...'
214 subject = subject[:maxsubject-3] + '...'
210 msg['Subject'] = mail.headencode(self.ui, subject,
215 msg['Subject'] = mail.headencode(self.ui, subject,
211 self.charsets, self.test)
216 self.charsets, self.test)
212
217
213 # try to make message have proper sender
218 # try to make message have proper sender
214 if not sender:
219 if not sender:
215 sender = self.ui.config('email', 'from') or self.ui.username()
220 sender = self.ui.config('email', 'from') or self.ui.username()
216 if '@' not in sender or '@localhost' in sender:
221 if '@' not in sender or '@localhost' in sender:
217 sender = self.fixmail(sender)
222 sender = self.fixmail(sender)
218 msg['From'] = mail.addressencode(self.ui, sender,
223 msg['From'] = mail.addressencode(self.ui, sender,
219 self.charsets, self.test)
224 self.charsets, self.test)
220
225
221 msg['X-Hg-Notification'] = 'changeset %s' % ctx
226 msg['X-Hg-Notification'] = 'changeset %s' % ctx
222 if not msg['Message-Id']:
227 if not msg['Message-Id']:
223 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
228 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
224 (ctx, int(time.time()),
229 (ctx, int(time.time()),
225 hash(self.repo.root), socket.getfqdn()))
230 hash(self.repo.root), socket.getfqdn()))
226 msg['To'] = ', '.join(self.subs)
231 msg['To'] = ', '.join(self.subs)
227
232
228 msgtext = msg.as_string()
233 msgtext = msg.as_string()
229 if self.test:
234 if self.test:
230 self.ui.write(msgtext)
235 self.ui.write(msgtext)
231 if not msgtext.endswith('\n'):
236 if not msgtext.endswith('\n'):
232 self.ui.write('\n')
237 self.ui.write('\n')
233 else:
238 else:
234 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
239 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
235 (len(self.subs), count))
240 (len(self.subs), count))
236 mail.sendmail(self.ui, util.email(msg['From']),
241 mail.sendmail(self.ui, util.email(msg['From']),
237 self.subs, msgtext)
242 self.subs, msgtext)
238
243
239 def diff(self, ctx, ref=None):
244 def diff(self, ctx, ref=None):
240
245
241 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
246 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
242 prev = ctx.parents()[0].node()
247 prev = ctx.parents()[0].node()
243 ref = ref and ref.node() or ctx.node()
248 ref = ref and ref.node() or ctx.node()
244 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
249 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
245 difflines = ''.join(chunks).splitlines()
250 difflines = ''.join(chunks).splitlines()
246
251
247 if self.ui.configbool('notify', 'diffstat', True):
252 if self.ui.configbool('notify', 'diffstat', True):
248 s = patch.diffstat(difflines)
253 s = patch.diffstat(difflines)
249 # s may be nil, don't include the header if it is
254 # s may be nil, don't include the header if it is
250 if s:
255 if s:
251 self.ui.write('\ndiffstat:\n\n%s' % s)
256 self.ui.write('\ndiffstat:\n\n%s' % s)
252
257
253 if maxdiff == 0:
258 if maxdiff == 0:
254 return
259 return
255 elif maxdiff > 0 and len(difflines) > maxdiff:
260 elif maxdiff > 0 and len(difflines) > maxdiff:
256 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
261 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
257 self.ui.write(msg % (len(difflines), maxdiff))
262 self.ui.write(msg % (len(difflines), maxdiff))
258 difflines = difflines[:maxdiff]
263 difflines = difflines[:maxdiff]
259 elif difflines:
264 elif difflines:
260 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
265 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
261
266
262 self.ui.write("\n".join(difflines))
267 self.ui.write("\n".join(difflines))
263
268
264 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
269 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
265 '''send email notifications to interested subscribers.
270 '''send email notifications to interested subscribers.
266
271
267 if used as changegroup hook, send one email for all changesets in
272 if used as changegroup hook, send one email for all changesets in
268 changegroup. else send one email per changeset.'''
273 changegroup. else send one email per changeset.'''
269
274
270 n = notifier(ui, repo, hooktype)
275 n = notifier(ui, repo, hooktype)
271 ctx = repo[node]
276 ctx = repo[node]
272
277
273 if not n.subs:
278 if not n.subs:
274 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
279 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
275 return
280 return
276 if n.skipsource(source):
281 if n.skipsource(source):
277 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
282 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
278 return
283 return
279
284
280 ui.pushbuffer()
285 ui.pushbuffer()
281 if hooktype == 'changegroup':
286 if hooktype == 'changegroup':
282 start, end = ctx.rev(), len(repo)
287 start, end = ctx.rev(), len(repo)
283 count = end - start
288 count = end - start
284 for rev in xrange(start, end):
289 for rev in xrange(start, end):
285 n.node(repo[rev])
290 n.node(repo[rev])
286 n.diff(ctx, repo['tip'])
291 n.diff(ctx, repo['tip'])
287 else:
292 else:
288 count = 1
293 count = 1
289 n.node(ctx)
294 n.node(ctx)
290 n.diff(ctx)
295 n.diff(ctx)
291
296
292 data = ui.popbuffer()
297 data = ui.popbuffer()
293 n.send(ctx, count, data)
298 n.send(ctx, count, data)
@@ -1,136 +1,139 b''
1 # demandimport.py - global demand-loading of modules for Mercurial
1 # demandimport.py - global demand-loading of modules for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''
8 '''
9 demandimport - automatic demandloading of modules
9 demandimport - automatic demandloading of modules
10
10
11 To enable this module, do:
11 To enable this module, do:
12
12
13 import demandimport; demandimport.enable()
13 import demandimport; demandimport.enable()
14
14
15 Imports of the following forms will be demand-loaded:
15 Imports of the following forms will be demand-loaded:
16
16
17 import a, b.c
17 import a, b.c
18 import a.b as c
18 import a.b as c
19 from a import b,c # a will be loaded immediately
19 from a import b,c # a will be loaded immediately
20
20
21 These imports will not be delayed:
21 These imports will not be delayed:
22
22
23 from a import *
23 from a import *
24 b = __import__(a)
24 b = __import__(a)
25 '''
25 '''
26
26
27 import __builtin__
27 import __builtin__
28 _origimport = __import__
28 _origimport = __import__
29
29
30 class _demandmod(object):
30 class _demandmod(object):
31 """module demand-loader and proxy"""
31 """module demand-loader and proxy"""
32 def __init__(self, name, globals, locals):
32 def __init__(self, name, globals, locals):
33 if '.' in name:
33 if '.' in name:
34 head, rest = name.split('.', 1)
34 head, rest = name.split('.', 1)
35 after = [rest]
35 after = [rest]
36 else:
36 else:
37 head = name
37 head = name
38 after = []
38 after = []
39 object.__setattr__(self, "_data", (head, globals, locals, after))
39 object.__setattr__(self, "_data", (head, globals, locals, after))
40 object.__setattr__(self, "_module", None)
40 object.__setattr__(self, "_module", None)
41 def _extend(self, name):
41 def _extend(self, name):
42 """add to the list of submodules to load"""
42 """add to the list of submodules to load"""
43 self._data[3].append(name)
43 self._data[3].append(name)
44 def _load(self):
44 def _load(self):
45 if not self._module:
45 if not self._module:
46 head, globals, locals, after = self._data
46 head, globals, locals, after = self._data
47 mod = _origimport(head, globals, locals)
47 mod = _origimport(head, globals, locals)
48 # load submodules
48 # load submodules
49 def subload(mod, p):
49 def subload(mod, p):
50 h, t = p, None
50 h, t = p, None
51 if '.' in p:
51 if '.' in p:
52 h, t = p.split('.', 1)
52 h, t = p.split('.', 1)
53 if not hasattr(mod, h):
53 if not hasattr(mod, h):
54 setattr(mod, h, _demandmod(p, mod.__dict__, mod.__dict__))
54 setattr(mod, h, _demandmod(p, mod.__dict__, mod.__dict__))
55 elif t:
55 elif t:
56 subload(getattr(mod, h), t)
56 subload(getattr(mod, h), t)
57
57
58 for x in after:
58 for x in after:
59 subload(mod, x)
59 subload(mod, x)
60
60
61 # are we in the locals dictionary still?
61 # are we in the locals dictionary still?
62 if locals and locals.get(head) == self:
62 if locals and locals.get(head) == self:
63 locals[head] = mod
63 locals[head] = mod
64 object.__setattr__(self, "_module", mod)
64 object.__setattr__(self, "_module", mod)
65
65
66 def __repr__(self):
66 def __repr__(self):
67 if self._module:
67 if self._module:
68 return "<proxied module '%s'>" % self._data[0]
68 return "<proxied module '%s'>" % self._data[0]
69 return "<unloaded module '%s'>" % self._data[0]
69 return "<unloaded module '%s'>" % self._data[0]
70 def __call__(self, *args, **kwargs):
70 def __call__(self, *args, **kwargs):
71 raise TypeError("%s object is not callable" % repr(self))
71 raise TypeError("%s object is not callable" % repr(self))
72 def __getattribute__(self, attr):
72 def __getattribute__(self, attr):
73 if attr in ('_data', '_extend', '_load', '_module'):
73 if attr in ('_data', '_extend', '_load', '_module'):
74 return object.__getattribute__(self, attr)
74 return object.__getattribute__(self, attr)
75 self._load()
75 self._load()
76 return getattr(self._module, attr)
76 return getattr(self._module, attr)
77 def __setattr__(self, attr, val):
77 def __setattr__(self, attr, val):
78 self._load()
78 self._load()
79 setattr(self._module, attr, val)
79 setattr(self._module, attr, val)
80
80
81 def _demandimport(name, globals=None, locals=None, fromlist=None, level=None):
81 def _demandimport(name, globals=None, locals=None, fromlist=None, level=None):
82 if not locals or name in ignore or fromlist == ('*',):
82 if not locals or name in ignore or fromlist == ('*',):
83 # these cases we can't really delay
83 # these cases we can't really delay
84 return _origimport(name, globals, locals, fromlist)
84 if level is None:
85 return _origimport(name, globals, locals, fromlist)
86 else:
87 return _origimport(name, globals, locals, fromlist, level)
85 elif not fromlist:
88 elif not fromlist:
86 # import a [as b]
89 # import a [as b]
87 if '.' in name: # a.b
90 if '.' in name: # a.b
88 base, rest = name.split('.', 1)
91 base, rest = name.split('.', 1)
89 # email.__init__ loading email.mime
92 # email.__init__ loading email.mime
90 if globals and globals.get('__name__', None) == base:
93 if globals and globals.get('__name__', None) == base:
91 return _origimport(name, globals, locals, fromlist)
94 return _origimport(name, globals, locals, fromlist)
92 # if a is already demand-loaded, add b to its submodule list
95 # if a is already demand-loaded, add b to its submodule list
93 if base in locals:
96 if base in locals:
94 if isinstance(locals[base], _demandmod):
97 if isinstance(locals[base], _demandmod):
95 locals[base]._extend(rest)
98 locals[base]._extend(rest)
96 return locals[base]
99 return locals[base]
97 return _demandmod(name, globals, locals)
100 return _demandmod(name, globals, locals)
98 else:
101 else:
99 if level is not None:
102 if level is not None:
100 # from . import b,c,d or from .a import b,c,d
103 # from . import b,c,d or from .a import b,c,d
101 return _origimport(name, globals, locals, fromlist, level)
104 return _origimport(name, globals, locals, fromlist, level)
102 # from a import b,c,d
105 # from a import b,c,d
103 mod = _origimport(name, globals, locals)
106 mod = _origimport(name, globals, locals)
104 # recurse down the module chain
107 # recurse down the module chain
105 for comp in name.split('.')[1:]:
108 for comp in name.split('.')[1:]:
106 if not hasattr(mod, comp):
109 if not hasattr(mod, comp):
107 setattr(mod, comp, _demandmod(comp, mod.__dict__, mod.__dict__))
110 setattr(mod, comp, _demandmod(comp, mod.__dict__, mod.__dict__))
108 mod = getattr(mod, comp)
111 mod = getattr(mod, comp)
109 for x in fromlist:
112 for x in fromlist:
110 # set requested submodules for demand load
113 # set requested submodules for demand load
111 if not(hasattr(mod, x)):
114 if not(hasattr(mod, x)):
112 setattr(mod, x, _demandmod(x, mod.__dict__, locals))
115 setattr(mod, x, _demandmod(x, mod.__dict__, locals))
113 return mod
116 return mod
114
117
115 ignore = [
118 ignore = [
116 '_hashlib',
119 '_hashlib',
117 '_xmlplus',
120 '_xmlplus',
118 'fcntl',
121 'fcntl',
119 'win32com.gen_py',
122 'win32com.gen_py',
120 'pythoncom',
123 'pythoncom',
121 # imported by tarfile, not available under Windows
124 # imported by tarfile, not available under Windows
122 'pwd',
125 'pwd',
123 'grp',
126 'grp',
124 # imported by profile, itself imported by hotshot.stats,
127 # imported by profile, itself imported by hotshot.stats,
125 # not available under Windows
128 # not available under Windows
126 'resource',
129 'resource',
127 ]
130 ]
128
131
129 def enable():
132 def enable():
130 "enable global demand-loading of modules"
133 "enable global demand-loading of modules"
131 __builtin__.__import__ = _demandimport
134 __builtin__.__import__ = _demandimport
132
135
133 def disable():
136 def disable():
134 "disable global demand-loading of modules"
137 "disable global demand-loading of modules"
135 __builtin__.__import__ = _origimport
138 __builtin__.__import__ = _origimport
136
139
@@ -1,113 +1,113 b''
1 #! /usr/bin/env python
1 #! /usr/bin/env python
2
2
3 import sys
3 import sys
4 from _lsprof import Profiler, profiler_entry
4 from _lsprof import Profiler, profiler_entry
5
5
6 __all__ = ['profile', 'Stats']
6 __all__ = ['profile', 'Stats']
7
7
8 def profile(f, *args, **kwds):
8 def profile(f, *args, **kwds):
9 """XXX docstring"""
9 """XXX docstring"""
10 p = Profiler()
10 p = Profiler()
11 p.enable(subcalls=True, builtins=True)
11 p.enable(subcalls=True, builtins=True)
12 try:
12 try:
13 f(*args, **kwds)
13 f(*args, **kwds)
14 finally:
14 finally:
15 p.disable()
15 p.disable()
16 return Stats(p.getstats())
16 return Stats(p.getstats())
17
17
18
18
19 class Stats(object):
19 class Stats(object):
20 """XXX docstring"""
20 """XXX docstring"""
21
21
22 def __init__(self, data):
22 def __init__(self, data):
23 self.data = data
23 self.data = data
24
24
25 def sort(self, crit="inlinetime"):
25 def sort(self, crit="inlinetime"):
26 """XXX docstring"""
26 """XXX docstring"""
27 if crit not in profiler_entry.__dict__:
27 if crit not in profiler_entry.__dict__:
28 raise ValueError("Can't sort by %s" % crit)
28 raise ValueError("Can't sort by %s" % crit)
29 self.data.sort(key=lambda x: getattr(x, crit), reverse=True)
29 self.data.sort(key=lambda x: getattr(x, crit), reverse=True)
30 for e in self.data:
30 for e in self.data:
31 if e.calls:
31 if e.calls:
32 e.calls.sort(key=lambda x: getattr(x, crit), reverse=True)
32 e.calls.sort(key=lambda x: getattr(x, crit), reverse=True)
33
33
34 def pprint(self, top=None, file=None, limit=None, climit=None):
34 def pprint(self, top=None, file=None, limit=None, climit=None):
35 """XXX docstring"""
35 """XXX docstring"""
36 if file is None:
36 if file is None:
37 file = sys.stdout
37 file = sys.stdout
38 d = self.data
38 d = self.data
39 if top is not None:
39 if top is not None:
40 d = d[:top]
40 d = d[:top]
41 cols = "% 12s %12s %11.4f %11.4f %s\n"
41 cols = "% 12s %12s %11.4f %11.4f %s\n"
42 hcols = "% 12s %12s %12s %12s %s\n"
42 hcols = "% 12s %12s %12s %12s %s\n"
43 file.write(hcols % ("CallCount", "Recursive", "Total(ms)",
43 file.write(hcols % ("CallCount", "Recursive", "Total(ms)",
44 "Inline(ms)", "module:lineno(function)"))
44 "Inline(ms)", "module:lineno(function)"))
45 count = 0
45 count = 0
46 for e in d:
46 for e in d:
47 file.write(cols % (e.callcount, e.reccallcount, e.totaltime,
47 file.write(cols % (e.callcount, e.reccallcount, e.totaltime,
48 e.inlinetime, label(e.code)))
48 e.inlinetime, label(e.code)))
49 count += 1
49 count += 1
50 if limit is not None and count == limit:
50 if limit is not None and count == limit:
51 return
51 return
52 ccount = 0
52 ccount = 0
53 if e.calls:
53 if e.calls:
54 for se in e.calls:
54 for se in e.calls:
55 file.write(cols % ("+%s" % se.callcount, se.reccallcount,
55 file.write(cols % ("+%s" % se.callcount, se.reccallcount,
56 se.totaltime, se.inlinetime,
56 se.totaltime, se.inlinetime,
57 "+%s" % label(se.code)))
57 "+%s" % label(se.code)))
58 count += 1
58 count += 1
59 ccount += 1
59 ccount += 1
60 if limit is not None and count == limit:
60 if limit is not None and count == limit:
61 return
61 return
62 if climit is not None and ccount == climit:
62 if climit is not None and ccount == climit:
63 break
63 break
64
64
65 def freeze(self):
65 def freeze(self):
66 """Replace all references to code objects with string
66 """Replace all references to code objects with string
67 descriptions; this makes it possible to pickle the instance."""
67 descriptions; this makes it possible to pickle the instance."""
68
68
69 # this code is probably rather ickier than it needs to be!
69 # this code is probably rather ickier than it needs to be!
70 for i in range(len(self.data)):
70 for i in range(len(self.data)):
71 e = self.data[i]
71 e = self.data[i]
72 if not isinstance(e.code, str):
72 if not isinstance(e.code, str):
73 self.data[i] = type(e)((label(e.code),) + e[1:])
73 self.data[i] = type(e)((label(e.code),) + e[1:])
74 if e.calls:
74 if e.calls:
75 for j in range(len(e.calls)):
75 for j in range(len(e.calls)):
76 se = e.calls[j]
76 se = e.calls[j]
77 if not isinstance(se.code, str):
77 if not isinstance(se.code, str):
78 e.calls[j] = type(se)((label(se.code),) + se[1:])
78 e.calls[j] = type(se)((label(se.code),) + se[1:])
79
79
80 _fn2mod = {}
80 _fn2mod = {}
81
81
82 def label(code):
82 def label(code):
83 if isinstance(code, str):
83 if isinstance(code, str):
84 return code
84 return code
85 try:
85 try:
86 mname = _fn2mod[code.co_filename]
86 mname = _fn2mod[code.co_filename]
87 except KeyError:
87 except KeyError:
88 for k, v in sys.modules.iteritems():
88 for k, v in list(sys.modules.iteritems()):
89 if v is None:
89 if v is None:
90 continue
90 continue
91 if not hasattr(v, '__file__'):
91 if not hasattr(v, '__file__'):
92 continue
92 continue
93 if not isinstance(v.__file__, str):
93 if not isinstance(v.__file__, str):
94 continue
94 continue
95 if v.__file__.startswith(code.co_filename):
95 if v.__file__.startswith(code.co_filename):
96 mname = _fn2mod[code.co_filename] = k
96 mname = _fn2mod[code.co_filename] = k
97 break
97 break
98 else:
98 else:
99 mname = _fn2mod[code.co_filename] = '<%s>'%code.co_filename
99 mname = _fn2mod[code.co_filename] = '<%s>'%code.co_filename
100
100
101 return '%s:%d(%s)' % (mname, code.co_firstlineno, code.co_name)
101 return '%s:%d(%s)' % (mname, code.co_firstlineno, code.co_name)
102
102
103
103
104 if __name__ == '__main__':
104 if __name__ == '__main__':
105 import os
105 import os
106 sys.argv = sys.argv[1:]
106 sys.argv = sys.argv[1:]
107 if not sys.argv:
107 if not sys.argv:
108 print >> sys.stderr, "usage: lsprof.py <script> <arguments...>"
108 print >> sys.stderr, "usage: lsprof.py <script> <arguments...>"
109 sys.exit(2)
109 sys.exit(2)
110 sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])))
110 sys.path.insert(0, os.path.abspath(os.path.dirname(sys.argv[0])))
111 stats = profile(execfile, sys.argv[0], globals(), locals())
111 stats = profile(execfile, sys.argv[0], globals(), locals())
112 stats.sort()
112 stats.sort()
113 stats.pprint()
113 stats.pprint()
General Comments 0
You need to be logged in to leave comments. Login now