##// END OF EJS Templates
stringutil: teach pprint() to recognize generators...
Gregory Szorc -
r39332:ce145f8e default
parent child Browse files
Show More
@@ -1,588 +1,591 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import ast
12 import ast
13 import codecs
13 import codecs
14 import re as remod
14 import re as remod
15 import textwrap
15 import textwrap
16 import types
16
17
17 from ..i18n import _
18 from ..i18n import _
18 from ..thirdparty import attr
19 from ..thirdparty import attr
19
20
20 from .. import (
21 from .. import (
21 encoding,
22 encoding,
22 error,
23 error,
23 pycompat,
24 pycompat,
24 )
25 )
25
26
26 # regex special chars pulled from https://bugs.python.org/issue29995
27 # regex special chars pulled from https://bugs.python.org/issue29995
27 # which was part of Python 3.7.
28 # which was part of Python 3.7.
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30
31
31 def reescape(pat):
32 def reescape(pat):
32 """Drop-in replacement for re.escape."""
33 """Drop-in replacement for re.escape."""
33 # NOTE: it is intentional that this works on unicodes and not
34 # NOTE: it is intentional that this works on unicodes and not
34 # bytes, as it's only possible to do the escaping with
35 # bytes, as it's only possible to do the escaping with
35 # unicode.translate, not bytes.translate. Sigh.
36 # unicode.translate, not bytes.translate. Sigh.
36 wantuni = True
37 wantuni = True
37 if isinstance(pat, bytes):
38 if isinstance(pat, bytes):
38 wantuni = False
39 wantuni = False
39 pat = pat.decode('latin1')
40 pat = pat.decode('latin1')
40 pat = pat.translate(_regexescapemap)
41 pat = pat.translate(_regexescapemap)
41 if wantuni:
42 if wantuni:
42 return pat
43 return pat
43 return pat.encode('latin1')
44 return pat.encode('latin1')
44
45
45 def pprint(o, bprefix=False):
46 def pprint(o, bprefix=False):
46 """Pretty print an object."""
47 """Pretty print an object."""
47 if isinstance(o, bytes):
48 if isinstance(o, bytes):
48 if bprefix:
49 if bprefix:
49 return "b'%s'" % escapestr(o)
50 return "b'%s'" % escapestr(o)
50 return "'%s'" % escapestr(o)
51 return "'%s'" % escapestr(o)
51 elif isinstance(o, bytearray):
52 elif isinstance(o, bytearray):
52 # codecs.escape_encode() can't handle bytearray, so escapestr fails
53 # codecs.escape_encode() can't handle bytearray, so escapestr fails
53 # without coercion.
54 # without coercion.
54 return "bytearray['%s']" % escapestr(bytes(o))
55 return "bytearray['%s']" % escapestr(bytes(o))
55 elif isinstance(o, list):
56 elif isinstance(o, list):
56 return '[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
57 return '[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
57 elif isinstance(o, dict):
58 elif isinstance(o, dict):
58 return '{%s}' % (b', '.join(
59 return '{%s}' % (b', '.join(
59 '%s: %s' % (pprint(k, bprefix=bprefix),
60 '%s: %s' % (pprint(k, bprefix=bprefix),
60 pprint(v, bprefix=bprefix))
61 pprint(v, bprefix=bprefix))
61 for k, v in sorted(o.items())))
62 for k, v in sorted(o.items())))
62 elif isinstance(o, set):
63 elif isinstance(o, set):
63 return 'set([%s])' % (b', '.join(
64 return 'set([%s])' % (b', '.join(
64 pprint(k, bprefix=bprefix) for k in sorted(o)))
65 pprint(k, bprefix=bprefix) for k in sorted(o)))
65 elif isinstance(o, tuple):
66 elif isinstance(o, tuple):
66 return '(%s)' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
67 return '(%s)' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
68 elif isinstance(o, types.GeneratorType):
69 return 'gen[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
67 else:
70 else:
68 return pycompat.byterepr(o)
71 return pycompat.byterepr(o)
69
72
70 def prettyrepr(o):
73 def prettyrepr(o):
71 """Pretty print a representation of a possibly-nested object"""
74 """Pretty print a representation of a possibly-nested object"""
72 lines = []
75 lines = []
73 rs = pycompat.byterepr(o)
76 rs = pycompat.byterepr(o)
74 p0 = p1 = 0
77 p0 = p1 = 0
75 while p0 < len(rs):
78 while p0 < len(rs):
76 # '... field=<type ... field=<type ...'
79 # '... field=<type ... field=<type ...'
77 # ~~~~~~~~~~~~~~~~
80 # ~~~~~~~~~~~~~~~~
78 # p0 p1 q0 q1
81 # p0 p1 q0 q1
79 q0 = -1
82 q0 = -1
80 q1 = rs.find('<', p1 + 1)
83 q1 = rs.find('<', p1 + 1)
81 if q1 < 0:
84 if q1 < 0:
82 q1 = len(rs)
85 q1 = len(rs)
83 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
86 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
84 # backtrack for ' field=<'
87 # backtrack for ' field=<'
85 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
88 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
86 if q0 < 0:
89 if q0 < 0:
87 q0 = q1
90 q0 = q1
88 else:
91 else:
89 q0 += 1 # skip ' '
92 q0 += 1 # skip ' '
90 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
93 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
91 assert l >= 0
94 assert l >= 0
92 lines.append((l, rs[p0:q0].rstrip()))
95 lines.append((l, rs[p0:q0].rstrip()))
93 p0, p1 = q0, q1
96 p0, p1 = q0, q1
94 return '\n'.join(' ' * l + s for l, s in lines)
97 return '\n'.join(' ' * l + s for l, s in lines)
95
98
96 def buildrepr(r):
99 def buildrepr(r):
97 """Format an optional printable representation from unexpanded bits
100 """Format an optional printable representation from unexpanded bits
98
101
99 ======== =================================
102 ======== =================================
100 type(r) example
103 type(r) example
101 ======== =================================
104 ======== =================================
102 tuple ('<not %r>', other)
105 tuple ('<not %r>', other)
103 bytes '<branch closed>'
106 bytes '<branch closed>'
104 callable lambda: '<branch %r>' % sorted(b)
107 callable lambda: '<branch %r>' % sorted(b)
105 object other
108 object other
106 ======== =================================
109 ======== =================================
107 """
110 """
108 if r is None:
111 if r is None:
109 return ''
112 return ''
110 elif isinstance(r, tuple):
113 elif isinstance(r, tuple):
111 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
114 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
112 elif isinstance(r, bytes):
115 elif isinstance(r, bytes):
113 return r
116 return r
114 elif callable(r):
117 elif callable(r):
115 return r()
118 return r()
116 else:
119 else:
117 return pprint(r)
120 return pprint(r)
118
121
119 def binary(s):
122 def binary(s):
120 """return true if a string is binary data"""
123 """return true if a string is binary data"""
121 return bool(s and '\0' in s)
124 return bool(s and '\0' in s)
122
125
123 def stringmatcher(pattern, casesensitive=True):
126 def stringmatcher(pattern, casesensitive=True):
124 """
127 """
125 accepts a string, possibly starting with 're:' or 'literal:' prefix.
128 accepts a string, possibly starting with 're:' or 'literal:' prefix.
126 returns the matcher name, pattern, and matcher function.
129 returns the matcher name, pattern, and matcher function.
127 missing or unknown prefixes are treated as literal matches.
130 missing or unknown prefixes are treated as literal matches.
128
131
129 helper for tests:
132 helper for tests:
130 >>> def test(pattern, *tests):
133 >>> def test(pattern, *tests):
131 ... kind, pattern, matcher = stringmatcher(pattern)
134 ... kind, pattern, matcher = stringmatcher(pattern)
132 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
135 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
133 >>> def itest(pattern, *tests):
136 >>> def itest(pattern, *tests):
134 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
137 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
135 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
138 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
136
139
137 exact matching (no prefix):
140 exact matching (no prefix):
138 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
141 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
139 ('literal', 'abcdefg', [False, False, True])
142 ('literal', 'abcdefg', [False, False, True])
140
143
141 regex matching ('re:' prefix)
144 regex matching ('re:' prefix)
142 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
145 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
143 ('re', 'a.+b', [False, False, True])
146 ('re', 'a.+b', [False, False, True])
144
147
145 force exact matches ('literal:' prefix)
148 force exact matches ('literal:' prefix)
146 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
149 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
147 ('literal', 're:foobar', [False, True])
150 ('literal', 're:foobar', [False, True])
148
151
149 unknown prefixes are ignored and treated as literals
152 unknown prefixes are ignored and treated as literals
150 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
153 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
151 ('literal', 'foo:bar', [False, False, True])
154 ('literal', 'foo:bar', [False, False, True])
152
155
153 case insensitive regex matches
156 case insensitive regex matches
154 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
157 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
155 ('re', 'A.+b', [False, False, True])
158 ('re', 'A.+b', [False, False, True])
156
159
157 case insensitive literal matches
160 case insensitive literal matches
158 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
161 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
159 ('literal', 'ABCDEFG', [False, False, True])
162 ('literal', 'ABCDEFG', [False, False, True])
160 """
163 """
161 if pattern.startswith('re:'):
164 if pattern.startswith('re:'):
162 pattern = pattern[3:]
165 pattern = pattern[3:]
163 try:
166 try:
164 flags = 0
167 flags = 0
165 if not casesensitive:
168 if not casesensitive:
166 flags = remod.I
169 flags = remod.I
167 regex = remod.compile(pattern, flags)
170 regex = remod.compile(pattern, flags)
168 except remod.error as e:
171 except remod.error as e:
169 raise error.ParseError(_('invalid regular expression: %s')
172 raise error.ParseError(_('invalid regular expression: %s')
170 % e)
173 % e)
171 return 're', pattern, regex.search
174 return 're', pattern, regex.search
172 elif pattern.startswith('literal:'):
175 elif pattern.startswith('literal:'):
173 pattern = pattern[8:]
176 pattern = pattern[8:]
174
177
175 match = pattern.__eq__
178 match = pattern.__eq__
176
179
177 if not casesensitive:
180 if not casesensitive:
178 ipat = encoding.lower(pattern)
181 ipat = encoding.lower(pattern)
179 match = lambda s: ipat == encoding.lower(s)
182 match = lambda s: ipat == encoding.lower(s)
180 return 'literal', pattern, match
183 return 'literal', pattern, match
181
184
182 def shortuser(user):
185 def shortuser(user):
183 """Return a short representation of a user name or email address."""
186 """Return a short representation of a user name or email address."""
184 f = user.find('@')
187 f = user.find('@')
185 if f >= 0:
188 if f >= 0:
186 user = user[:f]
189 user = user[:f]
187 f = user.find('<')
190 f = user.find('<')
188 if f >= 0:
191 if f >= 0:
189 user = user[f + 1:]
192 user = user[f + 1:]
190 f = user.find(' ')
193 f = user.find(' ')
191 if f >= 0:
194 if f >= 0:
192 user = user[:f]
195 user = user[:f]
193 f = user.find('.')
196 f = user.find('.')
194 if f >= 0:
197 if f >= 0:
195 user = user[:f]
198 user = user[:f]
196 return user
199 return user
197
200
198 def emailuser(user):
201 def emailuser(user):
199 """Return the user portion of an email address."""
202 """Return the user portion of an email address."""
200 f = user.find('@')
203 f = user.find('@')
201 if f >= 0:
204 if f >= 0:
202 user = user[:f]
205 user = user[:f]
203 f = user.find('<')
206 f = user.find('<')
204 if f >= 0:
207 if f >= 0:
205 user = user[f + 1:]
208 user = user[f + 1:]
206 return user
209 return user
207
210
208 def email(author):
211 def email(author):
209 '''get email of author.'''
212 '''get email of author.'''
210 r = author.find('>')
213 r = author.find('>')
211 if r == -1:
214 if r == -1:
212 r = None
215 r = None
213 return author[author.find('<') + 1:r]
216 return author[author.find('<') + 1:r]
214
217
215 def person(author):
218 def person(author):
216 """Returns the name before an email address,
219 """Returns the name before an email address,
217 interpreting it as per RFC 5322
220 interpreting it as per RFC 5322
218
221
219 >>> person(b'foo@bar')
222 >>> person(b'foo@bar')
220 'foo'
223 'foo'
221 >>> person(b'Foo Bar <foo@bar>')
224 >>> person(b'Foo Bar <foo@bar>')
222 'Foo Bar'
225 'Foo Bar'
223 >>> person(b'"Foo Bar" <foo@bar>')
226 >>> person(b'"Foo Bar" <foo@bar>')
224 'Foo Bar'
227 'Foo Bar'
225 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
228 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
226 'Foo "buz" Bar'
229 'Foo "buz" Bar'
227 >>> # The following are invalid, but do exist in real-life
230 >>> # The following are invalid, but do exist in real-life
228 ...
231 ...
229 >>> person(b'Foo "buz" Bar <foo@bar>')
232 >>> person(b'Foo "buz" Bar <foo@bar>')
230 'Foo "buz" Bar'
233 'Foo "buz" Bar'
231 >>> person(b'"Foo Bar <foo@bar>')
234 >>> person(b'"Foo Bar <foo@bar>')
232 'Foo Bar'
235 'Foo Bar'
233 """
236 """
234 if '@' not in author:
237 if '@' not in author:
235 return author
238 return author
236 f = author.find('<')
239 f = author.find('<')
237 if f != -1:
240 if f != -1:
238 return author[:f].strip(' "').replace('\\"', '"')
241 return author[:f].strip(' "').replace('\\"', '"')
239 f = author.find('@')
242 f = author.find('@')
240 return author[:f].replace('.', ' ')
243 return author[:f].replace('.', ' ')
241
244
242 @attr.s(hash=True)
245 @attr.s(hash=True)
243 class mailmapping(object):
246 class mailmapping(object):
244 '''Represents a username/email key or value in
247 '''Represents a username/email key or value in
245 a mailmap file'''
248 a mailmap file'''
246 email = attr.ib()
249 email = attr.ib()
247 name = attr.ib(default=None)
250 name = attr.ib(default=None)
248
251
249 def _ismailmaplineinvalid(names, emails):
252 def _ismailmaplineinvalid(names, emails):
250 '''Returns True if the parsed names and emails
253 '''Returns True if the parsed names and emails
251 in a mailmap entry are invalid.
254 in a mailmap entry are invalid.
252
255
253 >>> # No names or emails fails
256 >>> # No names or emails fails
254 >>> names, emails = [], []
257 >>> names, emails = [], []
255 >>> _ismailmaplineinvalid(names, emails)
258 >>> _ismailmaplineinvalid(names, emails)
256 True
259 True
257 >>> # Only one email fails
260 >>> # Only one email fails
258 >>> emails = [b'email@email.com']
261 >>> emails = [b'email@email.com']
259 >>> _ismailmaplineinvalid(names, emails)
262 >>> _ismailmaplineinvalid(names, emails)
260 True
263 True
261 >>> # One email and one name passes
264 >>> # One email and one name passes
262 >>> names = [b'Test Name']
265 >>> names = [b'Test Name']
263 >>> _ismailmaplineinvalid(names, emails)
266 >>> _ismailmaplineinvalid(names, emails)
264 False
267 False
265 >>> # No names but two emails passes
268 >>> # No names but two emails passes
266 >>> names = []
269 >>> names = []
267 >>> emails = [b'proper@email.com', b'commit@email.com']
270 >>> emails = [b'proper@email.com', b'commit@email.com']
268 >>> _ismailmaplineinvalid(names, emails)
271 >>> _ismailmaplineinvalid(names, emails)
269 False
272 False
270 '''
273 '''
271 return not emails or not names and len(emails) < 2
274 return not emails or not names and len(emails) < 2
272
275
273 def parsemailmap(mailmapcontent):
276 def parsemailmap(mailmapcontent):
274 """Parses data in the .mailmap format
277 """Parses data in the .mailmap format
275
278
276 >>> mmdata = b"\\n".join([
279 >>> mmdata = b"\\n".join([
277 ... b'# Comment',
280 ... b'# Comment',
278 ... b'Name <commit1@email.xx>',
281 ... b'Name <commit1@email.xx>',
279 ... b'<name@email.xx> <commit2@email.xx>',
282 ... b'<name@email.xx> <commit2@email.xx>',
280 ... b'Name <proper@email.xx> <commit3@email.xx>',
283 ... b'Name <proper@email.xx> <commit3@email.xx>',
281 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
284 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
282 ... ])
285 ... ])
283 >>> mm = parsemailmap(mmdata)
286 >>> mm = parsemailmap(mmdata)
284 >>> for key in sorted(mm.keys()):
287 >>> for key in sorted(mm.keys()):
285 ... print(key)
288 ... print(key)
286 mailmapping(email='commit1@email.xx', name=None)
289 mailmapping(email='commit1@email.xx', name=None)
287 mailmapping(email='commit2@email.xx', name=None)
290 mailmapping(email='commit2@email.xx', name=None)
288 mailmapping(email='commit3@email.xx', name=None)
291 mailmapping(email='commit3@email.xx', name=None)
289 mailmapping(email='commit4@email.xx', name='Commit')
292 mailmapping(email='commit4@email.xx', name='Commit')
290 >>> for val in sorted(mm.values()):
293 >>> for val in sorted(mm.values()):
291 ... print(val)
294 ... print(val)
292 mailmapping(email='commit1@email.xx', name='Name')
295 mailmapping(email='commit1@email.xx', name='Name')
293 mailmapping(email='name@email.xx', name=None)
296 mailmapping(email='name@email.xx', name=None)
294 mailmapping(email='proper@email.xx', name='Name')
297 mailmapping(email='proper@email.xx', name='Name')
295 mailmapping(email='proper@email.xx', name='Name')
298 mailmapping(email='proper@email.xx', name='Name')
296 """
299 """
297 mailmap = {}
300 mailmap = {}
298
301
299 if mailmapcontent is None:
302 if mailmapcontent is None:
300 return mailmap
303 return mailmap
301
304
302 for line in mailmapcontent.splitlines():
305 for line in mailmapcontent.splitlines():
303
306
304 # Don't bother checking the line if it is a comment or
307 # Don't bother checking the line if it is a comment or
305 # is an improperly formed author field
308 # is an improperly formed author field
306 if line.lstrip().startswith('#'):
309 if line.lstrip().startswith('#'):
307 continue
310 continue
308
311
309 # names, emails hold the parsed emails and names for each line
312 # names, emails hold the parsed emails and names for each line
310 # name_builder holds the words in a persons name
313 # name_builder holds the words in a persons name
311 names, emails = [], []
314 names, emails = [], []
312 namebuilder = []
315 namebuilder = []
313
316
314 for element in line.split():
317 for element in line.split():
315 if element.startswith('#'):
318 if element.startswith('#'):
316 # If we reach a comment in the mailmap file, move on
319 # If we reach a comment in the mailmap file, move on
317 break
320 break
318
321
319 elif element.startswith('<') and element.endswith('>'):
322 elif element.startswith('<') and element.endswith('>'):
320 # We have found an email.
323 # We have found an email.
321 # Parse it, and finalize any names from earlier
324 # Parse it, and finalize any names from earlier
322 emails.append(element[1:-1]) # Slice off the "<>"
325 emails.append(element[1:-1]) # Slice off the "<>"
323
326
324 if namebuilder:
327 if namebuilder:
325 names.append(' '.join(namebuilder))
328 names.append(' '.join(namebuilder))
326 namebuilder = []
329 namebuilder = []
327
330
328 # Break if we have found a second email, any other
331 # Break if we have found a second email, any other
329 # data does not fit the spec for .mailmap
332 # data does not fit the spec for .mailmap
330 if len(emails) > 1:
333 if len(emails) > 1:
331 break
334 break
332
335
333 else:
336 else:
334 # We have found another word in the committers name
337 # We have found another word in the committers name
335 namebuilder.append(element)
338 namebuilder.append(element)
336
339
337 # Check to see if we have parsed the line into a valid form
340 # Check to see if we have parsed the line into a valid form
338 # We require at least one email, and either at least one
341 # We require at least one email, and either at least one
339 # name or a second email
342 # name or a second email
340 if _ismailmaplineinvalid(names, emails):
343 if _ismailmaplineinvalid(names, emails):
341 continue
344 continue
342
345
343 mailmapkey = mailmapping(
346 mailmapkey = mailmapping(
344 email=emails[-1],
347 email=emails[-1],
345 name=names[-1] if len(names) == 2 else None,
348 name=names[-1] if len(names) == 2 else None,
346 )
349 )
347
350
348 mailmap[mailmapkey] = mailmapping(
351 mailmap[mailmapkey] = mailmapping(
349 email=emails[0],
352 email=emails[0],
350 name=names[0] if names else None,
353 name=names[0] if names else None,
351 )
354 )
352
355
353 return mailmap
356 return mailmap
354
357
355 def mapname(mailmap, author):
358 def mapname(mailmap, author):
356 """Returns the author field according to the mailmap cache, or
359 """Returns the author field according to the mailmap cache, or
357 the original author field.
360 the original author field.
358
361
359 >>> mmdata = b"\\n".join([
362 >>> mmdata = b"\\n".join([
360 ... b'# Comment',
363 ... b'# Comment',
361 ... b'Name <commit1@email.xx>',
364 ... b'Name <commit1@email.xx>',
362 ... b'<name@email.xx> <commit2@email.xx>',
365 ... b'<name@email.xx> <commit2@email.xx>',
363 ... b'Name <proper@email.xx> <commit3@email.xx>',
366 ... b'Name <proper@email.xx> <commit3@email.xx>',
364 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
367 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
365 ... ])
368 ... ])
366 >>> m = parsemailmap(mmdata)
369 >>> m = parsemailmap(mmdata)
367 >>> mapname(m, b'Commit <commit1@email.xx>')
370 >>> mapname(m, b'Commit <commit1@email.xx>')
368 'Name <commit1@email.xx>'
371 'Name <commit1@email.xx>'
369 >>> mapname(m, b'Name <commit2@email.xx>')
372 >>> mapname(m, b'Name <commit2@email.xx>')
370 'Name <name@email.xx>'
373 'Name <name@email.xx>'
371 >>> mapname(m, b'Commit <commit3@email.xx>')
374 >>> mapname(m, b'Commit <commit3@email.xx>')
372 'Name <proper@email.xx>'
375 'Name <proper@email.xx>'
373 >>> mapname(m, b'Commit <commit4@email.xx>')
376 >>> mapname(m, b'Commit <commit4@email.xx>')
374 'Name <proper@email.xx>'
377 'Name <proper@email.xx>'
375 >>> mapname(m, b'Unknown Name <unknown@email.com>')
378 >>> mapname(m, b'Unknown Name <unknown@email.com>')
376 'Unknown Name <unknown@email.com>'
379 'Unknown Name <unknown@email.com>'
377 """
380 """
378 # If the author field coming in isn't in the correct format,
381 # If the author field coming in isn't in the correct format,
379 # or the mailmap is empty just return the original author field
382 # or the mailmap is empty just return the original author field
380 if not isauthorwellformed(author) or not mailmap:
383 if not isauthorwellformed(author) or not mailmap:
381 return author
384 return author
382
385
383 # Turn the user name into a mailmapping
386 # Turn the user name into a mailmapping
384 commit = mailmapping(name=person(author), email=email(author))
387 commit = mailmapping(name=person(author), email=email(author))
385
388
386 try:
389 try:
387 # Try and use both the commit email and name as the key
390 # Try and use both the commit email and name as the key
388 proper = mailmap[commit]
391 proper = mailmap[commit]
389
392
390 except KeyError:
393 except KeyError:
391 # If the lookup fails, use just the email as the key instead
394 # If the lookup fails, use just the email as the key instead
392 # We call this commit2 as not to erase original commit fields
395 # We call this commit2 as not to erase original commit fields
393 commit2 = mailmapping(email=commit.email)
396 commit2 = mailmapping(email=commit.email)
394 proper = mailmap.get(commit2, mailmapping(None, None))
397 proper = mailmap.get(commit2, mailmapping(None, None))
395
398
396 # Return the author field with proper values filled in
399 # Return the author field with proper values filled in
397 return '%s <%s>' % (
400 return '%s <%s>' % (
398 proper.name if proper.name else commit.name,
401 proper.name if proper.name else commit.name,
399 proper.email if proper.email else commit.email,
402 proper.email if proper.email else commit.email,
400 )
403 )
401
404
402 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
405 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
403
406
404 def isauthorwellformed(author):
407 def isauthorwellformed(author):
405 '''Return True if the author field is well formed
408 '''Return True if the author field is well formed
406 (ie "Contributor Name <contrib@email.dom>")
409 (ie "Contributor Name <contrib@email.dom>")
407
410
408 >>> isauthorwellformed(b'Good Author <good@author.com>')
411 >>> isauthorwellformed(b'Good Author <good@author.com>')
409 True
412 True
410 >>> isauthorwellformed(b'Author <good@author.com>')
413 >>> isauthorwellformed(b'Author <good@author.com>')
411 True
414 True
412 >>> isauthorwellformed(b'Bad Author')
415 >>> isauthorwellformed(b'Bad Author')
413 False
416 False
414 >>> isauthorwellformed(b'Bad Author <author@author.com')
417 >>> isauthorwellformed(b'Bad Author <author@author.com')
415 False
418 False
416 >>> isauthorwellformed(b'Bad Author author@author.com')
419 >>> isauthorwellformed(b'Bad Author author@author.com')
417 False
420 False
418 >>> isauthorwellformed(b'<author@author.com>')
421 >>> isauthorwellformed(b'<author@author.com>')
419 False
422 False
420 >>> isauthorwellformed(b'Bad Author <author>')
423 >>> isauthorwellformed(b'Bad Author <author>')
421 False
424 False
422 '''
425 '''
423 return _correctauthorformat.match(author) is not None
426 return _correctauthorformat.match(author) is not None
424
427
425 def ellipsis(text, maxlength=400):
428 def ellipsis(text, maxlength=400):
426 """Trim string to at most maxlength (default: 400) columns in display."""
429 """Trim string to at most maxlength (default: 400) columns in display."""
427 return encoding.trim(text, maxlength, ellipsis='...')
430 return encoding.trim(text, maxlength, ellipsis='...')
428
431
429 def escapestr(s):
432 def escapestr(s):
430 if isinstance(s, memoryview):
433 if isinstance(s, memoryview):
431 s = bytes(s)
434 s = bytes(s)
432 # call underlying function of s.encode('string_escape') directly for
435 # call underlying function of s.encode('string_escape') directly for
433 # Python 3 compatibility
436 # Python 3 compatibility
434 return codecs.escape_encode(s)[0]
437 return codecs.escape_encode(s)[0]
435
438
436 def unescapestr(s):
439 def unescapestr(s):
437 return codecs.escape_decode(s)[0]
440 return codecs.escape_decode(s)[0]
438
441
439 def forcebytestr(obj):
442 def forcebytestr(obj):
440 """Portably format an arbitrary object (e.g. exception) into a byte
443 """Portably format an arbitrary object (e.g. exception) into a byte
441 string."""
444 string."""
442 try:
445 try:
443 return pycompat.bytestr(obj)
446 return pycompat.bytestr(obj)
444 except UnicodeEncodeError:
447 except UnicodeEncodeError:
445 # non-ascii string, may be lossy
448 # non-ascii string, may be lossy
446 return pycompat.bytestr(encoding.strtolocal(str(obj)))
449 return pycompat.bytestr(encoding.strtolocal(str(obj)))
447
450
448 def uirepr(s):
451 def uirepr(s):
449 # Avoid double backslash in Windows path repr()
452 # Avoid double backslash in Windows path repr()
450 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
453 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
451
454
452 # delay import of textwrap
455 # delay import of textwrap
453 def _MBTextWrapper(**kwargs):
456 def _MBTextWrapper(**kwargs):
454 class tw(textwrap.TextWrapper):
457 class tw(textwrap.TextWrapper):
455 """
458 """
456 Extend TextWrapper for width-awareness.
459 Extend TextWrapper for width-awareness.
457
460
458 Neither number of 'bytes' in any encoding nor 'characters' is
461 Neither number of 'bytes' in any encoding nor 'characters' is
459 appropriate to calculate terminal columns for specified string.
462 appropriate to calculate terminal columns for specified string.
460
463
461 Original TextWrapper implementation uses built-in 'len()' directly,
464 Original TextWrapper implementation uses built-in 'len()' directly,
462 so overriding is needed to use width information of each characters.
465 so overriding is needed to use width information of each characters.
463
466
464 In addition, characters classified into 'ambiguous' width are
467 In addition, characters classified into 'ambiguous' width are
465 treated as wide in East Asian area, but as narrow in other.
468 treated as wide in East Asian area, but as narrow in other.
466
469
467 This requires use decision to determine width of such characters.
470 This requires use decision to determine width of such characters.
468 """
471 """
469 def _cutdown(self, ucstr, space_left):
472 def _cutdown(self, ucstr, space_left):
470 l = 0
473 l = 0
471 colwidth = encoding.ucolwidth
474 colwidth = encoding.ucolwidth
472 for i in pycompat.xrange(len(ucstr)):
475 for i in pycompat.xrange(len(ucstr)):
473 l += colwidth(ucstr[i])
476 l += colwidth(ucstr[i])
474 if space_left < l:
477 if space_left < l:
475 return (ucstr[:i], ucstr[i:])
478 return (ucstr[:i], ucstr[i:])
476 return ucstr, ''
479 return ucstr, ''
477
480
478 # overriding of base class
481 # overriding of base class
479 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
482 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
480 space_left = max(width - cur_len, 1)
483 space_left = max(width - cur_len, 1)
481
484
482 if self.break_long_words:
485 if self.break_long_words:
483 cut, res = self._cutdown(reversed_chunks[-1], space_left)
486 cut, res = self._cutdown(reversed_chunks[-1], space_left)
484 cur_line.append(cut)
487 cur_line.append(cut)
485 reversed_chunks[-1] = res
488 reversed_chunks[-1] = res
486 elif not cur_line:
489 elif not cur_line:
487 cur_line.append(reversed_chunks.pop())
490 cur_line.append(reversed_chunks.pop())
488
491
489 # this overriding code is imported from TextWrapper of Python 2.6
492 # this overriding code is imported from TextWrapper of Python 2.6
490 # to calculate columns of string by 'encoding.ucolwidth()'
493 # to calculate columns of string by 'encoding.ucolwidth()'
491 def _wrap_chunks(self, chunks):
494 def _wrap_chunks(self, chunks):
492 colwidth = encoding.ucolwidth
495 colwidth = encoding.ucolwidth
493
496
494 lines = []
497 lines = []
495 if self.width <= 0:
498 if self.width <= 0:
496 raise ValueError("invalid width %r (must be > 0)" % self.width)
499 raise ValueError("invalid width %r (must be > 0)" % self.width)
497
500
498 # Arrange in reverse order so items can be efficiently popped
501 # Arrange in reverse order so items can be efficiently popped
499 # from a stack of chucks.
502 # from a stack of chucks.
500 chunks.reverse()
503 chunks.reverse()
501
504
502 while chunks:
505 while chunks:
503
506
504 # Start the list of chunks that will make up the current line.
507 # Start the list of chunks that will make up the current line.
505 # cur_len is just the length of all the chunks in cur_line.
508 # cur_len is just the length of all the chunks in cur_line.
506 cur_line = []
509 cur_line = []
507 cur_len = 0
510 cur_len = 0
508
511
509 # Figure out which static string will prefix this line.
512 # Figure out which static string will prefix this line.
510 if lines:
513 if lines:
511 indent = self.subsequent_indent
514 indent = self.subsequent_indent
512 else:
515 else:
513 indent = self.initial_indent
516 indent = self.initial_indent
514
517
515 # Maximum width for this line.
518 # Maximum width for this line.
516 width = self.width - len(indent)
519 width = self.width - len(indent)
517
520
518 # First chunk on line is whitespace -- drop it, unless this
521 # First chunk on line is whitespace -- drop it, unless this
519 # is the very beginning of the text (i.e. no lines started yet).
522 # is the very beginning of the text (i.e. no lines started yet).
520 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
523 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
521 del chunks[-1]
524 del chunks[-1]
522
525
523 while chunks:
526 while chunks:
524 l = colwidth(chunks[-1])
527 l = colwidth(chunks[-1])
525
528
526 # Can at least squeeze this chunk onto the current line.
529 # Can at least squeeze this chunk onto the current line.
527 if cur_len + l <= width:
530 if cur_len + l <= width:
528 cur_line.append(chunks.pop())
531 cur_line.append(chunks.pop())
529 cur_len += l
532 cur_len += l
530
533
531 # Nope, this line is full.
534 # Nope, this line is full.
532 else:
535 else:
533 break
536 break
534
537
535 # The current line is full, and the next chunk is too big to
538 # The current line is full, and the next chunk is too big to
536 # fit on *any* line (not just this one).
539 # fit on *any* line (not just this one).
537 if chunks and colwidth(chunks[-1]) > width:
540 if chunks and colwidth(chunks[-1]) > width:
538 self._handle_long_word(chunks, cur_line, cur_len, width)
541 self._handle_long_word(chunks, cur_line, cur_len, width)
539
542
540 # If the last chunk on this line is all whitespace, drop it.
543 # If the last chunk on this line is all whitespace, drop it.
541 if (self.drop_whitespace and
544 if (self.drop_whitespace and
542 cur_line and cur_line[-1].strip() == r''):
545 cur_line and cur_line[-1].strip() == r''):
543 del cur_line[-1]
546 del cur_line[-1]
544
547
545 # Convert current line back to a string and store it in list
548 # Convert current line back to a string and store it in list
546 # of all lines (return value).
549 # of all lines (return value).
547 if cur_line:
550 if cur_line:
548 lines.append(indent + r''.join(cur_line))
551 lines.append(indent + r''.join(cur_line))
549
552
550 return lines
553 return lines
551
554
552 global _MBTextWrapper
555 global _MBTextWrapper
553 _MBTextWrapper = tw
556 _MBTextWrapper = tw
554 return tw(**kwargs)
557 return tw(**kwargs)
555
558
556 def wrap(line, width, initindent='', hangindent=''):
559 def wrap(line, width, initindent='', hangindent=''):
557 maxindent = max(len(hangindent), len(initindent))
560 maxindent = max(len(hangindent), len(initindent))
558 if width <= maxindent:
561 if width <= maxindent:
559 # adjust for weird terminal size
562 # adjust for weird terminal size
560 width = max(78, maxindent + 1)
563 width = max(78, maxindent + 1)
561 line = line.decode(pycompat.sysstr(encoding.encoding),
564 line = line.decode(pycompat.sysstr(encoding.encoding),
562 pycompat.sysstr(encoding.encodingmode))
565 pycompat.sysstr(encoding.encodingmode))
563 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
566 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
564 pycompat.sysstr(encoding.encodingmode))
567 pycompat.sysstr(encoding.encodingmode))
565 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
568 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
566 pycompat.sysstr(encoding.encodingmode))
569 pycompat.sysstr(encoding.encodingmode))
567 wrapper = _MBTextWrapper(width=width,
570 wrapper = _MBTextWrapper(width=width,
568 initial_indent=initindent,
571 initial_indent=initindent,
569 subsequent_indent=hangindent)
572 subsequent_indent=hangindent)
570 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
573 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
571
574
572 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
575 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
573 '0': False, 'no': False, 'false': False, 'off': False,
576 '0': False, 'no': False, 'false': False, 'off': False,
574 'never': False}
577 'never': False}
575
578
576 def parsebool(s):
579 def parsebool(s):
577 """Parse s into a boolean.
580 """Parse s into a boolean.
578
581
579 If s is not a valid boolean, returns None.
582 If s is not a valid boolean, returns None.
580 """
583 """
581 return _booleans.get(s.lower(), None)
584 return _booleans.get(s.lower(), None)
582
585
583 def evalpythonliteral(s):
586 def evalpythonliteral(s):
584 """Evaluate a string containing a Python literal expression"""
587 """Evaluate a string containing a Python literal expression"""
585 # We could backport our tokenizer hack to rewrite '' to u'' if we want
588 # We could backport our tokenizer hack to rewrite '' to u'' if we want
586 if pycompat.ispy3:
589 if pycompat.ispy3:
587 return ast.literal_eval(s.decode('latin1'))
590 return ast.literal_eval(s.decode('latin1'))
588 return ast.literal_eval(s)
591 return ast.literal_eval(s)
General Comments 0
You need to be logged in to leave comments. Login now