##// END OF EJS Templates
template: FileNotFoundError is actually a built in exception...
marmoute -
r48665:2b76255a stable
parent child Browse files
Show More
@@ -1,529 +1,533 b''
1 1 # pycompat.py - portability shim for python 3
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """Mercurial portability shim for python 3.
7 7
8 8 This contains aliases to hide python version-specific details from the core.
9 9 """
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import getopt
14 14 import inspect
15 15 import json
16 16 import os
17 17 import shlex
18 18 import sys
19 19 import tempfile
20 20
21 21 ispy3 = sys.version_info[0] >= 3
22 22 ispypy = '__pypy__' in sys.builtin_module_names
23 23 TYPE_CHECKING = False
24 24
25 25 if not globals(): # hide this from non-pytype users
26 26 import typing
27 27
28 28 TYPE_CHECKING = typing.TYPE_CHECKING
29 29
30 30 if not ispy3:
31 31 import cookielib
32 32 import cPickle as pickle
33 33 import httplib
34 34 import Queue as queue
35 35 import SocketServer as socketserver
36 36 import xmlrpclib
37 37
38 38 from .thirdparty.concurrent import futures
39 39
40 40 def future_set_exception_info(f, exc_info):
41 41 f.set_exception_info(*exc_info)
42 42
43 # this is close enough for our usage
44 FileNotFoundError = OSError
43 45
44 46 else:
45 47 import concurrent.futures as futures
46 48 import http.cookiejar as cookielib
47 49 import http.client as httplib
48 50 import pickle
49 51 import queue as queue
50 52 import socketserver
51 53 import xmlrpc.client as xmlrpclib
52 54
53 55 def future_set_exception_info(f, exc_info):
54 56 f.set_exception(exc_info[0])
55 57
58 FileNotFoundError = __builtins__['FileNotFoundError']
59
56 60
57 61 def identity(a):
58 62 return a
59 63
60 64
61 65 def _rapply(f, xs):
62 66 if xs is None:
63 67 # assume None means non-value of optional data
64 68 return xs
65 69 if isinstance(xs, (list, set, tuple)):
66 70 return type(xs)(_rapply(f, x) for x in xs)
67 71 if isinstance(xs, dict):
68 72 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
69 73 return f(xs)
70 74
71 75
72 76 def rapply(f, xs):
73 77 """Apply function recursively to every item preserving the data structure
74 78
75 79 >>> def f(x):
76 80 ... return 'f(%s)' % x
77 81 >>> rapply(f, None) is None
78 82 True
79 83 >>> rapply(f, 'a')
80 84 'f(a)'
81 85 >>> rapply(f, {'a'}) == {'f(a)'}
82 86 True
83 87 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
84 88 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
85 89
86 90 >>> xs = [object()]
87 91 >>> rapply(identity, xs) is xs
88 92 True
89 93 """
90 94 if f is identity:
91 95 # fast path mainly for py2
92 96 return xs
93 97 return _rapply(f, xs)
94 98
95 99
96 100 if ispy3:
97 101 import builtins
98 102 import codecs
99 103 import functools
100 104 import io
101 105 import struct
102 106
103 107 if os.name == r'nt' and sys.version_info >= (3, 6):
104 108 # MBCS (or ANSI) filesystem encoding must be used as before.
105 109 # Otherwise non-ASCII filenames in existing repositories would be
106 110 # corrupted.
107 111 # This must be set once prior to any fsencode/fsdecode calls.
108 112 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
109 113
110 114 fsencode = os.fsencode
111 115 fsdecode = os.fsdecode
112 116 oscurdir = os.curdir.encode('ascii')
113 117 oslinesep = os.linesep.encode('ascii')
114 118 osname = os.name.encode('ascii')
115 119 ospathsep = os.pathsep.encode('ascii')
116 120 ospardir = os.pardir.encode('ascii')
117 121 ossep = os.sep.encode('ascii')
118 122 osaltsep = os.altsep
119 123 if osaltsep:
120 124 osaltsep = osaltsep.encode('ascii')
121 125 osdevnull = os.devnull.encode('ascii')
122 126
123 127 sysplatform = sys.platform.encode('ascii')
124 128 sysexecutable = sys.executable
125 129 if sysexecutable:
126 130 sysexecutable = os.fsencode(sysexecutable)
127 131 bytesio = io.BytesIO
128 132 # TODO deprecate stringio name, as it is a lie on Python 3.
129 133 stringio = bytesio
130 134
131 135 def maplist(*args):
132 136 return list(map(*args))
133 137
134 138 def rangelist(*args):
135 139 return list(range(*args))
136 140
137 141 def ziplist(*args):
138 142 return list(zip(*args))
139 143
140 144 rawinput = input
141 145 getargspec = inspect.getfullargspec
142 146
143 147 long = int
144 148
145 149 if getattr(sys, 'argv', None) is not None:
146 150 # On POSIX, the char** argv array is converted to Python str using
147 151 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
148 152 # isn't directly callable from Python code. In practice, os.fsencode()
149 153 # can be used instead (this is recommended by Python's documentation
150 154 # for sys.argv).
151 155 #
152 156 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
153 157 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
154 158 # there's an additional wrinkle. What we really want to access is the
155 159 # ANSI codepage representation of the arguments, as this is what
156 160 # `int main()` would receive if Python 3 didn't define `int wmain()`
157 161 # (this is how Python 2 worked). To get that, we encode with the mbcs
158 162 # encoding, which will pass CP_ACP to the underlying Windows API to
159 163 # produce bytes.
160 164 if os.name == r'nt':
161 165 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
162 166 else:
163 167 sysargv = [fsencode(a) for a in sys.argv]
164 168
165 169 bytechr = struct.Struct('>B').pack
166 170 byterepr = b'%r'.__mod__
167 171
168 172 class bytestr(bytes):
169 173 """A bytes which mostly acts as a Python 2 str
170 174
171 175 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
172 176 ('', 'foo', 'ascii', '1')
173 177 >>> s = bytestr(b'foo')
174 178 >>> assert s is bytestr(s)
175 179
176 180 __bytes__() should be called if provided:
177 181
178 182 >>> class bytesable(object):
179 183 ... def __bytes__(self):
180 184 ... return b'bytes'
181 185 >>> bytestr(bytesable())
182 186 'bytes'
183 187
184 188 There's no implicit conversion from non-ascii str as its encoding is
185 189 unknown:
186 190
187 191 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
188 192 Traceback (most recent call last):
189 193 ...
190 194 UnicodeEncodeError: ...
191 195
192 196 Comparison between bytestr and bytes should work:
193 197
194 198 >>> assert bytestr(b'foo') == b'foo'
195 199 >>> assert b'foo' == bytestr(b'foo')
196 200 >>> assert b'f' in bytestr(b'foo')
197 201 >>> assert bytestr(b'f') in b'foo'
198 202
199 203 Sliced elements should be bytes, not integer:
200 204
201 205 >>> s[1], s[:2]
202 206 (b'o', b'fo')
203 207 >>> list(s), list(reversed(s))
204 208 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
205 209
206 210 As bytestr type isn't propagated across operations, you need to cast
207 211 bytes to bytestr explicitly:
208 212
209 213 >>> s = bytestr(b'foo').upper()
210 214 >>> t = bytestr(s)
211 215 >>> s[0], t[0]
212 216 (70, b'F')
213 217
214 218 Be careful to not pass a bytestr object to a function which expects
215 219 bytearray-like behavior.
216 220
217 221 >>> t = bytes(t) # cast to bytes
218 222 >>> assert type(t) is bytes
219 223 """
220 224
221 225 def __new__(cls, s=b''):
222 226 if isinstance(s, bytestr):
223 227 return s
224 228 if not isinstance(
225 229 s, (bytes, bytearray)
226 230 ) and not hasattr( # hasattr-py3-only
227 231 s, u'__bytes__'
228 232 ):
229 233 s = str(s).encode('ascii')
230 234 return bytes.__new__(cls, s)
231 235
232 236 def __getitem__(self, key):
233 237 s = bytes.__getitem__(self, key)
234 238 if not isinstance(s, bytes):
235 239 s = bytechr(s)
236 240 return s
237 241
238 242 def __iter__(self):
239 243 return iterbytestr(bytes.__iter__(self))
240 244
241 245 def __repr__(self):
242 246 return bytes.__repr__(self)[1:] # drop b''
243 247
244 248 def iterbytestr(s):
245 249 """Iterate bytes as if it were a str object of Python 2"""
246 250 return map(bytechr, s)
247 251
248 252 def maybebytestr(s):
249 253 """Promote bytes to bytestr"""
250 254 if isinstance(s, bytes):
251 255 return bytestr(s)
252 256 return s
253 257
254 258 def sysbytes(s):
255 259 """Convert an internal str (e.g. keyword, __doc__) back to bytes
256 260
257 261 This never raises UnicodeEncodeError, but only ASCII characters
258 262 can be round-trip by sysstr(sysbytes(s)).
259 263 """
260 264 if isinstance(s, bytes):
261 265 return s
262 266 return s.encode('utf-8')
263 267
264 268 def sysstr(s):
265 269 """Return a keyword str to be passed to Python functions such as
266 270 getattr() and str.encode()
267 271
268 272 This never raises UnicodeDecodeError. Non-ascii characters are
269 273 considered invalid and mapped to arbitrary but unique code points
270 274 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
271 275 """
272 276 if isinstance(s, builtins.str):
273 277 return s
274 278 return s.decode('latin-1')
275 279
276 280 def strurl(url):
277 281 """Converts a bytes url back to str"""
278 282 if isinstance(url, bytes):
279 283 return url.decode('ascii')
280 284 return url
281 285
282 286 def bytesurl(url):
283 287 """Converts a str url to bytes by encoding in ascii"""
284 288 if isinstance(url, str):
285 289 return url.encode('ascii')
286 290 return url
287 291
288 292 def raisewithtb(exc, tb):
289 293 """Raise exception with the given traceback"""
290 294 raise exc.with_traceback(tb)
291 295
292 296 def getdoc(obj):
293 297 """Get docstring as bytes; may be None so gettext() won't confuse it
294 298 with _('')"""
295 299 doc = getattr(obj, '__doc__', None)
296 300 if doc is None:
297 301 return doc
298 302 return sysbytes(doc)
299 303
300 304 def _wrapattrfunc(f):
301 305 @functools.wraps(f)
302 306 def w(object, name, *args):
303 307 return f(object, sysstr(name), *args)
304 308
305 309 return w
306 310
307 311 # these wrappers are automagically imported by hgloader
308 312 delattr = _wrapattrfunc(builtins.delattr)
309 313 getattr = _wrapattrfunc(builtins.getattr)
310 314 hasattr = _wrapattrfunc(builtins.hasattr)
311 315 setattr = _wrapattrfunc(builtins.setattr)
312 316 xrange = builtins.range
313 317 unicode = str
314 318
315 319 def open(name, mode=b'r', buffering=-1, encoding=None):
316 320 return builtins.open(name, sysstr(mode), buffering, encoding)
317 321
318 322 safehasattr = _wrapattrfunc(builtins.hasattr)
319 323
320 324 def _getoptbwrapper(orig, args, shortlist, namelist):
321 325 """
322 326 Takes bytes arguments, converts them to unicode, pass them to
323 327 getopt.getopt(), convert the returned values back to bytes and then
324 328 return them for Python 3 compatibility as getopt.getopt() don't accepts
325 329 bytes on Python 3.
326 330 """
327 331 args = [a.decode('latin-1') for a in args]
328 332 shortlist = shortlist.decode('latin-1')
329 333 namelist = [a.decode('latin-1') for a in namelist]
330 334 opts, args = orig(args, shortlist, namelist)
331 335 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
332 336 args = [a.encode('latin-1') for a in args]
333 337 return opts, args
334 338
335 339 def strkwargs(dic):
336 340 """
337 341 Converts the keys of a python dictonary to str i.e. unicodes so that
338 342 they can be passed as keyword arguments as dictionaries with bytes keys
339 343 can't be passed as keyword arguments to functions on Python 3.
340 344 """
341 345 dic = {k.decode('latin-1'): v for k, v in dic.items()}
342 346 return dic
343 347
344 348 def byteskwargs(dic):
345 349 """
346 350 Converts keys of python dictionaries to bytes as they were converted to
347 351 str to pass that dictonary as a keyword argument on Python 3.
348 352 """
349 353 dic = {k.encode('latin-1'): v for k, v in dic.items()}
350 354 return dic
351 355
352 356 # TODO: handle shlex.shlex().
353 357 def shlexsplit(s, comments=False, posix=True):
354 358 """
355 359 Takes bytes argument, convert it to str i.e. unicodes, pass that into
356 360 shlex.split(), convert the returned value to bytes and return that for
357 361 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
358 362 """
359 363 ret = shlex.split(s.decode('latin-1'), comments, posix)
360 364 return [a.encode('latin-1') for a in ret]
361 365
362 366 iteritems = lambda x: x.items()
363 367 itervalues = lambda x: x.values()
364 368
365 369 # Python 3.5's json.load and json.loads require str. We polyfill its
366 370 # code for detecting encoding from bytes.
367 371 if sys.version_info[0:2] < (3, 6):
368 372
369 373 def _detect_encoding(b):
370 374 bstartswith = b.startswith
371 375 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
372 376 return 'utf-32'
373 377 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
374 378 return 'utf-16'
375 379 if bstartswith(codecs.BOM_UTF8):
376 380 return 'utf-8-sig'
377 381
378 382 if len(b) >= 4:
379 383 if not b[0]:
380 384 # 00 00 -- -- - utf-32-be
381 385 # 00 XX -- -- - utf-16-be
382 386 return 'utf-16-be' if b[1] else 'utf-32-be'
383 387 if not b[1]:
384 388 # XX 00 00 00 - utf-32-le
385 389 # XX 00 00 XX - utf-16-le
386 390 # XX 00 XX -- - utf-16-le
387 391 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
388 392 elif len(b) == 2:
389 393 if not b[0]:
390 394 # 00 XX - utf-16-be
391 395 return 'utf-16-be'
392 396 if not b[1]:
393 397 # XX 00 - utf-16-le
394 398 return 'utf-16-le'
395 399 # default
396 400 return 'utf-8'
397 401
398 402 def json_loads(s, *args, **kwargs):
399 403 if isinstance(s, (bytes, bytearray)):
400 404 s = s.decode(_detect_encoding(s), 'surrogatepass')
401 405
402 406 return json.loads(s, *args, **kwargs)
403 407
404 408 else:
405 409 json_loads = json.loads
406 410
407 411 else:
408 412 import cStringIO
409 413
410 414 xrange = xrange
411 415 unicode = unicode
412 416 bytechr = chr
413 417 byterepr = repr
414 418 bytestr = str
415 419 iterbytestr = iter
416 420 maybebytestr = identity
417 421 sysbytes = identity
418 422 sysstr = identity
419 423 strurl = identity
420 424 bytesurl = identity
421 425 open = open
422 426 delattr = delattr
423 427 getattr = getattr
424 428 hasattr = hasattr
425 429 setattr = setattr
426 430
427 431 # this can't be parsed on Python 3
428 432 exec(b'def raisewithtb(exc, tb):\n raise exc, None, tb\n')
429 433
430 434 def fsencode(filename):
431 435 """
432 436 Partial backport from os.py in Python 3, which only accepts bytes.
433 437 In Python 2, our paths should only ever be bytes, a unicode path
434 438 indicates a bug.
435 439 """
436 440 if isinstance(filename, str):
437 441 return filename
438 442 else:
439 443 raise TypeError("expect str, not %s" % type(filename).__name__)
440 444
441 445 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
442 446 # better not to touch Python 2 part as it's already working fine.
443 447 fsdecode = identity
444 448
445 449 def getdoc(obj):
446 450 return getattr(obj, '__doc__', None)
447 451
448 452 _notset = object()
449 453
450 454 def safehasattr(thing, attr):
451 455 return getattr(thing, attr, _notset) is not _notset
452 456
453 457 def _getoptbwrapper(orig, args, shortlist, namelist):
454 458 return orig(args, shortlist, namelist)
455 459
456 460 strkwargs = identity
457 461 byteskwargs = identity
458 462
459 463 oscurdir = os.curdir
460 464 oslinesep = os.linesep
461 465 osname = os.name
462 466 ospathsep = os.pathsep
463 467 ospardir = os.pardir
464 468 ossep = os.sep
465 469 osaltsep = os.altsep
466 470 osdevnull = os.devnull
467 471 long = long
468 472 if getattr(sys, 'argv', None) is not None:
469 473 sysargv = sys.argv
470 474 sysplatform = sys.platform
471 475 sysexecutable = sys.executable
472 476 shlexsplit = shlex.split
473 477 bytesio = cStringIO.StringIO
474 478 stringio = bytesio
475 479 maplist = map
476 480 rangelist = range
477 481 ziplist = zip
478 482 rawinput = raw_input
479 483 getargspec = inspect.getargspec
480 484 iteritems = lambda x: x.iteritems()
481 485 itervalues = lambda x: x.itervalues()
482 486 json_loads = json.loads
483 487
484 488 isjython = sysplatform.startswith(b'java')
485 489
486 490 isdarwin = sysplatform.startswith(b'darwin')
487 491 islinux = sysplatform.startswith(b'linux')
488 492 isposix = osname == b'posix'
489 493 iswindows = osname == b'nt'
490 494
491 495
492 496 def getoptb(args, shortlist, namelist):
493 497 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
494 498
495 499
496 500 def gnugetoptb(args, shortlist, namelist):
497 501 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
498 502
499 503
500 504 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
501 505 return tempfile.mkdtemp(suffix, prefix, dir)
502 506
503 507
504 508 # text=True is not supported; use util.from/tonativeeol() instead
505 509 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
506 510 return tempfile.mkstemp(suffix, prefix, dir)
507 511
508 512
509 513 # TemporaryFile does not support an "encoding=" argument on python2.
510 514 # This wrapper file are always open in byte mode.
511 515 def unnamedtempfile(mode=None, *args, **kwargs):
512 516 if mode is None:
513 517 mode = 'w+b'
514 518 else:
515 519 mode = sysstr(mode)
516 520 assert 'b' in mode
517 521 return tempfile.TemporaryFile(mode, *args, **kwargs)
518 522
519 523
520 524 # NamedTemporaryFile does not support an "encoding=" argument on python2.
521 525 # This wrapper file are always open in byte mode.
522 526 def namedtempfile(
523 527 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
524 528 ):
525 529 mode = sysstr(mode)
526 530 assert 'b' in mode
527 531 return tempfile.NamedTemporaryFile(
528 532 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
529 533 )
@@ -1,1149 +1,1152 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Slightly complicated template engine for commands and hgweb
9 9
10 10 This module provides low-level interface to the template engine. See the
11 11 formatter and cmdutil modules if you are looking for high-level functions
12 12 such as ``cmdutil.rendertemplate(ctx, tmpl)``.
13 13
14 14 Internal Data Types
15 15 -------------------
16 16
17 17 Template keywords and functions take a dictionary of current symbols and
18 18 resources (a "mapping") and return result. Inputs and outputs must be one
19 19 of the following data types:
20 20
21 21 bytes
22 22 a byte string, which is generally a human-readable text in local encoding.
23 23
24 24 generator
25 25 a lazily-evaluated byte string, which is a possibly nested generator of
26 26 values of any printable types, and will be folded by ``stringify()``
27 27 or ``flatten()``.
28 28
29 29 None
30 30 sometimes represents an empty value, which can be stringified to ''.
31 31
32 32 True, False, int, float
33 33 can be stringified as such.
34 34
35 35 wrappedbytes, wrappedvalue
36 36 a wrapper for the above printable types.
37 37
38 38 date
39 39 represents a (unixtime, offset) tuple.
40 40
41 41 hybrid
42 42 represents a list/dict of printable values, which can also be converted
43 43 to mappings by % operator.
44 44
45 45 hybriditem
46 46 represents a scalar printable value, also supports % operator.
47 47
48 48 revslist
49 49 represents a list of revision numbers.
50 50
51 51 mappinggenerator, mappinglist
52 52 represents mappings (i.e. a list of dicts), which may have default
53 53 output format.
54 54
55 55 mappingdict
56 56 represents a single mapping (i.e. a dict), which may have default output
57 57 format.
58 58
59 59 mappingnone
60 60 represents None of Optional[mappable], which will be mapped to an empty
61 61 string by % operation.
62 62
63 63 mappedgenerator
64 64 a lazily-evaluated list of byte strings, which is e.g. a result of %
65 65 operation.
66 66 """
67 67
68 68 from __future__ import absolute_import, print_function
69 69
70 70 import abc
71 71 import os
72 72
73 73 from .i18n import _
74 from .pycompat import getattr
74 from .pycompat import (
75 FileNotFoundError,
76 getattr,
77 )
75 78 from . import (
76 79 config,
77 80 encoding,
78 81 error,
79 82 parser,
80 83 pycompat,
81 84 templatefilters,
82 85 templatefuncs,
83 86 templateutil,
84 87 util,
85 88 )
86 89 from .utils import (
87 90 resourceutil,
88 91 stringutil,
89 92 )
90 93
91 94 # template parsing
92 95
93 96 elements = {
94 97 # token-type: binding-strength, primary, prefix, infix, suffix
95 98 b"(": (20, None, (b"group", 1, b")"), (b"func", 1, b")"), None),
96 99 b".": (18, None, None, (b".", 18), None),
97 100 b"%": (15, None, None, (b"%", 15), None),
98 101 b"|": (15, None, None, (b"|", 15), None),
99 102 b"*": (5, None, None, (b"*", 5), None),
100 103 b"/": (5, None, None, (b"/", 5), None),
101 104 b"+": (4, None, None, (b"+", 4), None),
102 105 b"-": (4, None, (b"negate", 19), (b"-", 4), None),
103 106 b"=": (3, None, None, (b"keyvalue", 3), None),
104 107 b",": (2, None, None, (b"list", 2), None),
105 108 b")": (0, None, None, None, None),
106 109 b"integer": (0, b"integer", None, None, None),
107 110 b"symbol": (0, b"symbol", None, None, None),
108 111 b"string": (0, b"string", None, None, None),
109 112 b"template": (0, b"template", None, None, None),
110 113 b"end": (0, None, None, None, None),
111 114 }
112 115
113 116
114 117 def tokenize(program, start, end, term=None):
115 118 """Parse a template expression into a stream of tokens, which must end
116 119 with term if specified"""
117 120 pos = start
118 121 program = pycompat.bytestr(program)
119 122 while pos < end:
120 123 c = program[pos]
121 124 if c.isspace(): # skip inter-token whitespace
122 125 pass
123 126 elif c in b"(=,).%|+-*/": # handle simple operators
124 127 yield (c, None, pos)
125 128 elif c in b'"\'': # handle quoted templates
126 129 s = pos + 1
127 130 data, pos = _parsetemplate(program, s, end, c)
128 131 yield (b'template', data, s)
129 132 pos -= 1
130 133 elif c == b'r' and program[pos : pos + 2] in (b"r'", b'r"'):
131 134 # handle quoted strings
132 135 c = program[pos + 1]
133 136 s = pos = pos + 2
134 137 while pos < end: # find closing quote
135 138 d = program[pos]
136 139 if d == b'\\': # skip over escaped characters
137 140 pos += 2
138 141 continue
139 142 if d == c:
140 143 yield (b'string', program[s:pos], s)
141 144 break
142 145 pos += 1
143 146 else:
144 147 raise error.ParseError(_(b"unterminated string"), s)
145 148 elif c.isdigit():
146 149 s = pos
147 150 while pos < end:
148 151 d = program[pos]
149 152 if not d.isdigit():
150 153 break
151 154 pos += 1
152 155 yield (b'integer', program[s:pos], s)
153 156 pos -= 1
154 157 elif (
155 158 c == b'\\'
156 159 and program[pos : pos + 2] in (br"\'", br'\"')
157 160 or c == b'r'
158 161 and program[pos : pos + 3] in (br"r\'", br'r\"')
159 162 ):
160 163 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
161 164 # where some of nested templates were preprocessed as strings and
162 165 # then compiled. therefore, \"...\" was allowed. (issue4733)
163 166 #
164 167 # processing flow of _evalifliteral() at 5ab28a2e9962:
165 168 # outer template string -> stringify() -> compiletemplate()
166 169 # ------------------------ ------------ ------------------
167 170 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
168 171 # ~~~~~~~~
169 172 # escaped quoted string
170 173 if c == b'r':
171 174 pos += 1
172 175 token = b'string'
173 176 else:
174 177 token = b'template'
175 178 quote = program[pos : pos + 2]
176 179 s = pos = pos + 2
177 180 while pos < end: # find closing escaped quote
178 181 if program.startswith(b'\\\\\\', pos, end):
179 182 pos += 4 # skip over double escaped characters
180 183 continue
181 184 if program.startswith(quote, pos, end):
182 185 # interpret as if it were a part of an outer string
183 186 data = parser.unescapestr(program[s:pos])
184 187 if token == b'template':
185 188 data = _parsetemplate(data, 0, len(data))[0]
186 189 yield (token, data, s)
187 190 pos += 1
188 191 break
189 192 pos += 1
190 193 else:
191 194 raise error.ParseError(_(b"unterminated string"), s)
192 195 elif c.isalnum() or c in b'_':
193 196 s = pos
194 197 pos += 1
195 198 while pos < end: # find end of symbol
196 199 d = program[pos]
197 200 if not (d.isalnum() or d == b"_"):
198 201 break
199 202 pos += 1
200 203 sym = program[s:pos]
201 204 yield (b'symbol', sym, s)
202 205 pos -= 1
203 206 elif c == term:
204 207 yield (b'end', None, pos)
205 208 return
206 209 else:
207 210 raise error.ParseError(_(b"syntax error"), pos)
208 211 pos += 1
209 212 if term:
210 213 raise error.ParseError(_(b"unterminated template expansion"), start)
211 214 yield (b'end', None, pos)
212 215
213 216
214 217 def _parsetemplate(tmpl, start, stop, quote=b''):
215 218 r"""
216 219 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
217 220 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
218 221 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
219 222 ([('string', 'foo'), ('symbol', 'bar')], 9)
220 223 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
221 224 ([('string', 'foo')], 4)
222 225 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
223 226 ([('string', 'foo"'), ('string', 'bar')], 9)
224 227 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
225 228 ([('string', 'foo\\')], 6)
226 229 """
227 230 parsed = []
228 231 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
229 232 if typ == b'string':
230 233 parsed.append((typ, val))
231 234 elif typ == b'template':
232 235 parsed.append(val)
233 236 elif typ == b'end':
234 237 return parsed, pos
235 238 else:
236 239 raise error.ProgrammingError(b'unexpected type: %s' % typ)
237 240 raise error.ProgrammingError(b'unterminated scanning of template')
238 241
239 242
240 243 def scantemplate(tmpl, raw=False):
241 244 r"""Scan (type, start, end) positions of outermost elements in template
242 245
243 246 If raw=True, a backslash is not taken as an escape character just like
244 247 r'' string in Python. Note that this is different from r'' literal in
245 248 template in that no template fragment can appear in r'', e.g. r'{foo}'
246 249 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
247 250 'foo'.
248 251
249 252 >>> list(scantemplate(b'foo{bar}"baz'))
250 253 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
251 254 >>> list(scantemplate(b'outer{"inner"}outer'))
252 255 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
253 256 >>> list(scantemplate(b'foo\\{escaped}'))
254 257 [('string', 0, 5), ('string', 5, 13)]
255 258 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
256 259 [('string', 0, 4), ('template', 4, 13)]
257 260 """
258 261 last = None
259 262 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
260 263 if last:
261 264 yield last + (pos,)
262 265 if typ == b'end':
263 266 return
264 267 else:
265 268 last = (typ, pos)
266 269 raise error.ProgrammingError(b'unterminated scanning of template')
267 270
268 271
269 272 def _scantemplate(tmpl, start, stop, quote=b'', raw=False):
270 273 """Parse template string into chunks of strings and template expressions"""
271 274 sepchars = b'{' + quote
272 275 unescape = [parser.unescapestr, pycompat.identity][raw]
273 276 pos = start
274 277 p = parser.parser(elements)
275 278 try:
276 279 while pos < stop:
277 280 n = min(
278 281 (tmpl.find(c, pos, stop) for c in pycompat.bytestr(sepchars)),
279 282 key=lambda n: (n < 0, n),
280 283 )
281 284 if n < 0:
282 285 yield (b'string', unescape(tmpl[pos:stop]), pos)
283 286 pos = stop
284 287 break
285 288 c = tmpl[n : n + 1]
286 289 bs = 0 # count leading backslashes
287 290 if not raw:
288 291 bs = (n - pos) - len(tmpl[pos:n].rstrip(b'\\'))
289 292 if bs % 2 == 1:
290 293 # escaped (e.g. '\{', '\\\{', but not '\\{')
291 294 yield (b'string', unescape(tmpl[pos : n - 1]) + c, pos)
292 295 pos = n + 1
293 296 continue
294 297 if n > pos:
295 298 yield (b'string', unescape(tmpl[pos:n]), pos)
296 299 if c == quote:
297 300 yield (b'end', None, n + 1)
298 301 return
299 302
300 303 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, b'}'))
301 304 if not tmpl.startswith(b'}', pos):
302 305 raise error.ParseError(_(b"invalid token"), pos)
303 306 yield (b'template', parseres, n)
304 307 pos += 1
305 308
306 309 if quote:
307 310 raise error.ParseError(_(b"unterminated string"), start)
308 311 except error.ParseError as inst:
309 312 _addparseerrorhint(inst, tmpl)
310 313 raise
311 314 yield (b'end', None, pos)
312 315
313 316
314 317 def _addparseerrorhint(inst, tmpl):
315 318 if inst.location is None:
316 319 return
317 320 loc = inst.location
318 321 # Offset the caret location by the number of newlines before the
319 322 # location of the error, since we will replace one-char newlines
320 323 # with the two-char literal r'\n'.
321 324 offset = tmpl[:loc].count(b'\n')
322 325 tmpl = tmpl.replace(b'\n', br'\n')
323 326 # We want the caret to point to the place in the template that
324 327 # failed to parse, but in a hint we get a open paren at the
325 328 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
326 329 # to line up the caret with the location of the error.
327 330 inst.hint = tmpl + b'\n' + b' ' * (loc + 1 + offset) + b'^ ' + _(b'here')
328 331
329 332
330 333 def _unnesttemplatelist(tree):
331 334 """Expand list of templates to node tuple
332 335
333 336 >>> def f(tree):
334 337 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
335 338 >>> f((b'template', []))
336 339 (string '')
337 340 >>> f((b'template', [(b'string', b'foo')]))
338 341 (string 'foo')
339 342 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
340 343 (template
341 344 (string 'foo')
342 345 (symbol 'rev'))
343 346 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
344 347 (template
345 348 (symbol 'rev'))
346 349 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
347 350 (string 'foo')
348 351 """
349 352 if not isinstance(tree, tuple):
350 353 return tree
351 354 op = tree[0]
352 355 if op != b'template':
353 356 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
354 357
355 358 assert len(tree) == 2
356 359 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
357 360 if not xs:
358 361 return (b'string', b'') # empty template ""
359 362 elif len(xs) == 1 and xs[0][0] == b'string':
360 363 return xs[0] # fast path for string with no template fragment "x"
361 364 else:
362 365 return (op,) + xs
363 366
364 367
365 368 def parse(tmpl):
366 369 """Parse template string into tree"""
367 370 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
368 371 assert pos == len(tmpl), b'unquoted template should be consumed'
369 372 return _unnesttemplatelist((b'template', parsed))
370 373
371 374
372 375 def parseexpr(expr):
373 376 """Parse a template expression into tree
374 377
375 378 >>> parseexpr(b'"foo"')
376 379 ('string', 'foo')
377 380 >>> parseexpr(b'foo(bar)')
378 381 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
379 382 >>> from . import error
380 383 >>> from . import pycompat
381 384 >>> try:
382 385 ... parseexpr(b'foo(')
383 386 ... except error.ParseError as e:
384 387 ... pycompat.sysstr(e.message)
385 388 ... e.location
386 389 'not a prefix: end'
387 390 4
388 391 >>> try:
389 392 ... parseexpr(b'"foo" "bar"')
390 393 ... except error.ParseError as e:
391 394 ... pycompat.sysstr(e.message)
392 395 ... e.location
393 396 'invalid token'
394 397 7
395 398 """
396 399 try:
397 400 return _parseexpr(expr)
398 401 except error.ParseError as inst:
399 402 _addparseerrorhint(inst, expr)
400 403 raise
401 404
402 405
403 406 def _parseexpr(expr):
404 407 p = parser.parser(elements)
405 408 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
406 409 if pos != len(expr):
407 410 raise error.ParseError(_(b'invalid token'), pos)
408 411 return _unnesttemplatelist(tree)
409 412
410 413
411 414 def prettyformat(tree):
412 415 return parser.prettyformat(tree, (b'integer', b'string', b'symbol'))
413 416
414 417
415 418 def compileexp(exp, context, curmethods):
416 419 """Compile parsed template tree to (func, data) pair"""
417 420 if not exp:
418 421 raise error.ParseError(_(b"missing argument"))
419 422 t = exp[0]
420 423 return curmethods[t](exp, context)
421 424
422 425
423 426 # template evaluation
424 427
425 428
426 429 def getsymbol(exp):
427 430 if exp[0] == b'symbol':
428 431 return exp[1]
429 432 raise error.ParseError(_(b"expected a symbol, got '%s'") % exp[0])
430 433
431 434
432 435 def getlist(x):
433 436 if not x:
434 437 return []
435 438 if x[0] == b'list':
436 439 return getlist(x[1]) + [x[2]]
437 440 return [x]
438 441
439 442
440 443 def gettemplate(exp, context):
441 444 """Compile given template tree or load named template from map file;
442 445 returns (func, data) pair"""
443 446 if exp[0] in (b'template', b'string'):
444 447 return compileexp(exp, context, methods)
445 448 if exp[0] == b'symbol':
446 449 # unlike runsymbol(), here 'symbol' is always taken as template name
447 450 # even if it exists in mapping. this allows us to override mapping
448 451 # by web templates, e.g. 'changelogtag' is redefined in map file.
449 452 return context._load(exp[1])
450 453 raise error.ParseError(_(b"expected template specifier"))
451 454
452 455
453 456 def _runrecursivesymbol(context, mapping, key):
454 457 raise error.InputError(_(b"recursive reference '%s' in template") % key)
455 458
456 459
457 460 def buildtemplate(exp, context):
458 461 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
459 462 return (templateutil.runtemplate, ctmpl)
460 463
461 464
462 465 def buildfilter(exp, context):
463 466 n = getsymbol(exp[2])
464 467 if n in context._filters:
465 468 filt = context._filters[n]
466 469 arg = compileexp(exp[1], context, methods)
467 470 return (templateutil.runfilter, (arg, filt))
468 471 if n in context._funcs:
469 472 f = context._funcs[n]
470 473 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
471 474 return (f, args)
472 475 raise error.ParseError(_(b"unknown function '%s'") % n)
473 476
474 477
475 478 def buildmap(exp, context):
476 479 darg = compileexp(exp[1], context, methods)
477 480 targ = gettemplate(exp[2], context)
478 481 return (templateutil.runmap, (darg, targ))
479 482
480 483
481 484 def buildmember(exp, context):
482 485 darg = compileexp(exp[1], context, methods)
483 486 memb = getsymbol(exp[2])
484 487 return (templateutil.runmember, (darg, memb))
485 488
486 489
487 490 def buildnegate(exp, context):
488 491 arg = compileexp(exp[1], context, exprmethods)
489 492 return (templateutil.runnegate, arg)
490 493
491 494
492 495 def buildarithmetic(exp, context, func):
493 496 left = compileexp(exp[1], context, exprmethods)
494 497 right = compileexp(exp[2], context, exprmethods)
495 498 return (templateutil.runarithmetic, (func, left, right))
496 499
497 500
498 501 def buildfunc(exp, context):
499 502 n = getsymbol(exp[1])
500 503 if n in context._funcs:
501 504 f = context._funcs[n]
502 505 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
503 506 return (f, args)
504 507 if n in context._filters:
505 508 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
506 509 if len(args) != 1:
507 510 raise error.ParseError(_(b"filter %s expects one argument") % n)
508 511 f = context._filters[n]
509 512 return (templateutil.runfilter, (args[0], f))
510 513 raise error.ParseError(_(b"unknown function '%s'") % n)
511 514
512 515
513 516 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
514 517 """Compile parsed tree of function arguments into list or dict of
515 518 (func, data) pairs
516 519
517 520 >>> context = engine(lambda t: (templateutil.runsymbol, t))
518 521 >>> def fargs(expr, argspec):
519 522 ... x = _parseexpr(expr)
520 523 ... n = getsymbol(x[1])
521 524 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
522 525 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
523 526 ['l', 'k']
524 527 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
525 528 >>> list(args.keys()), list(args[b'opts'].keys())
526 529 (['opts'], ['opts', 'k'])
527 530 """
528 531
529 532 def compiledict(xs):
530 533 return util.sortdict(
531 534 (k, compileexp(x, context, curmethods))
532 535 for k, x in pycompat.iteritems(xs)
533 536 )
534 537
535 538 def compilelist(xs):
536 539 return [compileexp(x, context, curmethods) for x in xs]
537 540
538 541 if not argspec:
539 542 # filter or function with no argspec: return list of positional args
540 543 return compilelist(getlist(exp))
541 544
542 545 # function with argspec: return dict of named args
543 546 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
544 547 treeargs = parser.buildargsdict(
545 548 getlist(exp),
546 549 funcname,
547 550 argspec,
548 551 keyvaluenode=b'keyvalue',
549 552 keynode=b'symbol',
550 553 )
551 554 compargs = util.sortdict()
552 555 if varkey:
553 556 compargs[varkey] = compilelist(treeargs.pop(varkey))
554 557 if optkey:
555 558 compargs[optkey] = compiledict(treeargs.pop(optkey))
556 559 compargs.update(compiledict(treeargs))
557 560 return compargs
558 561
559 562
560 563 def buildkeyvaluepair(exp, content):
561 564 raise error.ParseError(_(b"can't use a key-value pair in this context"))
562 565
563 566
564 567 def buildlist(exp, context):
565 568 raise error.ParseError(
566 569 _(b"can't use a list in this context"),
567 570 hint=_(b'check place of comma and parens'),
568 571 )
569 572
570 573
571 574 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
572 575 exprmethods = {
573 576 b"integer": lambda e, c: (templateutil.runinteger, e[1]),
574 577 b"string": lambda e, c: (templateutil.runstring, e[1]),
575 578 b"symbol": lambda e, c: (templateutil.runsymbol, e[1]),
576 579 b"template": buildtemplate,
577 580 b"group": lambda e, c: compileexp(e[1], c, exprmethods),
578 581 b".": buildmember,
579 582 b"|": buildfilter,
580 583 b"%": buildmap,
581 584 b"func": buildfunc,
582 585 b"keyvalue": buildkeyvaluepair,
583 586 b"list": buildlist,
584 587 b"+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
585 588 b"-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
586 589 b"negate": buildnegate,
587 590 b"*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
588 591 b"/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
589 592 }
590 593
591 594 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
592 595 methods = exprmethods.copy()
593 596 methods[b"integer"] = exprmethods[b"symbol"] # '{1}' as variable
594 597
595 598
596 599 class _aliasrules(parser.basealiasrules):
597 600 """Parsing and expansion rule set of template aliases"""
598 601
599 602 _section = _(b'template alias')
600 603 _parse = staticmethod(_parseexpr)
601 604
602 605 @staticmethod
603 606 def _trygetfunc(tree):
604 607 """Return (name, args) if tree is func(...) or ...|filter; otherwise
605 608 None"""
606 609 if tree[0] == b'func' and tree[1][0] == b'symbol':
607 610 return tree[1][1], getlist(tree[2])
608 611 if tree[0] == b'|' and tree[2][0] == b'symbol':
609 612 return tree[2][1], [tree[1]]
610 613
611 614
612 615 def expandaliases(tree, aliases):
613 616 """Return new tree of aliases are expanded"""
614 617 aliasmap = _aliasrules.buildmap(aliases)
615 618 return _aliasrules.expand(aliasmap, tree)
616 619
617 620
618 621 # template engine
619 622
620 623
621 624 def unquotestring(s):
622 625 '''unwrap quotes if any; otherwise returns unmodified string'''
623 626 if len(s) < 2 or s[0] not in b"'\"" or s[0] != s[-1]:
624 627 return s
625 628 return s[1:-1]
626 629
627 630
628 631 class resourcemapper(object): # pytype: disable=ignored-metaclass
629 632 """Mapper of internal template resources"""
630 633
631 634 __metaclass__ = abc.ABCMeta
632 635
633 636 @abc.abstractmethod
634 637 def availablekeys(self, mapping):
635 638 """Return a set of available resource keys based on the given mapping"""
636 639
637 640 @abc.abstractmethod
638 641 def knownkeys(self):
639 642 """Return a set of supported resource keys"""
640 643
641 644 @abc.abstractmethod
642 645 def lookup(self, mapping, key):
643 646 """Return a resource for the key if available; otherwise None"""
644 647
645 648 @abc.abstractmethod
646 649 def populatemap(self, context, origmapping, newmapping):
647 650 """Return a dict of additional mapping items which should be paired
648 651 with the given new mapping"""
649 652
650 653
651 654 class nullresourcemapper(resourcemapper):
652 655 def availablekeys(self, mapping):
653 656 return set()
654 657
655 658 def knownkeys(self):
656 659 return set()
657 660
658 661 def lookup(self, mapping, key):
659 662 return None
660 663
661 664 def populatemap(self, context, origmapping, newmapping):
662 665 return {}
663 666
664 667
665 668 class engine(object):
666 669 """template expansion engine.
667 670
668 671 template expansion works like this. a map file contains key=value
669 672 pairs. if value is quoted, it is treated as string. otherwise, it
670 673 is treated as name of template file.
671 674
672 675 templater is asked to expand a key in map. it looks up key, and
673 676 looks for strings like this: {foo}. it expands {foo} by looking up
674 677 foo in map, and substituting it. expansion is recursive: it stops
675 678 when there is no more {foo} to replace.
676 679
677 680 expansion also allows formatting and filtering.
678 681
679 682 format uses key to expand each item in list. syntax is
680 683 {key%format}.
681 684
682 685 filter uses function to transform value. syntax is
683 686 {key|filter1|filter2|...}."""
684 687
685 688 def __init__(self, loader, filters=None, defaults=None, resources=None):
686 689 self._loader = loader
687 690 if filters is None:
688 691 filters = {}
689 692 self._filters = filters
690 693 self._funcs = templatefuncs.funcs # make this a parameter if needed
691 694 if defaults is None:
692 695 defaults = {}
693 696 if resources is None:
694 697 resources = nullresourcemapper()
695 698 self._defaults = defaults
696 699 self._resources = resources
697 700 self._cache = {} # key: (func, data)
698 701 self._tmplcache = {} # literal template: (func, data)
699 702
700 703 def overlaymap(self, origmapping, newmapping):
701 704 """Create combined mapping from the original mapping and partial
702 705 mapping to override the original"""
703 706 # do not copy symbols which overrides the defaults depending on
704 707 # new resources, so the defaults will be re-evaluated (issue5612)
705 708 knownres = self._resources.knownkeys()
706 709 newres = self._resources.availablekeys(newmapping)
707 710 mapping = {
708 711 k: v
709 712 for k, v in pycompat.iteritems(origmapping)
710 713 if (
711 714 k in knownres # not a symbol per self.symbol()
712 715 or newres.isdisjoint(self._defaultrequires(k))
713 716 )
714 717 }
715 718 mapping.update(newmapping)
716 719 mapping.update(
717 720 self._resources.populatemap(self, origmapping, newmapping)
718 721 )
719 722 return mapping
720 723
721 724 def _defaultrequires(self, key):
722 725 """Resource keys required by the specified default symbol function"""
723 726 v = self._defaults.get(key)
724 727 if v is None or not callable(v):
725 728 return ()
726 729 return getattr(v, '_requires', ())
727 730
728 731 def symbol(self, mapping, key):
729 732 """Resolve symbol to value or function; None if nothing found"""
730 733 v = None
731 734 if key not in self._resources.knownkeys():
732 735 v = mapping.get(key)
733 736 if v is None:
734 737 v = self._defaults.get(key)
735 738 return v
736 739
737 740 def availableresourcekeys(self, mapping):
738 741 """Return a set of available resource keys based on the given mapping"""
739 742 return self._resources.availablekeys(mapping)
740 743
741 744 def knownresourcekeys(self):
742 745 """Return a set of supported resource keys"""
743 746 return self._resources.knownkeys()
744 747
745 748 def resource(self, mapping, key):
746 749 """Return internal data (e.g. cache) used for keyword/function
747 750 evaluation"""
748 751 v = self._resources.lookup(mapping, key)
749 752 if v is None:
750 753 raise templateutil.ResourceUnavailable(
751 754 _(b'template resource not available: %s') % key
752 755 )
753 756 return v
754 757
755 758 def _load(self, t):
756 759 '''load, parse, and cache a template'''
757 760 if t not in self._cache:
758 761 x = self._loader(t)
759 762 # put poison to cut recursion while compiling 't'
760 763 self._cache[t] = (_runrecursivesymbol, t)
761 764 try:
762 765 self._cache[t] = compileexp(x, self, methods)
763 766 except: # re-raises
764 767 del self._cache[t]
765 768 raise
766 769 return self._cache[t]
767 770
768 771 def _parse(self, tmpl):
769 772 """Parse and cache a literal template"""
770 773 if tmpl not in self._tmplcache:
771 774 x = parse(tmpl)
772 775 self._tmplcache[tmpl] = compileexp(x, self, methods)
773 776 return self._tmplcache[tmpl]
774 777
775 778 def preload(self, t):
776 779 """Load, parse, and cache the specified template if available"""
777 780 try:
778 781 self._load(t)
779 782 return True
780 783 except templateutil.TemplateNotFound:
781 784 return False
782 785
783 786 def process(self, t, mapping):
784 787 """Perform expansion. t is name of map element to expand.
785 788 mapping contains added elements for use during expansion. Is a
786 789 generator."""
787 790 func, data = self._load(t)
788 791 return self._expand(func, data, mapping)
789 792
790 793 def expand(self, tmpl, mapping):
791 794 """Perform expansion over a literal template
792 795
793 796 No user aliases will be expanded since this is supposed to be called
794 797 with an internal template string.
795 798 """
796 799 func, data = self._parse(tmpl)
797 800 return self._expand(func, data, mapping)
798 801
799 802 def _expand(self, func, data, mapping):
800 803 # populate additional items only if they don't exist in the given
801 804 # mapping. this is slightly different from overlaymap() because the
802 805 # initial 'revcache' may contain pre-computed items.
803 806 extramapping = self._resources.populatemap(self, {}, mapping)
804 807 if extramapping:
805 808 extramapping.update(mapping)
806 809 mapping = extramapping
807 810 return templateutil.flatten(self, mapping, func(self, mapping, data))
808 811
809 812
810 813 def stylelist():
811 814 path = templatedir()
812 815 if not path:
813 816 return _(b'no templates found, try `hg debuginstall` for more info')
814 817 dirlist = os.listdir(path)
815 818 stylelist = []
816 819 for file in dirlist:
817 820 split = file.split(b".")
818 821 if split[-1] in (b'orig', b'rej'):
819 822 continue
820 823 if split[0] == b"map-cmdline":
821 824 stylelist.append(split[1])
822 825 return b", ".join(sorted(stylelist))
823 826
824 827
825 828 def _open_mapfile(mapfile):
826 829 if os.path.exists(mapfile):
827 830 return util.posixfile(mapfile, b'rb')
828 831 raise error.Abort(
829 832 _(b"style '%s' not found") % mapfile,
830 833 hint=_(b"available styles: %s") % stylelist(),
831 834 )
832 835
833 836
834 837 def _readmapfile(fp, mapfile):
835 838 """Load template elements from the given map file"""
836 839 if pycompat.iswindows:
837 840 # quick hack to make sure we can process '/' in the code dealing with
838 841 # ressource. Ideally we would make sure we use `/` instead of `ossep`
839 842 # in the templater code, but that seems a bigger and less certain
840 843 # change that we better left for the default branch.
841 844 name_paths = mapfile.split(pycompat.ossep)
842 845 mapfile = b'/'.join(name_paths)
843 846 base = os.path.dirname(mapfile)
844 847 conf = config.config()
845 848
846 849 def include(rel, remap, sections):
847 850 subresource = None
848 851 if base:
849 852 abs = os.path.normpath(os.path.join(base, rel))
850 853 if os.path.isfile(abs):
851 854 subresource = util.posixfile(abs, b'rb')
852 855 if not subresource:
853 856 if pycompat.ossep not in rel:
854 857 abs = rel
855 858 try:
856 859 subresource = resourceutil.open_resource(
857 860 b'mercurial.templates', rel
858 861 )
859 except resourceutil.FileNotFoundError:
862 except FileNotFoundError:
860 863 subresource = None
861 864 else:
862 865 dir = templatedir()
863 866 if dir:
864 867 abs = os.path.normpath(os.path.join(dir, rel))
865 868 if os.path.isfile(abs):
866 869 subresource = util.posixfile(abs, b'rb')
867 870 if subresource:
868 871 data = subresource.read()
869 872 conf.parse(
870 873 abs,
871 874 data,
872 875 sections=sections,
873 876 remap=remap,
874 877 include=include,
875 878 )
876 879
877 880 data = fp.read()
878 881 conf.parse(mapfile, data, remap={b'': b'templates'}, include=include)
879 882
880 883 cache = {}
881 884 tmap = {}
882 885 aliases = []
883 886
884 887 val = conf.get(b'templates', b'__base__')
885 888 if val and val[0] not in b"'\"":
886 889 # treat as a pointer to a base class for this style
887 890 path = os.path.normpath(os.path.join(base, val))
888 891
889 892 # fallback check in template paths
890 893 if not os.path.exists(path):
891 894 dir = templatedir()
892 895 if dir is not None:
893 896 p2 = os.path.normpath(os.path.join(dir, val))
894 897 if os.path.isfile(p2):
895 898 path = p2
896 899 else:
897 900 p3 = os.path.normpath(os.path.join(p2, b"map"))
898 901 if os.path.isfile(p3):
899 902 path = p3
900 903
901 904 fp = _open_mapfile(path)
902 905 cache, tmap, aliases = _readmapfile(fp, path)
903 906
904 907 for key, val in conf.items(b'templates'):
905 908 if not val:
906 909 raise error.ParseError(
907 910 _(b'missing value'), conf.source(b'templates', key)
908 911 )
909 912 if val[0] in b"'\"":
910 913 if val[0] != val[-1]:
911 914 raise error.ParseError(
912 915 _(b'unmatched quotes'), conf.source(b'templates', key)
913 916 )
914 917 cache[key] = unquotestring(val)
915 918 elif key != b'__base__':
916 919 tmap[key] = os.path.join(base, val)
917 920 aliases.extend(conf.items(b'templatealias'))
918 921 return cache, tmap, aliases
919 922
920 923
921 924 class loader(object):
922 925 """Load template fragments optionally from a map file"""
923 926
924 927 def __init__(self, cache, aliases):
925 928 if cache is None:
926 929 cache = {}
927 930 self.cache = cache.copy()
928 931 self._map = {}
929 932 self._aliasmap = _aliasrules.buildmap(aliases)
930 933
931 934 def __contains__(self, key):
932 935 return key in self.cache or key in self._map
933 936
934 937 def load(self, t):
935 938 """Get parsed tree for the given template name. Use a local cache."""
936 939 if t not in self.cache:
937 940 try:
938 941 mapfile, fp = open_template(self._map[t])
939 942 self.cache[t] = fp.read()
940 943 except KeyError as inst:
941 944 raise templateutil.TemplateNotFound(
942 945 _(b'"%s" not in template map') % inst.args[0]
943 946 )
944 947 except IOError as inst:
945 948 reason = _(b'template file %s: %s') % (
946 949 self._map[t],
947 950 stringutil.forcebytestr(inst.args[1]),
948 951 )
949 952 raise IOError(inst.args[0], encoding.strfromlocal(reason))
950 953 return self._parse(self.cache[t])
951 954
952 955 def _parse(self, tmpl):
953 956 x = parse(tmpl)
954 957 if self._aliasmap:
955 958 x = _aliasrules.expand(self._aliasmap, x)
956 959 return x
957 960
958 961 def _findsymbolsused(self, tree, syms):
959 962 if not tree:
960 963 return
961 964 op = tree[0]
962 965 if op == b'symbol':
963 966 s = tree[1]
964 967 if s in syms[0]:
965 968 return # avoid recursion: s -> cache[s] -> s
966 969 syms[0].add(s)
967 970 if s in self.cache or s in self._map:
968 971 # s may be a reference for named template
969 972 self._findsymbolsused(self.load(s), syms)
970 973 return
971 974 if op in {b'integer', b'string'}:
972 975 return
973 976 # '{arg|func}' == '{func(arg)}'
974 977 if op == b'|':
975 978 syms[1].add(getsymbol(tree[2]))
976 979 self._findsymbolsused(tree[1], syms)
977 980 return
978 981 if op == b'func':
979 982 syms[1].add(getsymbol(tree[1]))
980 983 self._findsymbolsused(tree[2], syms)
981 984 return
982 985 for x in tree[1:]:
983 986 self._findsymbolsused(x, syms)
984 987
985 988 def symbolsused(self, t):
986 989 """Look up (keywords, filters/functions) referenced from the name
987 990 template 't'
988 991
989 992 This may load additional templates from the map file.
990 993 """
991 994 syms = (set(), set())
992 995 self._findsymbolsused(self.load(t), syms)
993 996 return syms
994 997
995 998
996 999 class templater(object):
997 1000 def __init__(
998 1001 self,
999 1002 filters=None,
1000 1003 defaults=None,
1001 1004 resources=None,
1002 1005 cache=None,
1003 1006 aliases=(),
1004 1007 minchunk=1024,
1005 1008 maxchunk=65536,
1006 1009 ):
1007 1010 """Create template engine optionally with preloaded template fragments
1008 1011
1009 1012 - ``filters``: a dict of functions to transform a value into another.
1010 1013 - ``defaults``: a dict of symbol values/functions; may be overridden
1011 1014 by a ``mapping`` dict.
1012 1015 - ``resources``: a resourcemapper object to look up internal data
1013 1016 (e.g. cache), inaccessible from user template.
1014 1017 - ``cache``: a dict of preloaded template fragments.
1015 1018 - ``aliases``: a list of alias (name, replacement) pairs.
1016 1019
1017 1020 self.cache may be updated later to register additional template
1018 1021 fragments.
1019 1022 """
1020 1023 allfilters = templatefilters.filters.copy()
1021 1024 if filters:
1022 1025 allfilters.update(filters)
1023 1026 self._loader = loader(cache, aliases)
1024 1027 self._proc = engine(self._loader.load, allfilters, defaults, resources)
1025 1028 self._minchunk, self._maxchunk = minchunk, maxchunk
1026 1029
1027 1030 @classmethod
1028 1031 def frommapfile(
1029 1032 cls,
1030 1033 mapfile,
1031 1034 fp=None,
1032 1035 filters=None,
1033 1036 defaults=None,
1034 1037 resources=None,
1035 1038 cache=None,
1036 1039 minchunk=1024,
1037 1040 maxchunk=65536,
1038 1041 ):
1039 1042 """Create templater from the specified map file"""
1040 1043 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1041 1044 if not fp:
1042 1045 fp = _open_mapfile(mapfile)
1043 1046 cache, tmap, aliases = _readmapfile(fp, mapfile)
1044 1047 t._loader.cache.update(cache)
1045 1048 t._loader._map = tmap
1046 1049 t._loader._aliasmap = _aliasrules.buildmap(aliases)
1047 1050 return t
1048 1051
1049 1052 def __contains__(self, key):
1050 1053 return key in self._loader
1051 1054
1052 1055 @property
1053 1056 def cache(self):
1054 1057 return self._loader.cache
1055 1058
1056 1059 # for highlight extension to insert one-time 'colorize' filter
1057 1060 @property
1058 1061 def _filters(self):
1059 1062 return self._proc._filters
1060 1063
1061 1064 @property
1062 1065 def defaults(self):
1063 1066 return self._proc._defaults
1064 1067
1065 1068 def load(self, t):
1066 1069 """Get parsed tree for the given template name. Use a local cache."""
1067 1070 return self._loader.load(t)
1068 1071
1069 1072 def symbolsuseddefault(self):
1070 1073 """Look up (keywords, filters/functions) referenced from the default
1071 1074 unnamed template
1072 1075
1073 1076 This may load additional templates from the map file.
1074 1077 """
1075 1078 return self.symbolsused(b'')
1076 1079
1077 1080 def symbolsused(self, t):
1078 1081 """Look up (keywords, filters/functions) referenced from the name
1079 1082 template 't'
1080 1083
1081 1084 This may load additional templates from the map file.
1082 1085 """
1083 1086 return self._loader.symbolsused(t)
1084 1087
1085 1088 def renderdefault(self, mapping):
1086 1089 """Render the default unnamed template and return result as string"""
1087 1090 return self.render(b'', mapping)
1088 1091
1089 1092 def render(self, t, mapping):
1090 1093 """Render the specified named template and return result as string"""
1091 1094 return b''.join(self.generate(t, mapping))
1092 1095
1093 1096 def generate(self, t, mapping):
1094 1097 """Return a generator that renders the specified named template and
1095 1098 yields chunks"""
1096 1099 stream = self._proc.process(t, mapping)
1097 1100 if self._minchunk:
1098 1101 stream = util.increasingchunks(
1099 1102 stream, min=self._minchunk, max=self._maxchunk
1100 1103 )
1101 1104 return stream
1102 1105
1103 1106
1104 1107 def templatedir():
1105 1108 '''return the directory used for template files, or None.'''
1106 1109 path = os.path.normpath(os.path.join(resourceutil.datapath, b'templates'))
1107 1110 return path if os.path.isdir(path) else None
1108 1111
1109 1112
1110 1113 def open_template(name, templatepath=None):
1111 1114 """returns a file-like object for the given template, and its full path
1112 1115
1113 1116 If the name is a relative path and we're in a frozen binary, the template
1114 1117 will be read from the mercurial.templates package instead. The returned path
1115 1118 will then be the relative path.
1116 1119 """
1117 1120 # Does the name point directly to a map file?
1118 1121 if os.path.isfile(name) or os.path.isabs(name):
1119 1122 return name, open(name, mode='rb')
1120 1123
1121 1124 # Does the name point to a template in the provided templatepath, or
1122 1125 # in mercurial/templates/ if no path was provided?
1123 1126 if templatepath is None:
1124 1127 templatepath = templatedir()
1125 1128 if templatepath is not None:
1126 1129 f = os.path.join(templatepath, name)
1127 1130 return f, open(f, mode='rb')
1128 1131
1129 1132 # Otherwise try to read it using the resources API
1130 1133 if pycompat.iswindows:
1131 1134 # quick hack to make sure we can process '/' in the code dealing with
1132 1135 # ressource. Ideally we would make sure we use `/` instead of `ossep`
1133 1136 # in the templater code, but that seems a bigger and less certain
1134 1137 # change that we better left for the default branch.
1135 1138 name_paths = name.split(pycompat.ossep)
1136 1139 name = b'/'.join(name_paths)
1137 1140 name_parts = name.split(b'/')
1138 1141 package_name = b'.'.join([b'mercurial', b'templates'] + name_parts[:-1])
1139 1142 return (
1140 1143 name,
1141 1144 resourceutil.open_resource(package_name, name_parts[-1]),
1142 1145 )
1143 1146
1144 1147
1145 1148 def try_open_template(name, templatepath=None):
1146 1149 try:
1147 1150 return open_template(name, templatepath)
1148 1151 except (EnvironmentError, ImportError):
1149 1152 return None, None
General Comments 0
You need to be logged in to leave comments. Login now