1""" 2ustache module. 3 4This module alone implements the entire ustache library, including its 5minimal command line interface. 6 7The main functions are considered to be :func:`cli`, :func:`render` and 8:func:`stream`, and they expose different approaches to template rendering: 9command line interface, buffered and streaming rendering API, respectively. 10 11Other functionality will be considered **advanced**, exposing some 12implementation-specific complexity and potentially non-standard behavior 13which could reduce compatibility with other mustache implentations and 14future major ustache versions. 15 16""" 17""" 18ustache, Mustache for Python 19============================ 20 21See `README.md`_, `project documentation`_ and `project repository`_. 22 23.. _README.md: https://ustache.readthedocs.io/en/latest/README.html 24.. _project documentation: https://ustache.readthedocs.io 25.. _project repository: https://gitlab.com/ergoithz/ustache 26 27 28License 29------- 30 31Copyright (c) 2021, Felipe A Hernandez. 32 33MIT License (see `LICENSE`_). 34 35.. _LICENSE: https://gitlab.com/ergoithz/ustache/-/blob/master/LICENSE 36 37""" 38 39import codecs 40import collections 41import collections.abc 42import functools 43import sys 44import types 45import typing 46 47try: 48 import xxhash # type: ignore 49 _cache_hash = next(filter(callable, ( 50 getattr(xxhash, name, None) 51 for name in ( 52 'xxh3_64_intdigest', 53 'xxh64_intdigest', 54 ) 55 ))) 56 """ 57 Generate template hash using the fastest algorithm available: 58 59 1. :py:func:`xxhash.xxh3_64_intdigest` from ``python-xxhash>=2.0.0``. 60 2. :py:func:`xxhash.xxh64_intdigest` from ``python-xxhash>=1.2.0``. 61 62 """ 63except (ImportError, StopIteration): 64 def _cache_hash(template: bytes) -> bytes: 65 """Get template itself as hash fallback.""" 66 return template 67 68__author__ = 'Felipe A Hernandez' 69__email__ = 'ergoithz@gmail.com' 70__license__ = 'MIT' 71__version__ = '0.1.5' 72__all__ = ( 73 # api 74 'tokenize', 75 'stream', 76 'render', 77 'cli', 78 # exceptions 79 'TokenException', 80 'ClosingTokenException', 81 'UnclosedTokenException', 82 'DelimiterTokenException', 83 # defaults 84 'default_resolver', 85 'default_getter', 86 'default_stringify', 87 'default_escape', 88 'default_lambda_render', 89 'default_tags', 90 'default_cache', 91 'default_virtuals', 92 # types 93 'TString', 94 'PartialResolver', 95 'PropertyGetter', 96 'StringifyFunction', 97 'EscapeFunction', 98 'LambdaRenderFunctionFactory', 99 'LambdaRenderFunctionConstructor', 100 'CompiledTemplate', 101 'CompiledToken', 102 'CompiledTemplateCache', 103 'TagsTuple', 104) 105 106if sys.version_info > (3, 9): 107 _abc = collections.abc 108 _TagsTuple = tuple[typing.AnyStr, typing.AnyStr] 109 _TagsByteTuple = tuple[bytes, bytes] 110 _CompiledToken = tuple[bool, bool, bool, slice, slice, int] 111 _CompiledTemplate = tuple[_CompiledToken, ...] 112 _CacheKey = tuple[typing.Union[bytes, int], bytes, bytes, bool] 113 _TokenizeStack = list[tuple[slice, int, int, int]] 114 _TokenizeRecording = list[_CompiledToken] 115 _SiblingIterator = typing.Optional[_abc.Iterator] 116 _ProcessStack = list[tuple[_SiblingIterator, bool, bool]] 117else: 118 _abc = typing 119 _TagsTuple = typing.Tuple[typing.AnyStr, typing.AnyStr] 120 _TagsByteTuple = typing.Tuple[bytes, bytes] 121 _CompiledToken = typing.Tuple[bool, bool, bool, slice, slice, int] 122 _CompiledTemplate = typing.Tuple[_CompiledToken, ...] 123 _CacheKey = typing.Tuple[typing.Union[bytes, int], bytes, bytes, bool] 124 _TokenizeStack = typing.List[typing.Tuple[slice, int, int, int]] 125 _TokenizeRecording = typing.List[_CompiledToken] 126 _SiblingIterator = typing.Optional[_abc.Iterator] 127 _ProcessStack = typing.List[typing.Tuple[_SiblingIterator, bool, bool]] 128 129T = typing.TypeVar('T') 130"""Generic.""" 131 132D = typing.TypeVar('D') 133"""Generic.""" 134 135TString = typing.TypeVar('TString', str, bytes) 136"""String/bytes generic.""" 137 138PartialResolver = _abc.Callable[[typing.AnyStr], typing.AnyStr] 139"""Template partial tag resolver function interface.""" 140 141PropertyGetter = _abc.Callable[ 142 [typing.Any, _abc.Sequence[typing.Any], typing.AnyStr, typing.Any], 143 typing.Any, 144] 145"""Template property getter function interface.""" 146 147StringifyFunction = _abc.Callable[[bytes, bool], bytes] 148"""Template variable general stringification function interface.""" 149 150EscapeFunction = _abc.Callable[[bytes], bytes] 151"""Template variable value escape function interface.""" 152 153LambdaRenderFunctionConstructor = _abc.Callable[ 154 ..., 155 _abc.Callable[..., typing.AnyStr], 156] 157"""Lambda render function constructor interface.""" 158 159LambdaRenderFunctionFactory = LambdaRenderFunctionConstructor 160""" 161.. deprecated:: 0.1.3 162 Use :attr:`LambdaRenderFunctionConstructor` instead. 163 164""" 165 166VirtualPropertyFunction = _abc.Callable[[typing.Any], typing.Any] 167"""Virtual property implementation callable interface.""" 168 169VirtualPropertyMapping = _abc.Mapping[str, VirtualPropertyFunction] 170"""Virtual property mapping interface.""" 171 172TagsTuple = _TagsTuple 173"""Mustache tag tuple interface.""" 174 175CompiledToken = _CompiledToken 176""" 177Compiled template token. 178 179Tokens are tuples containing a renderer decission path, key, content and flags. 180 181``type: bool`` 182 Decission for rendering path node `a`. 183 184``type: bool`` 185 Decission for rendering path node `b`. 186 187``type: bool`` 188 Decission for rendering path node `c` 189 190``key: Optional[slice]`` 191 Template slice for token scope key, if any. 192 193``content: Optional[slice]`` 194 Template slice for token content data, if any. 195 196``flags: int`` 197 Token flags. 198 199 - Unused: ``-1`` (default) 200 - Variable flags: 201 - ``0`` - escaped 202 - ``1`` - unescaped 203 - Block start flags: 204 - ``0`` - falsy 205 - ``1`` - truthy 206 - Block end value: block content index. 207 208""" 209 210CompiledTemplate = _CompiledTemplate 211""" 212Compiled template interface. 213 214.. seealso:: 215 216 :py:attr:`ustache.CompiledToken` 217 Item type. 218 219 :py:attr:`ustache.CompiledTemplateCache` 220 Interface exposing this type. 221 222""" 223 224CompiledTemplateCache = _abc.MutableMapping[_CacheKey, CompiledTemplate] 225""" 226Cache mapping interface. 227 228.. seealso:: 229 230 :py:attr:`ustache.CompiledTemplateCache` 231 Item type. 232 233""" 234 235 236class LRUCache(collections.OrderedDict, typing.Generic[T]): 237 """Capped mapping discarding least recently used elements.""" 238 239 def __init__(self, maxsize: int, *args, **kwargs) -> None: 240 """ 241 Initialize. 242 243 :param maxsize: maximum number of elements will be kept 244 245 Any parameter excess will be passed straight to dict constructor. 246 247 """ 248 self.maxsize = maxsize 249 super().__init__(*args, **kwargs) 250 251 def get(self, key: _abc.Hashable, default: D = None) -> typing.Union[T, D]: 252 """ 253 Get value for given key or default if not present. 254 255 :param key: hashable 256 :param default: value will be returned if key is not present 257 :returns: value if key is present, default if not 258 259 """ 260 try: 261 return self[key] 262 except KeyError: 263 return default # type: ignore 264 265 def __getitem__(self, key: _abc.Hashable) -> T: 266 """ 267 Get value for given key. 268 269 :param key: hashable 270 :returns: value if key is present 271 :raises KeyError: if key is not present 272 273 """ 274 self.move_to_end(key) 275 return super().__getitem__(key) 276 277 def __setitem__(self, key: _abc.Hashable, value: T) -> None: 278 """ 279 Set value for given key. 280 281 :param key: hashable will be used to retrieve values later on 282 :param value: value for given key 283 284 """ 285 super().__setitem__(key, value) 286 try: 287 self.move_to_end(key) 288 while len(self) > self.maxsize: 289 self.popitem(last=False) 290 except KeyError: # race condition 291 pass 292 293 294default_tags = (b'{{', b'}}') 295"""Tuple of default mustache tags (in bytes).""" 296 297default_cache: CompiledTemplateCache = LRUCache(1024) 298""" 299Default template cache mapping, keeping the 1024 most recently used 300compiled templates (LRU expiration). 301 302If `xxhash`_ is available, template data won't be included in cache. 303 304.. _xxhash: https://pypi.org/project/xxhash/ 305 306""" 307 308 309def virtual_length(ref: typing.Any) -> int: 310 """ 311 Resolve virtual length property. 312 313 :param ref: any non-mapping object implementing `__len__` 314 :returns: number of items 315 :raises TypeError: if ref is mapping or has no `__len__` method 316 317 """ 318 if isinstance(ref, collections.abc.Mapping): 319 raise TypeError 320 return len(ref) 321 322 323default_virtuals: VirtualPropertyMapping = types.MappingProxyType({ 324 'length': virtual_length, 325}) 326""" 327Immutable mapping with default virtual properties. 328 329The following virtual properties are implemented: 330 331- **length**, for non-mapping sized objects, returning ``len(ref)``. 332 333""" 334 335TOKEN_TYPES = [(True, False, True, False)] * 0x100 336TOKEN_TYPES[0x21] = False, False, False, True # '!' 337TOKEN_TYPES[0x23] = False, True, True, False # '#' 338TOKEN_TYPES[0x26] = True, True, True, True # '&' 339TOKEN_TYPES[0x2F] = False, True, False, False # '/' 340TOKEN_TYPES[0x3D] = False, False, False, False # '=' 341TOKEN_TYPES[0x3E] = False, False, True, False # '>' 342TOKEN_TYPES[0x5E] = False, True, True, True # '^' 343TOKEN_TYPES[0x7B] = True, True, False, True # '{' 344"""ASCII-indexed tokenizer decission matrix.""" 345 346FALSY_PRIMITIVES = None, False, 0, float('nan'), float('-nan') 347EMPTY = slice(0) 348EVERYTHING = slice(None) 349 350 351class TokenException(SyntaxError): 352 """Invalid token found during tokenization.""" 353 354 message = 'Invalid tag {tag} at line {row} column {column}' 355 356 @classmethod 357 def from_template( 358 cls, 359 template: bytes, 360 start: int, 361 end: int, 362 ) -> 'TokenException': 363 """ 364 Create exception instance from parsing data. 365 366 :param template: template bytes 367 :param start: character position where the offending tag starts at 368 :param end: character position where the offending tag ends at 369 :returns: exception instance 370 371 """ 372 tag = template[start:end].decode() 373 row = 1 + template[:start].count(b'\n') 374 column = 1 + start - max(0, template.rfind(b'\n', 0, start)) 375 return cls(cls.message.format(tag=tag, row=row, column=column)) 376 377 378class ClosingTokenException(TokenException): 379 """Non-matching closing token found during tokenization.""" 380 381 message = 'Non-matching tag {tag} at line {row} column {column}' 382 383 384class UnclosedTokenException(ClosingTokenException): 385 """Unclosed token found during tokenization.""" 386 387 message = 'Unclosed tag {tag} at line {row} column {column}' 388 389 390class DelimiterTokenException(TokenException): 391 """ 392 Invalid delimiters token found during tokenization. 393 394 .. versionadded:: 0.1.1 395 396 """ 397 398 message = 'Invalid delimiters {tag} at line {row} column {column}' 399 400 401def default_stringify( 402 data: typing.Any, 403 text: bool = False, 404) -> bytes: 405 """ 406 Convert arbitrary data to bytes. 407 408 :param data: value will be serialized 409 :param text: whether running in text mode or not (bytes mode) 410 :returns: template bytes 411 412 """ 413 return ( 414 data 415 if isinstance(data, bytes) and not text else 416 data.encode() 417 if isinstance(data, str) else 418 f'{data}'.encode() 419 ) 420 421 422def replay( 423 recording: typing.Sequence[CompiledToken], 424 start: int = 0, 425) -> _abc.Generator[CompiledToken, int, None]: 426 """ 427 Yield template tokenization from cached data. 428 429 This generator accepts sending back a token index, which will result on 430 rewinding it back and repeat everything from there. 431 432 :param recording: token list 433 :param start: starting index 434 :returns: token tuple generator 435 436 """ 437 size = len(recording) 438 while True: 439 for item in range(start, size): 440 start = yield recording[item] 441 if start is not None: 442 break 443 else: 444 break 445 446 447def default_escape(data: bytes) -> bytes: 448 """ 449 Convert bytes conflicting with HTML to their escape sequences. 450 451 :param data: bytes containing text 452 :returns: escaped text bytes 453 454 """ 455 return ( 456 data 457 .replace(b'&', b'&') 458 .replace(b'<', b'<') 459 .replace(b'>', b'>') 460 .replace(b'"', b'"') 461 .replace(b'\'', b'`') 462 .replace(b'`', b'=') 463 ) 464 465 466def default_resolver(name: typing.AnyStr) -> bytes: 467 """ 468 Mustache partial resolver function (stub). 469 470 :param name: partial template name 471 :returns: empty bytes 472 473 """ 474 return b'' 475 476 477def default_getter( 478 scope: typing.Any, 479 scopes: _abc.Sequence[typing.Any], 480 key: typing.AnyStr, 481 default: typing.Any = None, 482 *, 483 virtuals: VirtualPropertyMapping = default_virtuals, 484) -> typing.Any: 485 """ 486 Extract property value from scope hierarchy. 487 488 :param scope: uppermost scope (corresponding to key ``'.'``) 489 :param scopes: parent scope sequence 490 :param key: property key 491 :param default: value will be used as default when missing 492 :param virtuals: mapping of virtual property callables 493 :return: value from scope or default 494 495 .. versionadded:: 0.1.3 496 *virtuals* parameter. 497 498 Both :class:`AttributeError` and :class:`TypeError` exceptions 499 raised by virtual property implementations will be handled as if 500 that property doesn't exist, which can be useful to filter out 501 incompatible types. 502 503 """ 504 if key in (b'.', '.'): 505 return scope 506 507 binary_mode = not isinstance(key, str) 508 components = key.split(b'.' if binary_mode else '.') # type: ignore 509 for ref in (*scopes, scope)[::-1]: 510 for name in components: 511 if binary_mode: 512 try: 513 ref = ref[name] 514 continue 515 except (KeyError, TypeError, AttributeError): 516 pass 517 try: 518 name = name.decode() # type: ignore 519 except UnicodeDecodeError: 520 break 521 try: 522 ref = ref[name] 523 continue 524 except (KeyError, TypeError, AttributeError): 525 pass 526 try: 527 ref = ( 528 ref[int(name)] 529 if name.isdigit() else 530 getattr(ref, name) # type: ignore 531 ) 532 continue 533 except (KeyError, TypeError, AttributeError, IndexError): 534 pass 535 try: 536 ref = virtuals[name](ref) # type: ignore 537 continue 538 except (KeyError, TypeError, AttributeError): 539 pass 540 break 541 else: 542 return ref 543 return default 544 545 546def default_lambda_render( 547 scope: typing.Any, 548 **kwargs, 549) -> _abc.Callable[[TString], TString]: 550 r""" 551 Generate a template-only render function with fixed parameters. 552 553 :param scope: current scope 554 :param \**kwargs: parameters forwarded to :func:`render` 555 :returns: template render function 556 557 """ 558 559 def lambda_render(template: TString) -> TString: 560 """ 561 Render given template to string/bytes. 562 563 :param template: template text 564 :returns: rendered string or bytes (depending on template type) 565 566 """ 567 return render(template, scope, **kwargs) 568 569 return lambda_render 570 571 572def slicestrip(template: bytes, start: int, end: int) -> slice: 573 """ 574 Strip slice from whitespace on bytes. 575 576 :param template: bytes where whitespace should be stripped 577 :param start: substring slice start 578 :param end: substring slice end 579 :returns: resulting stripped slice 580 581 """ 582 c = template[start:end] 583 return slice(end - len(c.lstrip()), start + len(c.rstrip())) 584 585 586def tokenize( 587 template: bytes, 588 *, 589 tags: TagsTuple = default_tags, 590 comments: bool = False, 591 cache: CompiledTemplateCache = default_cache, 592) -> _abc.Generator[CompiledToken, int, None]: 593 """ 594 Generate token tuples from mustache template. 595 596 This generator accepts sending back a token index, which will result on 597 rewinding it back and repeat everything from there. 598 599 :param template: template as utf-8 encoded bytes 600 :param tags: mustache tag tuple (open, close) 601 :param comments: whether yield comment tokens or not (ignore comments) 602 :param cache: mutable mapping for compiled template cache 603 :return: token tuple generator (type, name slice, content slice, option) 604 605 :raises UnclosedTokenException: if token is left unclosed 606 :raises ClosingTokenException: if block closing token does not match 607 :raises DelimiterTokenException: if delimiter token syntax is invalid 608 609 """ 610 tokenization_key = _cache_hash(template), *tags, comments # type: ignore 611 612 cached = cache.get(tokenization_key) 613 if cached: # recordings must contain at least one token 614 yield from replay(cached) 615 return 616 617 template_find = template.find 618 619 stack: _TokenizeStack = [] 620 stack_append = stack.append 621 stack_pop = stack.pop 622 scope_label = EVERYTHING 623 scope_head = 0 624 scope_start = 0 625 scope_index = 0 626 627 empty = EMPTY 628 start_tag, end_tag = tags 629 end_literal = b'}' + end_tag 630 end_switch = b'=' + end_tag 631 start_len = len(start_tag) 632 end_len = len(end_tag) 633 634 token: CompiledToken 635 token_types = TOKEN_TYPES 636 t = slice 637 s = functools.partial(slicestrip, template) 638 recording: _TokenizeRecording = [] 639 record = recording.append 640 641 text_start, text_end = 0, template_find(start_tag) 642 while text_end != -1: 643 if text_start < text_end: # text 644 token = False, True, False, empty, t(text_start, text_end), -1 645 record(token) 646 yield token 647 648 tag_start = text_end + start_len 649 try: 650 a, b, c, d = token_types[template[tag_start]] 651 except IndexError: 652 raise UnclosedTokenException.from_template( 653 template=template, 654 start=text_end, 655 end=tag_start, 656 ) from None 657 658 if a: # variables 659 tag_start += b 660 661 if c: # variable 662 tag_end = template_find(end_tag, tag_start) 663 text_start = tag_end + end_len 664 665 else: # triple-keyed variable 666 tag_end = template_find(end_literal, tag_start) 667 text_start = tag_end + end_len + 1 668 669 token = False, True, True, s(tag_start, tag_end), empty, d 670 671 elif b: # block 672 tag_start += 1 673 tag_end = template_find(end_tag, tag_start) 674 text_start = tag_end + end_len 675 676 if c: # open 677 stack_append((scope_label, text_end, scope_start, scope_index)) 678 scope_label = s(tag_start, tag_end) 679 scope_head = text_end 680 scope_start = text_start 681 scope_index = len(recording) 682 token = True, True, False, scope_label, empty, d 683 684 elif template[scope_label] != template[tag_start:tag_end].strip(): 685 raise ClosingTokenException.from_template( 686 template=template, 687 start=text_end, 688 end=text_start, 689 ) 690 691 else: # close 692 token = ( 693 True, True, True, 694 scope_label, s(scope_start, text_end), scope_index, 695 ) 696 scope_label, scope_head, scope_start, scope_index = stack_pop() 697 698 elif c: # partial 699 tag_start += 1 700 tag_end = template_find(end_tag, tag_start) 701 text_start = tag_end + end_len 702 token = True, False, True, s(tag_start, tag_end), empty, -1 703 704 elif d: # comment 705 tag_start += 1 706 tag_end = template_find(end_tag, tag_start) 707 text_start = tag_end + end_len 708 709 if not comments: 710 text_end = template_find(start_tag, text_start) 711 continue 712 713 token = True, False, False, empty, s(tag_start, tag_end), -1 714 715 else: # tags 716 tag_start += 1 717 tag_end = template_find(end_switch, tag_start) 718 text_start = tag_end + end_len + 1 719 720 try: 721 start_tag, end_tag = template[tag_start:tag_end].split(b' ') 722 if not (start_tag and end_tag): 723 raise ValueError 724 725 except ValueError: 726 raise DelimiterTokenException.from_template( 727 template=template, 728 start=text_end, 729 end=text_start, 730 ) from None 731 732 end_literal = b'}' + end_tag 733 end_switch = b'=' + end_tag 734 start_len = len(start_tag) 735 end_len = len(end_tag) 736 start_end = tag_start + start_len 737 end_start = tag_end - end_len 738 token = ( 739 False, False, True, 740 s(tag_start, start_end), s(end_start, tag_end), -1, 741 ) 742 743 if tag_end < 0: 744 raise UnclosedTokenException.from_template( 745 template=template, 746 start=tag_start, 747 end=tag_end, 748 ) 749 750 record(token) 751 rewind = yield token 752 if rewind is not None: 753 yield from replay(recording, rewind) 754 755 text_end = template_find(start_tag, text_start) 756 757 if stack: 758 raise UnclosedTokenException.from_template( 759 template=template, 760 start=scope_head, 761 end=scope_start, 762 ) 763 764 # end 765 token = False, False, False, empty, t(text_start, None), -1 766 cache[tokenization_key] = (*recording, token) 767 yield token 768 769 770def process( 771 template: TString, 772 scope: typing.Any, 773 *, 774 scopes: _abc.Iterable[typing.Any] = (), 775 resolver: PartialResolver = default_resolver, 776 getter: PropertyGetter = default_getter, 777 stringify: StringifyFunction = default_stringify, 778 escape: EscapeFunction = default_escape, 779 lambda_render: LambdaRenderFunctionConstructor = default_lambda_render, 780 tags: TagsTuple = default_tags, 781 cache: CompiledTemplateCache = default_cache, 782) -> _abc.Generator[bytes, int, None]: 783 """ 784 Generate rendered mustache template byte chunks. 785 786 :param template: mustache template string 787 :param scope: root object used as root mustache scope 788 :param scopes: iterable of parent scopes 789 :param resolver: callable will be used to resolve partials (bytes) 790 :param getter: callable will be used to pick variables from scope 791 :param stringify: callable will be used to render python types (bytes) 792 :param escape: callable will be used to escape template (bytes) 793 :param lambda_render: lambda render function constructor 794 :param tags: mustache tag tuple (open, close) 795 :param cache: mutable mapping for compiled template cache 796 :return: byte chunk generator 797 798 :raises UnclosedTokenException: if token is left unclosed 799 :raises ClosingTokenException: if block closing token does not match 800 :raises DelimiterTokenException: if delimiter token syntax is invalid 801 802 """ 803 text_mode = isinstance(template, str) 804 805 # encoding 806 data: bytes = ( 807 template.encode() # type: ignore 808 if text_mode else 809 template 810 ) 811 current_tags: _TagsByteTuple = tuple( # type: ignore 812 tag.encode() if isinstance(tag, str) else tag 813 for tag in tags[:2] 814 ) 815 816 # current context 817 siblings: _SiblingIterator = None 818 callback = False 819 silent = False 820 821 # context stack 822 stack: _ProcessStack = [] 823 stack_append = stack.append 824 stack_pop = stack.pop 825 826 # scope stack 827 scopes = list(scopes) 828 scopes_append = scopes.append 829 scopes_pop = scopes.pop 830 831 # locals 832 read = data.__getitem__ 833 decode = ( 834 (lambda x: data[x].decode()) # type: ignore 835 if text_mode else 836 read 837 ) 838 missing = object() 839 falsy_primitives = FALSY_PRIMITIVES 840 TIterable = collections.abc.Iterable 841 TNonLooping = ( 842 str, 843 bytes, 844 collections.abc.Mapping, 845 ) 846 TSequence = collections.abc.Sequence 847 848 tokens = tokenize(data, tags=current_tags, cache=cache) 849 rewind = tokens.send 850 for a, b, c, token_name, token_content, token_option in tokens: 851 if silent: 852 if a and b: 853 if c: # closing/loop 854 closing_scope = scope 855 closing_callback = callback 856 857 scope = scopes_pop() 858 siblings, callback, silent = stack_pop() 859 860 if closing_callback and not silent: 861 yield stringify( 862 closing_scope( 863 decode(token_content), 864 lambda_render( 865 scope=scope, 866 scopes=scopes, 867 resolver=resolver, 868 escape=escape, 869 tags=current_tags, 870 cache=cache, 871 ), 872 ), 873 text_mode, 874 ) 875 876 else: # block 877 scopes_append(scope) 878 stack_append((siblings, callback, silent)) 879 elif a: 880 if b: 881 if c: # closing/loop 882 if siblings: 883 try: 884 scope = next(siblings) 885 callback = callable(scope) 886 silent = callback 887 rewind(token_option) 888 except StopIteration: 889 scope = scopes_pop() 890 siblings, callback, silent = stack_pop() 891 else: 892 scope = scopes_pop() 893 siblings, callback, silent = stack_pop() 894 895 else: # block 896 scopes_append(scope) 897 stack_append((siblings, callback, silent)) 898 899 scope = getter(scope, scopes, decode(token_name), None) 900 falsy = ( # emulate JS falseness 901 scope in falsy_primitives 902 or isinstance(scope, TSequence) and not scope 903 ) 904 if token_option: # falsy block 905 siblings = None 906 callback = False 907 silent = not falsy 908 elif falsy: # truthy block with falsy value 909 siblings = None 910 callback = False 911 silent = True 912 elif ( 913 isinstance(scope, TIterable) 914 and not isinstance(scope, TNonLooping) 915 ): # loop block 916 try: 917 siblings = iter(scope) 918 scope = next(siblings) 919 callback = callable(scope) 920 silent = callback 921 except StopIteration: 922 siblings = None 923 scope = None 924 callback = False 925 silent = True 926 else: # truthy block with truthy value 927 siblings = None 928 callback = callable(scope) 929 silent = callback 930 931 elif c: # partial 932 value = resolver(decode(token_name)) 933 if value: 934 yield from process( 935 template=value, 936 scope=scope, 937 scopes=scopes, 938 resolver=resolver, 939 escape=escape, 940 tags=current_tags, 941 cache=cache, 942 ) 943 # else: comment 944 elif b: 945 if c: # variable 946 value = getter(scope, scopes, decode(token_name), missing) 947 if value is not missing: 948 yield ( 949 stringify(value, text_mode) 950 if token_option else 951 escape(stringify(value, text_mode)) 952 ) 953 954 else: # text 955 yield read(token_content) 956 957 elif c: # tags 958 current_tags = read(token_name), read(token_content) 959 960 else: # end 961 yield read(token_content) 962 963 964def stream( 965 template: TString, 966 scope: typing.Any, 967 *, 968 scopes: _abc.Iterable[typing.Any] = (), 969 resolver: PartialResolver = default_resolver, 970 getter: PropertyGetter = default_getter, 971 stringify: StringifyFunction = default_stringify, 972 escape: EscapeFunction = default_escape, 973 lambda_render: LambdaRenderFunctionConstructor = default_lambda_render, 974 tags: TagsTuple = default_tags, 975 cache: CompiledTemplateCache = default_cache, 976) -> _abc.Generator[TString, None, None]: 977 """ 978 Generate rendered mustache template chunks. 979 980 :param template: mustache template (str or bytes) 981 :param scope: current rendering scope (data object) 982 :param scopes: list of precedent scopes 983 :param resolver: callable will be used to resolve partials (bytes) 984 :param getter: callable will be used to pick variables from scope 985 :param stringify: callable will be used to render python types (bytes) 986 :param escape: callable will be used to escape template (bytes) 987 :param lambda_render: lambda render function constructor 988 :param tags: tuple (start, end) specifying the initial mustache delimiters 989 :param cache: mutable mapping for compiled template cache 990 :returns: generator of bytes/str chunks (same type as template) 991 992 :raises UnclosedTokenException: if token is left unclosed 993 :raises ClosingTokenException: if block closing token does not match 994 :raises DelimiterTokenException: if delimiter token syntax is invalid 995 996 """ 997 chunks = process( 998 template=template, 999 scope=scope, 1000 scopes=scopes, 1001 resolver=resolver, 1002 getter=getter, 1003 stringify=stringify, 1004 escape=escape, 1005 lambda_render=lambda_render, 1006 tags=tags, 1007 cache=cache, 1008 ) 1009 yield from ( 1010 codecs.iterdecode(chunks, 'utf8') 1011 if isinstance(template, str) else 1012 chunks 1013 ) 1014 1015 1016def render( 1017 template: TString, 1018 scope: typing.Any, 1019 *, 1020 scopes: _abc.Iterable[typing.Any] = (), 1021 resolver: PartialResolver = default_resolver, 1022 getter: PropertyGetter = default_getter, 1023 stringify: StringifyFunction = default_stringify, 1024 escape: EscapeFunction = default_escape, 1025 lambda_render: LambdaRenderFunctionConstructor = default_lambda_render, 1026 tags: TagsTuple = default_tags, 1027 cache: CompiledTemplateCache = default_cache, 1028) -> TString: 1029 """ 1030 Render mustache template. 1031 1032 :param template: mustache template 1033 :param scope: current rendering scope (data object) 1034 :param scopes: list of precedent scopes 1035 :param resolver: callable will be used to resolve partials (bytes) 1036 :param getter: callable will be used to pick variables from scope 1037 :param stringify: callable will be used to render python types (bytes) 1038 :param escape: callable will be used to escape template (bytes) 1039 :param lambda_render: lambda render function constructor 1040 :param tags: tuple (start, end) specifying the initial mustache delimiters 1041 :param cache: mutable mapping for compiled template cache 1042 :returns: rendered bytes/str (type depends on template) 1043 1044 :raises UnclosedTokenException: if token is left unclosed 1045 :raises ClosingTokenException: if block closing token does not match 1046 :raises DelimiterTokenException: if delimiter token syntax is invalid 1047 1048 """ 1049 data = b''.join(process( 1050 template=template, 1051 scope=scope, 1052 scopes=scopes, 1053 resolver=resolver, 1054 getter=getter, 1055 stringify=stringify, 1056 escape=escape, 1057 lambda_render=lambda_render, 1058 tags=tags, 1059 cache=cache, 1060 )) 1061 return data.decode() if isinstance(template, str) else data 1062 1063 1064def cli(argv: typing.Optional[_abc.Sequence[str]] = None) -> None: 1065 """ 1066 Render template from command line. 1067 1068 Use `python -m ustache --help` to check available options. 1069 1070 :param argv: command line arguments, :attr:`sys.argv` when None 1071 1072 """ 1073 import argparse 1074 import json 1075 import sys 1076 1077 arguments = argparse.ArgumentParser( 1078 description='Render mustache template.', 1079 ) 1080 arguments.add_argument( 1081 'template', 1082 metavar='PATH', 1083 type=argparse.FileType('r'), 1084 help='template file', 1085 ) 1086 arguments.add_argument( 1087 '-j', '--json', 1088 metavar='PATH', 1089 type=argparse.FileType('r'), 1090 default=sys.stdin, 1091 help='JSON file, default: stdin', 1092 ) 1093 arguments.add_argument( 1094 '-o', '--output', 1095 metavar='PATH', 1096 type=argparse.FileType('w'), 1097 default=sys.stdout, 1098 help='output file, default: stdout', 1099 ) 1100 args = arguments.parse_args(argv) 1101 try: 1102 args.output.write(render(args.template.read(), json.load(args.json))) 1103 finally: 1104 args.template.close() 1105 if args.json is not sys.stdin: 1106 args.json.close() 1107 if args.output is not sys.stdout: 1108 args.output.close() 1109 1110 1111if __name__ == '__main__': 1112 cli() 1113