##// END OF EJS Templates
stringutil: remove Python 2 support code...
Gregory Szorc -
r49766:46b3ecfb default
parent child Browse files
Show More
@@ -1,969 +1,967 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 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@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
10
11 import ast
11 import ast
12 import codecs
12 import codecs
13 import re as remod
13 import re as remod
14 import textwrap
14 import textwrap
15 import types
15 import types
16
16
17 from ..i18n import _
17 from ..i18n import _
18 from ..thirdparty import attr
18 from ..thirdparty import attr
19
19
20 from .. import (
20 from .. import (
21 encoding,
21 encoding,
22 error,
22 error,
23 pycompat,
23 pycompat,
24 )
24 )
25
25
26 # regex special chars pulled from https://bugs.python.org/issue29995
26 # regex special chars pulled from https://bugs.python.org/issue29995
27 # which was part of Python 3.7.
27 # which was part of Python 3.7.
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
30 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
31
31
32
32
33 def reescape(pat):
33 def reescape(pat):
34 """Drop-in replacement for re.escape."""
34 """Drop-in replacement for re.escape."""
35 # NOTE: it is intentional that this works on unicodes and not
35 # NOTE: it is intentional that this works on unicodes and not
36 # bytes, as it's only possible to do the escaping with
36 # bytes, as it's only possible to do the escaping with
37 # unicode.translate, not bytes.translate. Sigh.
37 # unicode.translate, not bytes.translate. Sigh.
38 wantuni = True
38 wantuni = True
39 if isinstance(pat, bytes):
39 if isinstance(pat, bytes):
40 wantuni = False
40 wantuni = False
41 pat = pat.decode('latin1')
41 pat = pat.decode('latin1')
42 pat = pat.translate(_regexescapemap)
42 pat = pat.translate(_regexescapemap)
43 if wantuni:
43 if wantuni:
44 return pat
44 return pat
45 return pat.encode('latin1')
45 return pat.encode('latin1')
46
46
47
47
48 def pprint(o, bprefix=False, indent=0, level=0):
48 def pprint(o, bprefix=False, indent=0, level=0):
49 """Pretty print an object."""
49 """Pretty print an object."""
50 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
50 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
51
51
52
52
53 def pprintgen(o, bprefix=False, indent=0, level=0):
53 def pprintgen(o, bprefix=False, indent=0, level=0):
54 """Pretty print an object to a generator of atoms.
54 """Pretty print an object to a generator of atoms.
55
55
56 ``bprefix`` is a flag influencing whether bytestrings are preferred with
56 ``bprefix`` is a flag influencing whether bytestrings are preferred with
57 a ``b''`` prefix.
57 a ``b''`` prefix.
58
58
59 ``indent`` controls whether collections and nested data structures
59 ``indent`` controls whether collections and nested data structures
60 span multiple lines via the indentation amount in spaces. By default,
60 span multiple lines via the indentation amount in spaces. By default,
61 no newlines are emitted.
61 no newlines are emitted.
62
62
63 ``level`` specifies the initial indent level. Used if ``indent > 0``.
63 ``level`` specifies the initial indent level. Used if ``indent > 0``.
64 """
64 """
65
65
66 if isinstance(o, bytes):
66 if isinstance(o, bytes):
67 if bprefix:
67 if bprefix:
68 yield b"b'%s'" % escapestr(o)
68 yield b"b'%s'" % escapestr(o)
69 else:
69 else:
70 yield b"'%s'" % escapestr(o)
70 yield b"'%s'" % escapestr(o)
71 elif isinstance(o, bytearray):
71 elif isinstance(o, bytearray):
72 # codecs.escape_encode() can't handle bytearray, so escapestr fails
72 # codecs.escape_encode() can't handle bytearray, so escapestr fails
73 # without coercion.
73 # without coercion.
74 yield b"bytearray['%s']" % escapestr(bytes(o))
74 yield b"bytearray['%s']" % escapestr(bytes(o))
75 elif isinstance(o, list):
75 elif isinstance(o, list):
76 if not o:
76 if not o:
77 yield b'[]'
77 yield b'[]'
78 return
78 return
79
79
80 yield b'['
80 yield b'['
81
81
82 if indent:
82 if indent:
83 level += 1
83 level += 1
84 yield b'\n'
84 yield b'\n'
85 yield b' ' * (level * indent)
85 yield b' ' * (level * indent)
86
86
87 for i, a in enumerate(o):
87 for i, a in enumerate(o):
88 for chunk in pprintgen(
88 for chunk in pprintgen(
89 a, bprefix=bprefix, indent=indent, level=level
89 a, bprefix=bprefix, indent=indent, level=level
90 ):
90 ):
91 yield chunk
91 yield chunk
92
92
93 if i + 1 < len(o):
93 if i + 1 < len(o):
94 if indent:
94 if indent:
95 yield b',\n'
95 yield b',\n'
96 yield b' ' * (level * indent)
96 yield b' ' * (level * indent)
97 else:
97 else:
98 yield b', '
98 yield b', '
99
99
100 if indent:
100 if indent:
101 level -= 1
101 level -= 1
102 yield b'\n'
102 yield b'\n'
103 yield b' ' * (level * indent)
103 yield b' ' * (level * indent)
104
104
105 yield b']'
105 yield b']'
106 elif isinstance(o, dict):
106 elif isinstance(o, dict):
107 if not o:
107 if not o:
108 yield b'{}'
108 yield b'{}'
109 return
109 return
110
110
111 yield b'{'
111 yield b'{'
112
112
113 if indent:
113 if indent:
114 level += 1
114 level += 1
115 yield b'\n'
115 yield b'\n'
116 yield b' ' * (level * indent)
116 yield b' ' * (level * indent)
117
117
118 for i, (k, v) in enumerate(sorted(o.items())):
118 for i, (k, v) in enumerate(sorted(o.items())):
119 for chunk in pprintgen(
119 for chunk in pprintgen(
120 k, bprefix=bprefix, indent=indent, level=level
120 k, bprefix=bprefix, indent=indent, level=level
121 ):
121 ):
122 yield chunk
122 yield chunk
123
123
124 yield b': '
124 yield b': '
125
125
126 for chunk in pprintgen(
126 for chunk in pprintgen(
127 v, bprefix=bprefix, indent=indent, level=level
127 v, bprefix=bprefix, indent=indent, level=level
128 ):
128 ):
129 yield chunk
129 yield chunk
130
130
131 if i + 1 < len(o):
131 if i + 1 < len(o):
132 if indent:
132 if indent:
133 yield b',\n'
133 yield b',\n'
134 yield b' ' * (level * indent)
134 yield b' ' * (level * indent)
135 else:
135 else:
136 yield b', '
136 yield b', '
137
137
138 if indent:
138 if indent:
139 level -= 1
139 level -= 1
140 yield b'\n'
140 yield b'\n'
141 yield b' ' * (level * indent)
141 yield b' ' * (level * indent)
142
142
143 yield b'}'
143 yield b'}'
144 elif isinstance(o, set):
144 elif isinstance(o, set):
145 if not o:
145 if not o:
146 yield b'set([])'
146 yield b'set([])'
147 return
147 return
148
148
149 yield b'set(['
149 yield b'set(['
150
150
151 if indent:
151 if indent:
152 level += 1
152 level += 1
153 yield b'\n'
153 yield b'\n'
154 yield b' ' * (level * indent)
154 yield b' ' * (level * indent)
155
155
156 for i, k in enumerate(sorted(o)):
156 for i, k in enumerate(sorted(o)):
157 for chunk in pprintgen(
157 for chunk in pprintgen(
158 k, bprefix=bprefix, indent=indent, level=level
158 k, bprefix=bprefix, indent=indent, level=level
159 ):
159 ):
160 yield chunk
160 yield chunk
161
161
162 if i + 1 < len(o):
162 if i + 1 < len(o):
163 if indent:
163 if indent:
164 yield b',\n'
164 yield b',\n'
165 yield b' ' * (level * indent)
165 yield b' ' * (level * indent)
166 else:
166 else:
167 yield b', '
167 yield b', '
168
168
169 if indent:
169 if indent:
170 level -= 1
170 level -= 1
171 yield b'\n'
171 yield b'\n'
172 yield b' ' * (level * indent)
172 yield b' ' * (level * indent)
173
173
174 yield b'])'
174 yield b'])'
175 elif isinstance(o, tuple):
175 elif isinstance(o, tuple):
176 if not o:
176 if not o:
177 yield b'()'
177 yield b'()'
178 return
178 return
179
179
180 yield b'('
180 yield b'('
181
181
182 if indent:
182 if indent:
183 level += 1
183 level += 1
184 yield b'\n'
184 yield b'\n'
185 yield b' ' * (level * indent)
185 yield b' ' * (level * indent)
186
186
187 for i, a in enumerate(o):
187 for i, a in enumerate(o):
188 for chunk in pprintgen(
188 for chunk in pprintgen(
189 a, bprefix=bprefix, indent=indent, level=level
189 a, bprefix=bprefix, indent=indent, level=level
190 ):
190 ):
191 yield chunk
191 yield chunk
192
192
193 if i + 1 < len(o):
193 if i + 1 < len(o):
194 if indent:
194 if indent:
195 yield b',\n'
195 yield b',\n'
196 yield b' ' * (level * indent)
196 yield b' ' * (level * indent)
197 else:
197 else:
198 yield b', '
198 yield b', '
199
199
200 if indent:
200 if indent:
201 level -= 1
201 level -= 1
202 yield b'\n'
202 yield b'\n'
203 yield b' ' * (level * indent)
203 yield b' ' * (level * indent)
204
204
205 yield b')'
205 yield b')'
206 elif isinstance(o, types.GeneratorType):
206 elif isinstance(o, types.GeneratorType):
207 # Special case of empty generator.
207 # Special case of empty generator.
208 try:
208 try:
209 nextitem = next(o)
209 nextitem = next(o)
210 except StopIteration:
210 except StopIteration:
211 yield b'gen[]'
211 yield b'gen[]'
212 return
212 return
213
213
214 yield b'gen['
214 yield b'gen['
215
215
216 if indent:
216 if indent:
217 level += 1
217 level += 1
218 yield b'\n'
218 yield b'\n'
219 yield b' ' * (level * indent)
219 yield b' ' * (level * indent)
220
220
221 last = False
221 last = False
222
222
223 while not last:
223 while not last:
224 current = nextitem
224 current = nextitem
225
225
226 try:
226 try:
227 nextitem = next(o)
227 nextitem = next(o)
228 except StopIteration:
228 except StopIteration:
229 last = True
229 last = True
230
230
231 for chunk in pprintgen(
231 for chunk in pprintgen(
232 current, bprefix=bprefix, indent=indent, level=level
232 current, bprefix=bprefix, indent=indent, level=level
233 ):
233 ):
234 yield chunk
234 yield chunk
235
235
236 if not last:
236 if not last:
237 if indent:
237 if indent:
238 yield b',\n'
238 yield b',\n'
239 yield b' ' * (level * indent)
239 yield b' ' * (level * indent)
240 else:
240 else:
241 yield b', '
241 yield b', '
242
242
243 if indent:
243 if indent:
244 level -= 1
244 level -= 1
245 yield b'\n'
245 yield b'\n'
246 yield b' ' * (level * indent)
246 yield b' ' * (level * indent)
247
247
248 yield b']'
248 yield b']'
249 else:
249 else:
250 yield pycompat.byterepr(o)
250 yield pycompat.byterepr(o)
251
251
252
252
253 def prettyrepr(o):
253 def prettyrepr(o):
254 """Pretty print a representation of a possibly-nested object"""
254 """Pretty print a representation of a possibly-nested object"""
255 lines = []
255 lines = []
256 rs = pycompat.byterepr(o)
256 rs = pycompat.byterepr(o)
257 p0 = p1 = 0
257 p0 = p1 = 0
258 while p0 < len(rs):
258 while p0 < len(rs):
259 # '... field=<type ... field=<type ...'
259 # '... field=<type ... field=<type ...'
260 # ~~~~~~~~~~~~~~~~
260 # ~~~~~~~~~~~~~~~~
261 # p0 p1 q0 q1
261 # p0 p1 q0 q1
262 q0 = -1
262 q0 = -1
263 q1 = rs.find(b'<', p1 + 1)
263 q1 = rs.find(b'<', p1 + 1)
264 if q1 < 0:
264 if q1 < 0:
265 q1 = len(rs)
265 q1 = len(rs)
266 # pytype: disable=wrong-arg-count
266 # pytype: disable=wrong-arg-count
267 # TODO: figure out why pytype doesn't recognize the optional start
267 # TODO: figure out why pytype doesn't recognize the optional start
268 # arg
268 # arg
269 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
269 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
270 # pytype: enable=wrong-arg-count
270 # pytype: enable=wrong-arg-count
271 # backtrack for ' field=<'
271 # backtrack for ' field=<'
272 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
272 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
273 if q0 < 0:
273 if q0 < 0:
274 q0 = q1
274 q0 = q1
275 else:
275 else:
276 q0 += 1 # skip ' '
276 q0 += 1 # skip ' '
277 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
277 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
278 assert l >= 0
278 assert l >= 0
279 lines.append((l, rs[p0:q0].rstrip()))
279 lines.append((l, rs[p0:q0].rstrip()))
280 p0, p1 = q0, q1
280 p0, p1 = q0, q1
281 return b'\n'.join(b' ' * l + s for l, s in lines)
281 return b'\n'.join(b' ' * l + s for l, s in lines)
282
282
283
283
284 def buildrepr(r):
284 def buildrepr(r):
285 """Format an optional printable representation from unexpanded bits
285 """Format an optional printable representation from unexpanded bits
286
286
287 ======== =================================
287 ======== =================================
288 type(r) example
288 type(r) example
289 ======== =================================
289 ======== =================================
290 tuple ('<not %r>', other)
290 tuple ('<not %r>', other)
291 bytes '<branch closed>'
291 bytes '<branch closed>'
292 callable lambda: '<branch %r>' % sorted(b)
292 callable lambda: '<branch %r>' % sorted(b)
293 object other
293 object other
294 ======== =================================
294 ======== =================================
295 """
295 """
296 if r is None:
296 if r is None:
297 return b''
297 return b''
298 elif isinstance(r, tuple):
298 elif isinstance(r, tuple):
299 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
299 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
300 elif isinstance(r, bytes):
300 elif isinstance(r, bytes):
301 return r
301 return r
302 elif callable(r):
302 elif callable(r):
303 return r()
303 return r()
304 else:
304 else:
305 return pprint(r)
305 return pprint(r)
306
306
307
307
308 def binary(s):
308 def binary(s):
309 """return true if a string is binary data"""
309 """return true if a string is binary data"""
310 return bool(s and b'\0' in s)
310 return bool(s and b'\0' in s)
311
311
312
312
313 def _splitpattern(pattern):
313 def _splitpattern(pattern):
314 if pattern.startswith(b're:'):
314 if pattern.startswith(b're:'):
315 return b're', pattern[3:]
315 return b're', pattern[3:]
316 elif pattern.startswith(b'literal:'):
316 elif pattern.startswith(b'literal:'):
317 return b'literal', pattern[8:]
317 return b'literal', pattern[8:]
318 return b'literal', pattern
318 return b'literal', pattern
319
319
320
320
321 def stringmatcher(pattern, casesensitive=True):
321 def stringmatcher(pattern, casesensitive=True):
322 """
322 """
323 accepts a string, possibly starting with 're:' or 'literal:' prefix.
323 accepts a string, possibly starting with 're:' or 'literal:' prefix.
324 returns the matcher name, pattern, and matcher function.
324 returns the matcher name, pattern, and matcher function.
325 missing or unknown prefixes are treated as literal matches.
325 missing or unknown prefixes are treated as literal matches.
326
326
327 helper for tests:
327 helper for tests:
328 >>> def test(pattern, *tests):
328 >>> def test(pattern, *tests):
329 ... kind, pattern, matcher = stringmatcher(pattern)
329 ... kind, pattern, matcher = stringmatcher(pattern)
330 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
330 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
331 >>> def itest(pattern, *tests):
331 >>> def itest(pattern, *tests):
332 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
332 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
333 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
333 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
334
334
335 exact matching (no prefix):
335 exact matching (no prefix):
336 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
336 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
337 ('literal', 'abcdefg', [False, False, True])
337 ('literal', 'abcdefg', [False, False, True])
338
338
339 regex matching ('re:' prefix)
339 regex matching ('re:' prefix)
340 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
340 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
341 ('re', 'a.+b', [False, False, True])
341 ('re', 'a.+b', [False, False, True])
342
342
343 force exact matches ('literal:' prefix)
343 force exact matches ('literal:' prefix)
344 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
344 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
345 ('literal', 're:foobar', [False, True])
345 ('literal', 're:foobar', [False, True])
346
346
347 unknown prefixes are ignored and treated as literals
347 unknown prefixes are ignored and treated as literals
348 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
348 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
349 ('literal', 'foo:bar', [False, False, True])
349 ('literal', 'foo:bar', [False, False, True])
350
350
351 case insensitive regex matches
351 case insensitive regex matches
352 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
352 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
353 ('re', 'A.+b', [False, False, True])
353 ('re', 'A.+b', [False, False, True])
354
354
355 case insensitive literal matches
355 case insensitive literal matches
356 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
356 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
357 ('literal', 'ABCDEFG', [False, False, True])
357 ('literal', 'ABCDEFG', [False, False, True])
358 """
358 """
359 kind, pattern = _splitpattern(pattern)
359 kind, pattern = _splitpattern(pattern)
360 if kind == b're':
360 if kind == b're':
361 try:
361 try:
362 flags = 0
362 flags = 0
363 if not casesensitive:
363 if not casesensitive:
364 flags = remod.I
364 flags = remod.I
365 regex = remod.compile(pattern, flags)
365 regex = remod.compile(pattern, flags)
366 except remod.error as e:
366 except remod.error as e:
367 raise error.ParseError(
367 raise error.ParseError(
368 _(b'invalid regular expression: %s') % forcebytestr(e)
368 _(b'invalid regular expression: %s') % forcebytestr(e)
369 )
369 )
370 return kind, pattern, regex.search
370 return kind, pattern, regex.search
371 elif kind == b'literal':
371 elif kind == b'literal':
372 if casesensitive:
372 if casesensitive:
373 match = pattern.__eq__
373 match = pattern.__eq__
374 else:
374 else:
375 ipat = encoding.lower(pattern)
375 ipat = encoding.lower(pattern)
376 match = lambda s: ipat == encoding.lower(s)
376 match = lambda s: ipat == encoding.lower(s)
377 return kind, pattern, match
377 return kind, pattern, match
378
378
379 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
379 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
380
380
381
381
382 def substringregexp(pattern, flags=0):
382 def substringregexp(pattern, flags=0):
383 """Build a regexp object from a string pattern possibly starting with
383 """Build a regexp object from a string pattern possibly starting with
384 're:' or 'literal:' prefix.
384 're:' or 'literal:' prefix.
385
385
386 helper for tests:
386 helper for tests:
387 >>> def test(pattern, *tests):
387 >>> def test(pattern, *tests):
388 ... regexp = substringregexp(pattern)
388 ... regexp = substringregexp(pattern)
389 ... return [bool(regexp.search(t)) for t in tests]
389 ... return [bool(regexp.search(t)) for t in tests]
390 >>> def itest(pattern, *tests):
390 >>> def itest(pattern, *tests):
391 ... regexp = substringregexp(pattern, remod.I)
391 ... regexp = substringregexp(pattern, remod.I)
392 ... return [bool(regexp.search(t)) for t in tests]
392 ... return [bool(regexp.search(t)) for t in tests]
393
393
394 substring matching (no prefix):
394 substring matching (no prefix):
395 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
395 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
396 [False, False, True]
396 [False, False, True]
397
397
398 substring pattern should be escaped:
398 substring pattern should be escaped:
399 >>> substringregexp(b'.bc').pattern
399 >>> substringregexp(b'.bc').pattern
400 '\\\\.bc'
400 '\\\\.bc'
401 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
401 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
402 [False, False, False]
402 [False, False, False]
403
403
404 regex matching ('re:' prefix)
404 regex matching ('re:' prefix)
405 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
405 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
406 [False, False, True]
406 [False, False, True]
407
407
408 force substring matches ('literal:' prefix)
408 force substring matches ('literal:' prefix)
409 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
409 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
410 [False, True]
410 [False, True]
411
411
412 case insensitive literal matches
412 case insensitive literal matches
413 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
413 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
414 [False, False, True]
414 [False, False, True]
415
415
416 case insensitive regex matches
416 case insensitive regex matches
417 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
417 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
418 [False, False, True]
418 [False, False, True]
419 """
419 """
420 kind, pattern = _splitpattern(pattern)
420 kind, pattern = _splitpattern(pattern)
421 if kind == b're':
421 if kind == b're':
422 try:
422 try:
423 return remod.compile(pattern, flags)
423 return remod.compile(pattern, flags)
424 except remod.error as e:
424 except remod.error as e:
425 raise error.ParseError(
425 raise error.ParseError(
426 _(b'invalid regular expression: %s') % forcebytestr(e)
426 _(b'invalid regular expression: %s') % forcebytestr(e)
427 )
427 )
428 elif kind == b'literal':
428 elif kind == b'literal':
429 return remod.compile(remod.escape(pattern), flags)
429 return remod.compile(remod.escape(pattern), flags)
430
430
431 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
431 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
432
432
433
433
434 def shortuser(user):
434 def shortuser(user):
435 """Return a short representation of a user name or email address."""
435 """Return a short representation of a user name or email address."""
436 f = user.find(b'@')
436 f = user.find(b'@')
437 if f >= 0:
437 if f >= 0:
438 user = user[:f]
438 user = user[:f]
439 f = user.find(b'<')
439 f = user.find(b'<')
440 if f >= 0:
440 if f >= 0:
441 user = user[f + 1 :]
441 user = user[f + 1 :]
442 f = user.find(b' ')
442 f = user.find(b' ')
443 if f >= 0:
443 if f >= 0:
444 user = user[:f]
444 user = user[:f]
445 f = user.find(b'.')
445 f = user.find(b'.')
446 if f >= 0:
446 if f >= 0:
447 user = user[:f]
447 user = user[:f]
448 return user
448 return user
449
449
450
450
451 def emailuser(user):
451 def emailuser(user):
452 """Return the user portion of an email address."""
452 """Return the user portion of an email address."""
453 f = user.find(b'@')
453 f = user.find(b'@')
454 if f >= 0:
454 if f >= 0:
455 user = user[:f]
455 user = user[:f]
456 f = user.find(b'<')
456 f = user.find(b'<')
457 if f >= 0:
457 if f >= 0:
458 user = user[f + 1 :]
458 user = user[f + 1 :]
459 return user
459 return user
460
460
461
461
462 def email(author):
462 def email(author):
463 '''get email of author.'''
463 '''get email of author.'''
464 r = author.find(b'>')
464 r = author.find(b'>')
465 if r == -1:
465 if r == -1:
466 r = None
466 r = None
467 return author[author.find(b'<') + 1 : r]
467 return author[author.find(b'<') + 1 : r]
468
468
469
469
470 def person(author):
470 def person(author):
471 """Returns the name before an email address,
471 """Returns the name before an email address,
472 interpreting it as per RFC 5322
472 interpreting it as per RFC 5322
473
473
474 >>> person(b'foo@bar')
474 >>> person(b'foo@bar')
475 'foo'
475 'foo'
476 >>> person(b'Foo Bar <foo@bar>')
476 >>> person(b'Foo Bar <foo@bar>')
477 'Foo Bar'
477 'Foo Bar'
478 >>> person(b'"Foo Bar" <foo@bar>')
478 >>> person(b'"Foo Bar" <foo@bar>')
479 'Foo Bar'
479 'Foo Bar'
480 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
480 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
481 'Foo "buz" Bar'
481 'Foo "buz" Bar'
482 >>> # The following are invalid, but do exist in real-life
482 >>> # The following are invalid, but do exist in real-life
483 ...
483 ...
484 >>> person(b'Foo "buz" Bar <foo@bar>')
484 >>> person(b'Foo "buz" Bar <foo@bar>')
485 'Foo "buz" Bar'
485 'Foo "buz" Bar'
486 >>> person(b'"Foo Bar <foo@bar>')
486 >>> person(b'"Foo Bar <foo@bar>')
487 'Foo Bar'
487 'Foo Bar'
488 """
488 """
489 if b'@' not in author:
489 if b'@' not in author:
490 return author
490 return author
491 f = author.find(b'<')
491 f = author.find(b'<')
492 if f != -1:
492 if f != -1:
493 return author[:f].strip(b' "').replace(b'\\"', b'"')
493 return author[:f].strip(b' "').replace(b'\\"', b'"')
494 f = author.find(b'@')
494 f = author.find(b'@')
495 return author[:f].replace(b'.', b' ')
495 return author[:f].replace(b'.', b' ')
496
496
497
497
498 @attr.s(hash=True)
498 @attr.s(hash=True)
499 class mailmapping(object):
499 class mailmapping(object):
500 """Represents a username/email key or value in
500 """Represents a username/email key or value in
501 a mailmap file"""
501 a mailmap file"""
502
502
503 email = attr.ib()
503 email = attr.ib()
504 name = attr.ib(default=None)
504 name = attr.ib(default=None)
505
505
506
506
507 def _ismailmaplineinvalid(names, emails):
507 def _ismailmaplineinvalid(names, emails):
508 """Returns True if the parsed names and emails
508 """Returns True if the parsed names and emails
509 in a mailmap entry are invalid.
509 in a mailmap entry are invalid.
510
510
511 >>> # No names or emails fails
511 >>> # No names or emails fails
512 >>> names, emails = [], []
512 >>> names, emails = [], []
513 >>> _ismailmaplineinvalid(names, emails)
513 >>> _ismailmaplineinvalid(names, emails)
514 True
514 True
515 >>> # Only one email fails
515 >>> # Only one email fails
516 >>> emails = [b'email@email.com']
516 >>> emails = [b'email@email.com']
517 >>> _ismailmaplineinvalid(names, emails)
517 >>> _ismailmaplineinvalid(names, emails)
518 True
518 True
519 >>> # One email and one name passes
519 >>> # One email and one name passes
520 >>> names = [b'Test Name']
520 >>> names = [b'Test Name']
521 >>> _ismailmaplineinvalid(names, emails)
521 >>> _ismailmaplineinvalid(names, emails)
522 False
522 False
523 >>> # No names but two emails passes
523 >>> # No names but two emails passes
524 >>> names = []
524 >>> names = []
525 >>> emails = [b'proper@email.com', b'commit@email.com']
525 >>> emails = [b'proper@email.com', b'commit@email.com']
526 >>> _ismailmaplineinvalid(names, emails)
526 >>> _ismailmaplineinvalid(names, emails)
527 False
527 False
528 """
528 """
529 return not emails or not names and len(emails) < 2
529 return not emails or not names and len(emails) < 2
530
530
531
531
532 def parsemailmap(mailmapcontent):
532 def parsemailmap(mailmapcontent):
533 """Parses data in the .mailmap format
533 """Parses data in the .mailmap format
534
534
535 >>> mmdata = b"\\n".join([
535 >>> mmdata = b"\\n".join([
536 ... b'# Comment',
536 ... b'# Comment',
537 ... b'Name <commit1@email.xx>',
537 ... b'Name <commit1@email.xx>',
538 ... b'<name@email.xx> <commit2@email.xx>',
538 ... b'<name@email.xx> <commit2@email.xx>',
539 ... b'Name <proper@email.xx> <commit3@email.xx>',
539 ... b'Name <proper@email.xx> <commit3@email.xx>',
540 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
540 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
541 ... ])
541 ... ])
542 >>> mm = parsemailmap(mmdata)
542 >>> mm = parsemailmap(mmdata)
543 >>> for key in sorted(mm.keys()):
543 >>> for key in sorted(mm.keys()):
544 ... print(key)
544 ... print(key)
545 mailmapping(email='commit1@email.xx', name=None)
545 mailmapping(email='commit1@email.xx', name=None)
546 mailmapping(email='commit2@email.xx', name=None)
546 mailmapping(email='commit2@email.xx', name=None)
547 mailmapping(email='commit3@email.xx', name=None)
547 mailmapping(email='commit3@email.xx', name=None)
548 mailmapping(email='commit4@email.xx', name='Commit')
548 mailmapping(email='commit4@email.xx', name='Commit')
549 >>> for val in sorted(mm.values()):
549 >>> for val in sorted(mm.values()):
550 ... print(val)
550 ... print(val)
551 mailmapping(email='commit1@email.xx', name='Name')
551 mailmapping(email='commit1@email.xx', name='Name')
552 mailmapping(email='name@email.xx', name=None)
552 mailmapping(email='name@email.xx', name=None)
553 mailmapping(email='proper@email.xx', name='Name')
553 mailmapping(email='proper@email.xx', name='Name')
554 mailmapping(email='proper@email.xx', name='Name')
554 mailmapping(email='proper@email.xx', name='Name')
555 """
555 """
556 mailmap = {}
556 mailmap = {}
557
557
558 if mailmapcontent is None:
558 if mailmapcontent is None:
559 return mailmap
559 return mailmap
560
560
561 for line in mailmapcontent.splitlines():
561 for line in mailmapcontent.splitlines():
562
562
563 # Don't bother checking the line if it is a comment or
563 # Don't bother checking the line if it is a comment or
564 # is an improperly formed author field
564 # is an improperly formed author field
565 if line.lstrip().startswith(b'#'):
565 if line.lstrip().startswith(b'#'):
566 continue
566 continue
567
567
568 # names, emails hold the parsed emails and names for each line
568 # names, emails hold the parsed emails and names for each line
569 # name_builder holds the words in a persons name
569 # name_builder holds the words in a persons name
570 names, emails = [], []
570 names, emails = [], []
571 namebuilder = []
571 namebuilder = []
572
572
573 for element in line.split():
573 for element in line.split():
574 if element.startswith(b'#'):
574 if element.startswith(b'#'):
575 # If we reach a comment in the mailmap file, move on
575 # If we reach a comment in the mailmap file, move on
576 break
576 break
577
577
578 elif element.startswith(b'<') and element.endswith(b'>'):
578 elif element.startswith(b'<') and element.endswith(b'>'):
579 # We have found an email.
579 # We have found an email.
580 # Parse it, and finalize any names from earlier
580 # Parse it, and finalize any names from earlier
581 emails.append(element[1:-1]) # Slice off the "<>"
581 emails.append(element[1:-1]) # Slice off the "<>"
582
582
583 if namebuilder:
583 if namebuilder:
584 names.append(b' '.join(namebuilder))
584 names.append(b' '.join(namebuilder))
585 namebuilder = []
585 namebuilder = []
586
586
587 # Break if we have found a second email, any other
587 # Break if we have found a second email, any other
588 # data does not fit the spec for .mailmap
588 # data does not fit the spec for .mailmap
589 if len(emails) > 1:
589 if len(emails) > 1:
590 break
590 break
591
591
592 else:
592 else:
593 # We have found another word in the committers name
593 # We have found another word in the committers name
594 namebuilder.append(element)
594 namebuilder.append(element)
595
595
596 # Check to see if we have parsed the line into a valid form
596 # Check to see if we have parsed the line into a valid form
597 # We require at least one email, and either at least one
597 # We require at least one email, and either at least one
598 # name or a second email
598 # name or a second email
599 if _ismailmaplineinvalid(names, emails):
599 if _ismailmaplineinvalid(names, emails):
600 continue
600 continue
601
601
602 mailmapkey = mailmapping(
602 mailmapkey = mailmapping(
603 email=emails[-1],
603 email=emails[-1],
604 name=names[-1] if len(names) == 2 else None,
604 name=names[-1] if len(names) == 2 else None,
605 )
605 )
606
606
607 mailmap[mailmapkey] = mailmapping(
607 mailmap[mailmapkey] = mailmapping(
608 email=emails[0],
608 email=emails[0],
609 name=names[0] if names else None,
609 name=names[0] if names else None,
610 )
610 )
611
611
612 return mailmap
612 return mailmap
613
613
614
614
615 def mapname(mailmap, author):
615 def mapname(mailmap, author):
616 """Returns the author field according to the mailmap cache, or
616 """Returns the author field according to the mailmap cache, or
617 the original author field.
617 the original author field.
618
618
619 >>> mmdata = b"\\n".join([
619 >>> mmdata = b"\\n".join([
620 ... b'# Comment',
620 ... b'# Comment',
621 ... b'Name <commit1@email.xx>',
621 ... b'Name <commit1@email.xx>',
622 ... b'<name@email.xx> <commit2@email.xx>',
622 ... b'<name@email.xx> <commit2@email.xx>',
623 ... b'Name <proper@email.xx> <commit3@email.xx>',
623 ... b'Name <proper@email.xx> <commit3@email.xx>',
624 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
624 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
625 ... ])
625 ... ])
626 >>> m = parsemailmap(mmdata)
626 >>> m = parsemailmap(mmdata)
627 >>> mapname(m, b'Commit <commit1@email.xx>')
627 >>> mapname(m, b'Commit <commit1@email.xx>')
628 'Name <commit1@email.xx>'
628 'Name <commit1@email.xx>'
629 >>> mapname(m, b'Name <commit2@email.xx>')
629 >>> mapname(m, b'Name <commit2@email.xx>')
630 'Name <name@email.xx>'
630 'Name <name@email.xx>'
631 >>> mapname(m, b'Commit <commit3@email.xx>')
631 >>> mapname(m, b'Commit <commit3@email.xx>')
632 'Name <proper@email.xx>'
632 'Name <proper@email.xx>'
633 >>> mapname(m, b'Commit <commit4@email.xx>')
633 >>> mapname(m, b'Commit <commit4@email.xx>')
634 'Name <proper@email.xx>'
634 'Name <proper@email.xx>'
635 >>> mapname(m, b'Unknown Name <unknown@email.com>')
635 >>> mapname(m, b'Unknown Name <unknown@email.com>')
636 'Unknown Name <unknown@email.com>'
636 'Unknown Name <unknown@email.com>'
637 """
637 """
638 # If the author field coming in isn't in the correct format,
638 # If the author field coming in isn't in the correct format,
639 # or the mailmap is empty just return the original author field
639 # or the mailmap is empty just return the original author field
640 if not isauthorwellformed(author) or not mailmap:
640 if not isauthorwellformed(author) or not mailmap:
641 return author
641 return author
642
642
643 # Turn the user name into a mailmapping
643 # Turn the user name into a mailmapping
644 commit = mailmapping(name=person(author), email=email(author))
644 commit = mailmapping(name=person(author), email=email(author))
645
645
646 try:
646 try:
647 # Try and use both the commit email and name as the key
647 # Try and use both the commit email and name as the key
648 proper = mailmap[commit]
648 proper = mailmap[commit]
649
649
650 except KeyError:
650 except KeyError:
651 # If the lookup fails, use just the email as the key instead
651 # If the lookup fails, use just the email as the key instead
652 # We call this commit2 as not to erase original commit fields
652 # We call this commit2 as not to erase original commit fields
653 commit2 = mailmapping(email=commit.email)
653 commit2 = mailmapping(email=commit.email)
654 proper = mailmap.get(commit2, mailmapping(None, None))
654 proper = mailmap.get(commit2, mailmapping(None, None))
655
655
656 # Return the author field with proper values filled in
656 # Return the author field with proper values filled in
657 return b'%s <%s>' % (
657 return b'%s <%s>' % (
658 proper.name if proper.name else commit.name,
658 proper.name if proper.name else commit.name,
659 proper.email if proper.email else commit.email,
659 proper.email if proper.email else commit.email,
660 )
660 )
661
661
662
662
663 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
663 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
664
664
665
665
666 def isauthorwellformed(author):
666 def isauthorwellformed(author):
667 """Return True if the author field is well formed
667 """Return True if the author field is well formed
668 (ie "Contributor Name <contrib@email.dom>")
668 (ie "Contributor Name <contrib@email.dom>")
669
669
670 >>> isauthorwellformed(b'Good Author <good@author.com>')
670 >>> isauthorwellformed(b'Good Author <good@author.com>')
671 True
671 True
672 >>> isauthorwellformed(b'Author <good@author.com>')
672 >>> isauthorwellformed(b'Author <good@author.com>')
673 True
673 True
674 >>> isauthorwellformed(b'Bad Author')
674 >>> isauthorwellformed(b'Bad Author')
675 False
675 False
676 >>> isauthorwellformed(b'Bad Author <author@author.com')
676 >>> isauthorwellformed(b'Bad Author <author@author.com')
677 False
677 False
678 >>> isauthorwellformed(b'Bad Author author@author.com')
678 >>> isauthorwellformed(b'Bad Author author@author.com')
679 False
679 False
680 >>> isauthorwellformed(b'<author@author.com>')
680 >>> isauthorwellformed(b'<author@author.com>')
681 False
681 False
682 >>> isauthorwellformed(b'Bad Author <author>')
682 >>> isauthorwellformed(b'Bad Author <author>')
683 False
683 False
684 """
684 """
685 return _correctauthorformat.match(author) is not None
685 return _correctauthorformat.match(author) is not None
686
686
687
687
688 def ellipsis(text, maxlength=400):
688 def ellipsis(text, maxlength=400):
689 """Trim string to at most maxlength (default: 400) columns in display."""
689 """Trim string to at most maxlength (default: 400) columns in display."""
690 return encoding.trim(text, maxlength, ellipsis=b'...')
690 return encoding.trim(text, maxlength, ellipsis=b'...')
691
691
692
692
693 def escapestr(s):
693 def escapestr(s):
694 if isinstance(s, memoryview):
694 if isinstance(s, memoryview):
695 s = bytes(s)
695 s = bytes(s)
696 # call underlying function of s.encode('string_escape') directly for
696 # call underlying function of s.encode('string_escape') directly for
697 # Python 3 compatibility
697 # Python 3 compatibility
698 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
698 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
699
699
700
700
701 def unescapestr(s):
701 def unescapestr(s):
702 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
702 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
703
703
704
704
705 def forcebytestr(obj):
705 def forcebytestr(obj):
706 """Portably format an arbitrary object (e.g. exception) into a byte
706 """Portably format an arbitrary object (e.g. exception) into a byte
707 string."""
707 string."""
708 try:
708 try:
709 return pycompat.bytestr(obj)
709 return pycompat.bytestr(obj)
710 except UnicodeEncodeError:
710 except UnicodeEncodeError:
711 # non-ascii string, may be lossy
711 # non-ascii string, may be lossy
712 return pycompat.bytestr(encoding.strtolocal(str(obj)))
712 return pycompat.bytestr(encoding.strtolocal(str(obj)))
713
713
714
714
715 def uirepr(s):
715 def uirepr(s):
716 # Avoid double backslash in Windows path repr()
716 # Avoid double backslash in Windows path repr()
717 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
717 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
718
718
719
719
720 # delay import of textwrap
720 # delay import of textwrap
721 def _MBTextWrapper(**kwargs):
721 def _MBTextWrapper(**kwargs):
722 class tw(textwrap.TextWrapper):
722 class tw(textwrap.TextWrapper):
723 """
723 """
724 Extend TextWrapper for width-awareness.
724 Extend TextWrapper for width-awareness.
725
725
726 Neither number of 'bytes' in any encoding nor 'characters' is
726 Neither number of 'bytes' in any encoding nor 'characters' is
727 appropriate to calculate terminal columns for specified string.
727 appropriate to calculate terminal columns for specified string.
728
728
729 Original TextWrapper implementation uses built-in 'len()' directly,
729 Original TextWrapper implementation uses built-in 'len()' directly,
730 so overriding is needed to use width information of each characters.
730 so overriding is needed to use width information of each characters.
731
731
732 In addition, characters classified into 'ambiguous' width are
732 In addition, characters classified into 'ambiguous' width are
733 treated as wide in East Asian area, but as narrow in other.
733 treated as wide in East Asian area, but as narrow in other.
734
734
735 This requires use decision to determine width of such characters.
735 This requires use decision to determine width of such characters.
736 """
736 """
737
737
738 def _cutdown(self, ucstr, space_left):
738 def _cutdown(self, ucstr, space_left):
739 l = 0
739 l = 0
740 colwidth = encoding.ucolwidth
740 colwidth = encoding.ucolwidth
741 for i in pycompat.xrange(len(ucstr)):
741 for i in pycompat.xrange(len(ucstr)):
742 l += colwidth(ucstr[i])
742 l += colwidth(ucstr[i])
743 if space_left < l:
743 if space_left < l:
744 return (ucstr[:i], ucstr[i:])
744 return (ucstr[:i], ucstr[i:])
745 return ucstr, b''
745 return ucstr, b''
746
746
747 # overriding of base class
747 # overriding of base class
748 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
748 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
749 space_left = max(width - cur_len, 1)
749 space_left = max(width - cur_len, 1)
750
750
751 if self.break_long_words:
751 if self.break_long_words:
752 cut, res = self._cutdown(reversed_chunks[-1], space_left)
752 cut, res = self._cutdown(reversed_chunks[-1], space_left)
753 cur_line.append(cut)
753 cur_line.append(cut)
754 reversed_chunks[-1] = res
754 reversed_chunks[-1] = res
755 elif not cur_line:
755 elif not cur_line:
756 cur_line.append(reversed_chunks.pop())
756 cur_line.append(reversed_chunks.pop())
757
757
758 # this overriding code is imported from TextWrapper of Python 2.6
758 # this overriding code is imported from TextWrapper of Python 2.6
759 # to calculate columns of string by 'encoding.ucolwidth()'
759 # to calculate columns of string by 'encoding.ucolwidth()'
760 def _wrap_chunks(self, chunks):
760 def _wrap_chunks(self, chunks):
761 colwidth = encoding.ucolwidth
761 colwidth = encoding.ucolwidth
762
762
763 lines = []
763 lines = []
764 if self.width <= 0:
764 if self.width <= 0:
765 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
765 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
766
766
767 # Arrange in reverse order so items can be efficiently popped
767 # Arrange in reverse order so items can be efficiently popped
768 # from a stack of chucks.
768 # from a stack of chucks.
769 chunks.reverse()
769 chunks.reverse()
770
770
771 while chunks:
771 while chunks:
772
772
773 # Start the list of chunks that will make up the current line.
773 # Start the list of chunks that will make up the current line.
774 # cur_len is just the length of all the chunks in cur_line.
774 # cur_len is just the length of all the chunks in cur_line.
775 cur_line = []
775 cur_line = []
776 cur_len = 0
776 cur_len = 0
777
777
778 # Figure out which static string will prefix this line.
778 # Figure out which static string will prefix this line.
779 if lines:
779 if lines:
780 indent = self.subsequent_indent
780 indent = self.subsequent_indent
781 else:
781 else:
782 indent = self.initial_indent
782 indent = self.initial_indent
783
783
784 # Maximum width for this line.
784 # Maximum width for this line.
785 width = self.width - len(indent)
785 width = self.width - len(indent)
786
786
787 # First chunk on line is whitespace -- drop it, unless this
787 # First chunk on line is whitespace -- drop it, unless this
788 # is the very beginning of the text (i.e. no lines started yet).
788 # is the very beginning of the text (i.e. no lines started yet).
789 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
789 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
790 del chunks[-1]
790 del chunks[-1]
791
791
792 while chunks:
792 while chunks:
793 l = colwidth(chunks[-1])
793 l = colwidth(chunks[-1])
794
794
795 # Can at least squeeze this chunk onto the current line.
795 # Can at least squeeze this chunk onto the current line.
796 if cur_len + l <= width:
796 if cur_len + l <= width:
797 cur_line.append(chunks.pop())
797 cur_line.append(chunks.pop())
798 cur_len += l
798 cur_len += l
799
799
800 # Nope, this line is full.
800 # Nope, this line is full.
801 else:
801 else:
802 break
802 break
803
803
804 # The current line is full, and the next chunk is too big to
804 # The current line is full, and the next chunk is too big to
805 # fit on *any* line (not just this one).
805 # fit on *any* line (not just this one).
806 if chunks and colwidth(chunks[-1]) > width:
806 if chunks and colwidth(chunks[-1]) > width:
807 self._handle_long_word(chunks, cur_line, cur_len, width)
807 self._handle_long_word(chunks, cur_line, cur_len, width)
808
808
809 # If the last chunk on this line is all whitespace, drop it.
809 # If the last chunk on this line is all whitespace, drop it.
810 if (
810 if (
811 self.drop_whitespace
811 self.drop_whitespace
812 and cur_line
812 and cur_line
813 and cur_line[-1].strip() == r''
813 and cur_line[-1].strip() == r''
814 ):
814 ):
815 del cur_line[-1]
815 del cur_line[-1]
816
816
817 # Convert current line back to a string and store it in list
817 # Convert current line back to a string and store it in list
818 # of all lines (return value).
818 # of all lines (return value).
819 if cur_line:
819 if cur_line:
820 lines.append(indent + ''.join(cur_line))
820 lines.append(indent + ''.join(cur_line))
821
821
822 return lines
822 return lines
823
823
824 global _MBTextWrapper
824 global _MBTextWrapper
825 _MBTextWrapper = tw
825 _MBTextWrapper = tw
826 return tw(**kwargs)
826 return tw(**kwargs)
827
827
828
828
829 def wrap(line, width, initindent=b'', hangindent=b''):
829 def wrap(line, width, initindent=b'', hangindent=b''):
830 maxindent = max(len(hangindent), len(initindent))
830 maxindent = max(len(hangindent), len(initindent))
831 if width <= maxindent:
831 if width <= maxindent:
832 # adjust for weird terminal size
832 # adjust for weird terminal size
833 width = max(78, maxindent + 1)
833 width = max(78, maxindent + 1)
834 line = line.decode(
834 line = line.decode(
835 pycompat.sysstr(encoding.encoding),
835 pycompat.sysstr(encoding.encoding),
836 pycompat.sysstr(encoding.encodingmode),
836 pycompat.sysstr(encoding.encodingmode),
837 )
837 )
838 initindent = initindent.decode(
838 initindent = initindent.decode(
839 pycompat.sysstr(encoding.encoding),
839 pycompat.sysstr(encoding.encoding),
840 pycompat.sysstr(encoding.encodingmode),
840 pycompat.sysstr(encoding.encodingmode),
841 )
841 )
842 hangindent = hangindent.decode(
842 hangindent = hangindent.decode(
843 pycompat.sysstr(encoding.encoding),
843 pycompat.sysstr(encoding.encoding),
844 pycompat.sysstr(encoding.encodingmode),
844 pycompat.sysstr(encoding.encodingmode),
845 )
845 )
846 wrapper = _MBTextWrapper(
846 wrapper = _MBTextWrapper(
847 width=width, initial_indent=initindent, subsequent_indent=hangindent
847 width=width, initial_indent=initindent, subsequent_indent=hangindent
848 )
848 )
849 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
849 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
850
850
851
851
852 _booleans = {
852 _booleans = {
853 b'1': True,
853 b'1': True,
854 b'yes': True,
854 b'yes': True,
855 b'true': True,
855 b'true': True,
856 b'on': True,
856 b'on': True,
857 b'always': True,
857 b'always': True,
858 b'0': False,
858 b'0': False,
859 b'no': False,
859 b'no': False,
860 b'false': False,
860 b'false': False,
861 b'off': False,
861 b'off': False,
862 b'never': False,
862 b'never': False,
863 }
863 }
864
864
865
865
866 def parsebool(s):
866 def parsebool(s):
867 """Parse s into a boolean.
867 """Parse s into a boolean.
868
868
869 If s is not a valid boolean, returns None.
869 If s is not a valid boolean, returns None.
870 """
870 """
871 return _booleans.get(s.lower(), None)
871 return _booleans.get(s.lower(), None)
872
872
873
873
874 def parselist(value):
874 def parselist(value):
875 """parse a configuration value as a list of comma/space separated strings
875 """parse a configuration value as a list of comma/space separated strings
876
876
877 >>> parselist(b'this,is "a small" ,test')
877 >>> parselist(b'this,is "a small" ,test')
878 ['this', 'is', 'a small', 'test']
878 ['this', 'is', 'a small', 'test']
879 """
879 """
880
880
881 def _parse_plain(parts, s, offset):
881 def _parse_plain(parts, s, offset):
882 whitespace = False
882 whitespace = False
883 while offset < len(s) and (
883 while offset < len(s) and (
884 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
884 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
885 ):
885 ):
886 whitespace = True
886 whitespace = True
887 offset += 1
887 offset += 1
888 if offset >= len(s):
888 if offset >= len(s):
889 return None, parts, offset
889 return None, parts, offset
890 if whitespace:
890 if whitespace:
891 parts.append(b'')
891 parts.append(b'')
892 if s[offset : offset + 1] == b'"' and not parts[-1]:
892 if s[offset : offset + 1] == b'"' and not parts[-1]:
893 return _parse_quote, parts, offset + 1
893 return _parse_quote, parts, offset + 1
894 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
894 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
895 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
895 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
896 return _parse_plain, parts, offset + 1
896 return _parse_plain, parts, offset + 1
897 parts[-1] += s[offset : offset + 1]
897 parts[-1] += s[offset : offset + 1]
898 return _parse_plain, parts, offset + 1
898 return _parse_plain, parts, offset + 1
899
899
900 def _parse_quote(parts, s, offset):
900 def _parse_quote(parts, s, offset):
901 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
901 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
902 parts.append(b'')
902 parts.append(b'')
903 offset += 1
903 offset += 1
904 while offset < len(s) and (
904 while offset < len(s) and (
905 s[offset : offset + 1].isspace()
905 s[offset : offset + 1].isspace()
906 or s[offset : offset + 1] == b','
906 or s[offset : offset + 1] == b','
907 ):
907 ):
908 offset += 1
908 offset += 1
909 return _parse_plain, parts, offset
909 return _parse_plain, parts, offset
910
910
911 while offset < len(s) and s[offset : offset + 1] != b'"':
911 while offset < len(s) and s[offset : offset + 1] != b'"':
912 if (
912 if (
913 s[offset : offset + 1] == b'\\'
913 s[offset : offset + 1] == b'\\'
914 and offset + 1 < len(s)
914 and offset + 1 < len(s)
915 and s[offset + 1 : offset + 2] == b'"'
915 and s[offset + 1 : offset + 2] == b'"'
916 ):
916 ):
917 offset += 1
917 offset += 1
918 parts[-1] += b'"'
918 parts[-1] += b'"'
919 else:
919 else:
920 parts[-1] += s[offset : offset + 1]
920 parts[-1] += s[offset : offset + 1]
921 offset += 1
921 offset += 1
922
922
923 if offset >= len(s):
923 if offset >= len(s):
924 real_parts = _configlist(parts[-1])
924 real_parts = _configlist(parts[-1])
925 if not real_parts:
925 if not real_parts:
926 parts[-1] = b'"'
926 parts[-1] = b'"'
927 else:
927 else:
928 real_parts[0] = b'"' + real_parts[0]
928 real_parts[0] = b'"' + real_parts[0]
929 parts = parts[:-1]
929 parts = parts[:-1]
930 parts.extend(real_parts)
930 parts.extend(real_parts)
931 return None, parts, offset
931 return None, parts, offset
932
932
933 offset += 1
933 offset += 1
934 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
934 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
935 offset += 1
935 offset += 1
936
936
937 if offset < len(s):
937 if offset < len(s):
938 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
938 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
939 parts[-1] += b'"'
939 parts[-1] += b'"'
940 offset += 1
940 offset += 1
941 else:
941 else:
942 parts.append(b'')
942 parts.append(b'')
943 else:
943 else:
944 return None, parts, offset
944 return None, parts, offset
945
945
946 return _parse_plain, parts, offset
946 return _parse_plain, parts, offset
947
947
948 def _configlist(s):
948 def _configlist(s):
949 s = s.rstrip(b' ,')
949 s = s.rstrip(b' ,')
950 if not s:
950 if not s:
951 return []
951 return []
952 parser, parts, offset = _parse_plain, [b''], 0
952 parser, parts, offset = _parse_plain, [b''], 0
953 while parser:
953 while parser:
954 parser, parts, offset = parser(parts, s, offset)
954 parser, parts, offset = parser(parts, s, offset)
955 return parts
955 return parts
956
956
957 if value is not None and isinstance(value, bytes):
957 if value is not None and isinstance(value, bytes):
958 result = _configlist(value.lstrip(b' ,\n'))
958 result = _configlist(value.lstrip(b' ,\n'))
959 else:
959 else:
960 result = value
960 result = value
961 return result or []
961 return result or []
962
962
963
963
964 def evalpythonliteral(s):
964 def evalpythonliteral(s):
965 """Evaluate a string containing a Python literal expression"""
965 """Evaluate a string containing a Python literal expression"""
966 # We could backport our tokenizer hack to rewrite '' to u'' if we want
966 # We could backport our tokenizer hack to rewrite '' to u'' if we want
967 if pycompat.ispy3:
967 return ast.literal_eval(s.decode('latin1'))
968 return ast.literal_eval(s.decode('latin1'))
969 return ast.literal_eval(s)
General Comments 0
You need to be logged in to leave comments. Login now