Source code for ll.ul4c

# -*- coding: utf-8 -*-
# cython: language_level=3, always_allow_keywords=True

## Copyright 2009-2024 by LivingLogic AG, Bayreuth/Germany
## Copyright 2009-2024 by Walter Dörwald
##
## All Rights Reserved
##
## See ll/xist/__init__.py for the license


"""
:mod:`!ll.ul4c` provides templating for XML/HTML as well as any other text-based
format. A template defines placeholders for data output and basic logic (like
loops and conditional blocks), that define how the final rendered output will
look.

:mod:`!ll.ul4c` compiles a template to an internal format, which makes it
possible to implement template renderers in multiple programming languages.
"""


__docformat__ = "reStructuredText"


import re, io, os.path, datetime, urllib.parse as urlparse, json, collections
import locale, itertools, random, functools, math, inspect, contextlib
import types, textwrap, decimal, operator

from collections import abc

import antlr3


# Regular expression used for splitting dates in isoformat
_datesplitter = re.compile("[-T:.]")


_defaultitem = object()


def register(name):
	from ll import ul4on

	def registration(cls):
		ul4on.register("de.livinglogic.ul4." + name)(cls)
		cls.type = name
		return cls
	return registration


[docs] def withcontext(f): """ Normally when a function is exposed to UL4 this function will be called directly. However when the function needs access to the rendering context (i.e. the local variables or information about the call stack), the function must have an additional first parameter, and UL4 must be told that this parmeter is required. This can be done with this decorator. """ f.ul4_context = True return f
def _create_module(name, doc, **attrs): module = types.ModuleType(name, doc) module.ul4_attrs = {"__name__", "__doc__"} for (attrname, attrvalue) in attrs.items(): setattr(module, attrname, attrvalue) module.ul4_attrs.add(attrname) return module ### ### Stream classes for various output modes. ###
[docs] class NullStream: """ Output stream that ignores all writes. """ def write(self, text): pass def writelines(self, lines): pass def flush(self): pass def close(self): pass def __enter__(self): pass def __exit__(self, exc_type, exc_value, traceback): pass
[docs] class XMLEscapeStream: """ Output stream that delegates all writes to an underlying stream, but XML escapes everything that gets written. """ def __init__(self, stream): self.stream = stream def write(self, text): self.stream.write(_xmlescape(text)) def writelines(self, lines): for line in lines: self.write(line) def flush(self): self.stream.flush() def close(self): self.stream.close() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close()
error_underline = os.environ.get("LL_UL4_ERRORUNDERLINE", "~")[:1] or "~" ### ### Exceptions ###
[docs] class LocationError(Exception): """ Exception class that provides a location inside an UL4 template. If an exception happens inside an UL4 template, this exception will propagate outwards and will be decorated with :class:`LocationError` instances which will be chained via the ``__cause__`` attribute. Only the original exception will be reraised again and again, so these :class:`LocationError` will never have a traceback attached to them. The first ``__cause__`` attribute marks the location in the UL4 source where the exception happened and the last ``__cause__`` attribute at the end of the exception chain marks the outermost call. """ def __init__(self, location): self.location = location _condensewhitespace = re.compile("[\t\n\r\f\v]+") def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__} in {self.location} offset {_offset(self.location.pos)} at {id(self):#x}>" def strtemplate(self): template = self.location.template prefix = "in local template" if template.parenttemplate is not None else "in template" out = [] while template is not None: fullname = template.fullname out.append(repr(fullname) if fullname is not None else "(unnamed)") template = template.parenttemplate return f"{prefix} {' in '.join(out)}" def strlocation(self): loc = self.location return f"offset {_offset(loc.startpos)}; line {loc.startline:,}; col {loc.startcol:,}" def __str__(self): prefix = repr(self._condensewhitespace.sub(" ", self.location.startsourceprefix))[1:-1] source = repr(self._condensewhitespace.sub(" ", self.location.startsource))[1:-1] suffix = repr(self._condensewhitespace.sub(" ", self.location.startsourcesuffix))[1:-1] indent = ' '*len(prefix) underline = error_underline*len(source) return f"{self.strtemplate()}: {self.strlocation()}\n{prefix}{source}{suffix}\n{indent}{underline}" def ul4_getattr(self, name): if name == "context": if self.__context__ is not None and not self.__suppress_context__: return self.__context__ elif self.__cause__ is not None: return self.__cause__ return None elif name == "location": return self.location else: raise AttributeError(name)
[docs] class BlockError(Exception): """ Exception that is raised by the compiler when an illegal block structure is detected (e.g. an ``<?end if?>`` without a previous ``<?if?>``). """ def __init__(self, message): self.message = message def __str__(self): return self.message
### ### Exceptions used by the interpreted code for flow control ### class BreakException(Exception): pass class ContinueException(Exception): pass class ReturnException(Exception): def __init__(self, value): self.value = value
[docs] class Context: """ A :class:`Context` object stores the context of a call to a template. This consists of local, global and builtin variables and the indent stack. """ # "Builtin" functions, types and modules. Will be exposed to UL4 code builtins = {} def __init__(self, globals=None, stream=None): self._globals = globals if globals is not None else {} if not self.builtins: self.add_builtins() self.vars = collections.ChainMap({}, self.globals, self.builtins) self.indents = [] # Stack of additional indentations for the ``<?render?>`` tag self.escapes = [] # Stack of functions for escaping the output self.asts = [] # Call stack (of :class:`AST` objects) self.stream = stream if stream is not None else NullStream() @property def globals(self): return self._globals @globals.setter def globals(self, vars): self.vars.maps[-2] = self._globals = vars @classmethod def add_builtins(cls): from ll import misc, color, ul4on b = cls.builtins if not b: b["repr"] = _repr b["ascii"] = _ascii b["now"] = _now b["today"] = datetime.date.today b["utcnow"] = datetime.datetime.utcnow b["random"] = random.random b["xmlescape"] = _xmlescape b["csv"] = _csv b["asjson"] = _asjson b["fromjson"] = _fromjson b["asul4on"] = ul4on.dumps b["fromul4on"] = _fromul4on b["len"] = len b["abs"] = abs b["any"] = any b["all"] = all b["enumerate"] = enumerate b["enumfl"] = _enumfl b["isfirst"] = misc.isfirst b["islast"] = misc.islast b["isfirstlast"] = misc.isfirstlast b["isundefined"] = _isundefined b["isdefined"] = _isdefined b["isnone"] = _isnone b["isstr"] = _isstr b["isint"] = _isint b["isfloat"] = _isfloat b["isbool"] = _isbool b["isdate"] = _isdate b["isdatetime"] = _isdatetime b["istimedelta"] = _istimedelta b["ismonthdelta"] = _ismonthdelta b["isexception"] = _isexception b["isinstance"] = _isinstance b["islist"] = _islist b["isset"] = _isset b["isdict"] = _isdict b["iscolor"] = _iscolor b["istemplate"] = _istemplate b["isfunction"] = _isfunction b["chr"] = chr b["ord"] = ord b["hex"] = hex b["oct"] = oct b["bin"] = bin b["min"] = _min b["max"] = _max b["first"] = misc.first b["last"] = misc.last b["sum"] = sum b["sorted"] = _sorted b["range"] = range b["slice"] = itertools.islice b["type"] = _type b["reversed"] = reversed b["randrange"] = _randrange b["randchoice"] = random.choice b["format"] = _format b["zip"] = zip b["urlquote"] = _urlquote b["urlunquote"] = _urlunquote b["rgb"] = color.Color.fromrgb b["hls"] = color.Color.fromhls b["hsv"] = color.Color.fromhsv b["md5"] = _md5 b["scrypt"] = _scrypt b["round"] = _round b["floor"] = _floor b["ceil"] = _ceil b["exp"] = math.exp b["log"] = math.log b["pow"] = math.pow b["getattr"] = _getattr b["setattr"] = _setattr b["hasattr"] = _hasattr b["dir"] = _dir b["bool"] = BoolType b["int"] = IntType b["float"] = FloatType b["str"] = StrType b["date"] = DateType b["datetime"] = DateTimeType b["timedelta"] = TimeDeltaType b["monthdelta"] = misc.monthdelta.ul4_type b["list"] = ListType b["set"] = SetType b["dict"] = DictType b["color"] = _create_module( "color", "Types and functions for handling RGBA colors", Color=color.Color.ul4_type, css=color.css, mix=color.mix, ) b["ul4"] = _create_module( "ul4", "UL4 - A templating language", AST=AST.ul4_type, TextAST=TextAST.ul4_type, IndentAST=IndentAST.ul4_type, LineEndAST=LineEndAST.ul4_type, CodeAST=CodeAST.ul4_type, ConstAST=ConstAST.ul4_type, SeqItemAST=SeqItemAST.ul4_type, UnpackSeqItemAST=UnpackSeqItemAST.ul4_type, ListAST=ListAST.ul4_type, ListComprehensionAST=ListComprehensionAST.ul4_type, SetAST=SetAST.ul4_type, SetComprehensionAST=SetComprehensionAST.ul4_type, DictItemAST=DictItemAST.ul4_type, UnpackDictItemAST=UnpackDictItemAST.ul4_type, DictAST=DictAST.ul4_type, DictComprehensionAST=DictComprehensionAST.ul4_type, GeneratorExpressionAST=GeneratorExpressionAST.ul4_type, VarAST=VarAST.ul4_type, BlockAST=BlockAST.ul4_type, ConditionalBlocksAST=ConditionalBlocksAST.ul4_type, IfBlockAST=IfBlockAST.ul4_type, ElIfBlockAST=ElIfBlockAST.ul4_type, ElseBlockAST=ElseBlockAST.ul4_type, ForBlockAST=ForBlockAST.ul4_type, WhileBlockAST=WhileBlockAST.ul4_type, BreakAST=BreakAST.ul4_type, ContinueAST=ContinueAST.ul4_type, AttrAST=AttrAST.ul4_type, SliceAST=SliceAST.ul4_type, UnaryAST=UnaryAST.ul4_type, NotAST=NotAST.ul4_type, IfAST=IfAST.ul4_type, NegAST=NegAST.ul4_type, BitNotAST=BitNotAST.ul4_type, PrintAST=PrintAST.ul4_type, PrintXAST=PrintXAST.ul4_type, ReturnAST=ReturnAST.ul4_type, BinaryAST=BinaryAST.ul4_type, ItemAST=ItemAST.ul4_type, ShiftLeftAST=ShiftLeftAST.ul4_type, ShiftRightAST=ShiftRightAST.ul4_type, BitAndAST=BitAndAST.ul4_type, BitXOrAST=BitXOrAST.ul4_type, BitOrAST=BitOrAST.ul4_type, IsAST=IsAST.ul4_type, IsNotAST=IsNotAST.ul4_type, EQAST=EQAST.ul4_type, NEAST=NEAST.ul4_type, LTAST=LTAST.ul4_type, LEAST=LEAST.ul4_type, GTAST=GTAST.ul4_type, GEAST=GEAST.ul4_type, ContainsAST=ContainsAST.ul4_type, NotContainsAST=NotContainsAST.ul4_type, AddAST=AddAST.ul4_type, SubAST=SubAST.ul4_type, MulAST=MulAST.ul4_type, FloorDivAST=FloorDivAST.ul4_type, TrueDivAST=TrueDivAST.ul4_type, OrAST=OrAST.ul4_type, AndAST=AndAST.ul4_type, ModAST=ModAST.ul4_type, ChangeVarAST=ChangeVarAST.ul4_type, SetVarAST=SetVarAST.ul4_type, AddVarAST=AddVarAST.ul4_type, SubVarAST=SubVarAST.ul4_type, MulVarAST=MulVarAST.ul4_type, FloorDivVarAST=FloorDivVarAST.ul4_type, TrueDivVarAST=TrueDivVarAST.ul4_type, ModVarAST=ModVarAST.ul4_type, ShiftLeftVarAST=ShiftLeftVarAST.ul4_type, ShiftRightVarAST=ShiftRightVarAST.ul4_type, BitAndVarAST=BitAndVarAST.ul4_type, BitXOrVarAST=BitXOrVarAST.ul4_type, BitOrVarAST=BitOrVarAST.ul4_type, PositionalArgumentAST=PositionalArgumentAST.ul4_type, KeywordArgumentAST=KeywordArgumentAST.ul4_type, UnpackListArgumentAST=UnpackListArgumentAST.ul4_type, UnpackDictArgumentAST=UnpackDictArgumentAST.ul4_type, CallAST=CallAST.ul4_type, RenderAST=RenderAST.ul4_type, RenderXAST=RenderXAST.ul4_type, RenderOrPrintAST=RenderOrPrintAST.ul4_type, RenderOrPrintXAST=RenderOrPrintXAST.ul4_type, RenderXOrPrintAST=RenderXOrPrintAST.ul4_type, RenderXOrPrintXAST=RenderXOrPrintXAST.ul4_type, RenderBlockAST=RenderBlockAST.ul4_type, RenderBlocksAST=RenderBlocksAST.ul4_type, SignatureAST=SignatureAST.ul4_type, Template=Template.ul4_type, TemplateClosure=TemplateClosure.ul4_type, ) b["ul4on"] = _create_module( "ul4on", "Object serialization", loads=ul4on.loads, dumps=ul4on.dumps, Encoder=ul4on.Encoder.ul4_type, Decoder=ul4on.Decoder.ul4_type, ) b["operator"] = _create_module( "operator", "Various operators as functions", attrgetter=AttrGetter.ul4_type, itemgetter=ItemGetter.ul4_type, ) b["math"] = _create_module( "math", "Math related functions and constants", cos=math.cos, sin=math.sin, tan=math.tan, sqrt=math.sqrt, isclose=math.isclose, pi=math.pi, e=math.e, tau=math.tau, ) @contextlib.contextmanager def replacevars(self, vars): oldvars = self.vars.maps[0] try: self.vars.maps[0] = vars yield finally: self.vars.maps[0] = oldvars @contextlib.contextmanager def chainvars(self): try: self.vars.maps.insert(0, {}) yield finally: del self.vars.maps[0] @contextlib.contextmanager def replacestream(self, stream): oldstream = self.stream try: self.stream = stream yield finally: self.stream = oldstream def write(self, string): self.stream.write(string)
### ### Helper functions ### def _decorateexception(exc, ast, obj=None): # Find the end of the exception chain while exc.__cause__: exc = exc.__cause__ # Attach location to innermost exception if not isinstance(exc, LocationError) or (isinstance(ast, CallAST) and isinstance(obj, (Template, TemplateClosure))): exc.__cause__ = LocationError(ast) def _handleeval(f): """ Decorator for an implementation of the :meth:`eval` method that does not do output (so it is a normal method). This decorator is responsible for exception handling. An exception that bubbles up the Python call stack will generate an exception chain that follows the UL4 call stack. """ @functools.wraps(f) def wrapped(self, context, /, *args, **kwargs): context.asts.append(self) try: return f(self, context, *args, **kwargs) except (BreakException, ContinueException, ReturnException): # Pass those exception through to the AST nodes that will handle them (:class:`ForBlockAST` or :class:`Template`) raise except Exception as exc: _decorateexception(exc, self) raise finally: context.asts.pop() return wrapped def _unpackvar(lvalue, value): """ A generator used for recursively unpacking values for assignment. ``lvalue`` may be an :class:`AST` object (in which case the recursion ends) or a (possible nested) sequence of :class:`AST` objects. The values produced are (AST node, value) tuples. """ if isinstance(lvalue, AST): yield (lvalue, value) else: # Materialize iterators on the right hand side, but protect against infinite iterators if not isinstance(value, (tuple, list, str)): # If we get one item more than required, we have an error # Also :func:`islice` might fail if the right hand side isn't iterable (e.g. ``(a, b) = 42``) value = list(itertools.islice(value, len(lvalue)+1)) if len(lvalue) != len(value): # The number of variables on the left hand side doesn't match the number of values on the right hand side raise TypeError(f"need {len(lvalue):,} value{'s' if len(lvalue) != 1 else ''} to unpack") for (lvalue, value) in zip(lvalue, value): yield from _unpackvar(lvalue, value) def _makevars(signature, args, kwargs): """ Bind ``args`` and ``kwargs`` to the :class:`inspect.Signature` object ``signature`` and return the resulting argument dictionary. (This differs from :meth:`inspect.Signature.bind` in that it handles default values too.) ``signature`` may also be :const:`None` in which case ``args`` must be empty and `kwargs` is returned, i.e. the signature is treated as accepting no positional argument and any keyword argument. """ if signature is None: if args: raise TypeError("positional arguments not supported") return kwargs else: vars = signature.bind(*args, **kwargs) vars.apply_defaults() return vars.arguments def _linecol(source, index): index = index or 0 lastlinefeed = source.rfind("\n", 0, index) if lastlinefeed >= 0: return (source.count("\n", 0, index)+1, index-lastlinefeed) else: return (1, index + 1) def _offset(pos): offset = ["["] if pos.start is not None: offset.append(f"{pos.start:,}") offset.append(":") if pos.stop is not None: offset.append(f"{pos.stop:,}") offset.append("]") return "".join(offset) def _sourceprefix(source, pos): outerstartpos = innerstartpos = pos preprefix = "" maxprefix = 40 while maxprefix > 0: # We arrived at the start of the source if outerstartpos == 0: break # We arrived at the start of the line if source[outerstartpos-1] == "\n": break maxprefix -= 1 outerstartpos -= 1 else: # We've exhausted the length of the prefix preprefix = "\N{HORIZONTAL ELLIPSIS}" return preprefix + source[outerstartpos:innerstartpos] def _sourcesuffix(source, pos): outerstoppos = innerstoppos = pos postsuffix = "" maxsuffix = 40 while maxsuffix > 0: # We arrived at the end of the source if outerstoppos >= len(source): break # We arrived at the end of the line if source[outerstoppos] == "\n": break maxsuffix -= 1 outerstoppos += 1 else: # We've exhausted the length of the suffix postsuffix = "\N{HORIZONTAL ELLIPSIS}" return source[innerstoppos:outerstoppos] + postsuffix def _dumpslice(encoder, slice): encoder.dump(slice.start if slice is not None and slice.start is not None else -1) encoder.dump(slice.stop if slice is not None and slice.stop is not None else -1) def _loadslice(decoder): posstart = decoder.load() posstop = decoder.load() if posstart >= 0: if posstop >= 0: return slice(posstart, posstop) else: return slice(posstart, None) else: if posstop >= 0: return slice(None, posstop) else: return None ### ### Helper functions for the various UL4 functions ### def _str(obj="", /): from ll import color if obj is None: return "" elif isinstance(obj, Undefined): return "" elif isinstance(obj, str): return obj elif isinstance(obj, datetime.datetime): if obj.microsecond or obj.second: return str(obj) else: return str(obj)[:-3] elif isinstance(obj, color.Color): return str(obj) elif isinstance(obj, (abc.Sequence, abc.Set, abc.Mapping)): return _repr(obj) elif isinstance(obj, inspect.Signature): v = [] v.append("(") for (i, p) in enumerate(obj.parameters.values()): if i: v.append(", ") if p.kind == p.VAR_POSITIONAL: v.append("*") elif p.kind == p.VAR_KEYWORD: v.append("**") v.append(p.name) if p.default is not p.empty: v.append("=") v.append(_repr(p.default)) v.append(")") return "".join(v) else: return str(obj) def _repr_helper(obj, seen, forceascii): from ll import color if isinstance(obj, str): if forceascii: yield ascii(obj) else: yield repr(obj) elif isinstance(obj, datetime.datetime): s = obj.isoformat() if s.endswith("T00:00:00"): s = s[:-8] elif s.endswith(":00"): s = s[:-3] yield f"@({s})" elif isinstance(obj, datetime.date): yield f"@({obj.isoformat()})" elif isinstance(obj, datetime.timedelta): yield repr(obj).partition(".")[-1] elif isinstance(obj, color.Color): if obj[3] == 0xff: s = f"#{obj[0]:02x}{obj[1]:02x}{obj[2]:02x}" if s[1] == s[2] and s[3] == s[4] and s[5] == s[6]: s = f"#{s[1]}{s[3]}{s[5]}" yield s else: s = f"#{obj[0]:02x}{obj[1]:02x}{obj[2]:02x}{obj[3]:02x}" if s[1] == s[2] and s[3] == s[4] and s[5] == s[6] and s[7] == s[8]: s = f"#{s[1]}{s[3]}{s[5]}{s[7]}" yield s elif isinstance(obj, abc.Sequence): if id(obj) in seen: yield "..." else: seen.add(id(obj)) yield "[" for (i, item) in enumerate(obj): if i: yield ", " yield from _repr_helper(item, seen, forceascii) yield "]" seen.discard(id(obj)) elif isinstance(obj, abc.Set): if id(obj) in seen: yield "..." else: if obj: seen.add(id(obj)) yield "{" for (i, item) in enumerate(obj): if i: yield ", " yield from _repr_helper(item, seen, forceascii) yield "}" seen.discard(id(obj)) else: yield "{/}" elif isinstance(obj, inspect.Signature): if id(obj) in seen: yield "..." else: seen.add(id(obj)) yield "<Signature (" for (i, p) in enumerate(obj.parameters.values()): if i: yield ", " if p.kind == p.VAR_POSITIONAL: yield "*" elif p.kind == p.VAR_KEYWORD: yield "**" yield p.name if p.default is not p.empty: yield "=" yield from _repr_helper(p.default, seen, forceascii) yield ")>" seen.discard(id(obj)) elif isinstance(obj, abc.Mapping): if id(obj) in seen: yield "..." else: seen.add(id(obj)) yield "{" for (i, (key, value)) in enumerate(obj.items()): if i: yield ", " yield from _repr_helper(key, seen, forceascii) yield ": " yield from _repr_helper(value, seen, forceascii) yield "}" seen.discard(id(obj)) else: if forceascii: yield ascii(obj) else: yield repr(obj) def _repr(obj, /): return "".join(_repr_helper(obj, set(), False)) def _ascii(obj, /): return "".join(_repr_helper(obj, set(), True)) def _asjson(obj, /): from ll import misc, color if obj is None: return "null" elif isinstance(obj, Undefined): return "undefined" elif isinstance(obj, (bool, int, float)): return json.dumps(obj) elif isinstance(obj, str): return json.dumps(obj).replace("<", "\\u003c") # Prevent XSS (when the value is embedded literally in a ``<script>`` tag) elif isinstance(obj, datetime.datetime): return f"new Date({obj.year}, {obj.month-1}, {obj.day}, {obj.hour}, {obj.minute}, {obj.second}, {obj.microsecond//1000})" elif isinstance(obj, datetime.date): return f"new ul4.Date_({obj.year}, {obj.month}, {obj.day})" elif isinstance(obj, datetime.timedelta): return f"new ul4.TimeDelta({obj.days}, {obj.seconds}, {obj.microseconds})" elif isinstance(obj, misc.monthdelta): return f"new ul4.MonthDelta({obj.months()})" elif isinstance(obj, color.Color): return f"new ul4.Color({obj[0]}, {obj[1]}, {obj[2]}, {obj[3]})" elif isinstance(obj, abc.Mapping): items = ", ".join(f"{_asjson(key)}: {_asjson(value)}" for (key, value) in obj.items()) return f"{{{items}}}" elif isinstance(obj, abc.Sequence): items = ", ".join(_asjson(item) for item in obj) return f"[{items}]" elif isinstance(obj, Template): return obj.jssource() else: raise TypeError(f"can't handle object of type {type(obj)}") def _xmlescape(obj, /): if obj is None: return "" elif isinstance(obj, Undefined): return "" else: from ll import misc return misc.xmlescape(_str(obj)) ### ### Type objects for UL4 types ### class Type: ul4_attrs = {"__module__", "__name__", "__doc__"} # Attributes that are returned via a simple ``getattr`` call (either data attributes or as bound methods) plainattrs = frozenset() # Attributes that should appear as data attributes, but are implemented as methods in the :class:`Type` subclass wrappeddataattrs = frozenset() # Attributes that should appear as methods and are implemented as methods in the :class:`Type` subclass wrappedmethattrs = frozenset() def __init__(self, module=None, name=None, doc=None, type=None): self.type = type self.__module__ = module self.__name__ = name if doc is not None: doc = textwrap.dedent(doc).strip() self.__doc__ = doc def __repr__(self): if self.__module__ is None: return f"<type {self.__name__}>" else: return f"<type {self.__module__}.{self.__name__}>" def __set_name__(self, type, name): self.type = type if self.__name__ is None and type.__name__ is not None: self.__name__ = type.__name__ if self.__doc__ is None and type.__doc__ is not None: self.__doc__ = textwrap.dedent(type.__doc__).strip().split("\n\n")[0] def instancecheck(self, obj): return isinstance(obj, self.type) def _wrapmethod(self, obj, name): func = getattr(self, name) def wrapped(*args, **kwargs): return func(obj, *args, **kwargs) wrapped.__name__ = name wrapped.__module__ = self.__module__ wrapped.__qualname__ = name return wrapped def getattr(self, obj, name, default=object): """ Return the attribute ``name`` of the object :obj`obj` and honor ``ul4_getattr`` and ``ul4_attrs``. """ ul4_getattr = getattr(obj, "ul4_getattr", None) ul4_attrs = getattr(obj, "ul4_attrs", None) if ul4_getattr is not None: try: return ul4_getattr(name) except AttributeError: return self.missing(obj, name, default) else: if ul4_attrs is not None and name in ul4_attrs: return getattr(obj, name) elif name in self.plainattrs: return getattr(obj, name) elif name in self.wrappeddataattrs: return getattr(self, name)(obj) elif name in self.wrappedmethattrs: return self._wrapmethod(obj, name) return self.missing(obj, name, default) def missing(self, obj, name, default=object): if default is object: raise AttributeError(name) return default def setattr(self, obj, name, value): """ Set the attribute ``name`` of the object :obj`obj` to ``value`` and honors ``ul4_setattr`` and ``ul4_attrs``. """ ul4_setattr = getattr(obj, "ul4_setattr", None) if ul4_setattr is not None: ul4_setattr(name, value) else: ul4_attrs = getattr(obj, "ul4_attrs", None) if ul4_attrs is not None: # An ``ul4_attrs`` attribute without ``ul4_setattr`` will *not* make the attribute writable from ll import misc raise TypeError(f"attribute {misc.format_class(obj)}.{name!r} is readonly") else: obj[name] = value def hasattr(self, obj, name): """ Return whether the object :obj`obj` has an attribute ``name`` and honors ``ul4_hasattr`` and ``ul4_attrs``. """ ul4_hasattr = getattr(obj, "ul4_hasattr", None) if ul4_hasattr is not None: return ul4_hasattr(name) else: ul4_attrs = getattr(obj, "ul4_attrs", None) if ul4_attrs is not None: return name in ul4_attrs else: return name in self.plainattrs or name in self.wrappeddataattrs or name in self.wrappedmethattrs def dir(self, obj): ul4_dir = getattr(obj, "ul4_dir", None) if ul4_dir is not None: return ul4_dir() ul4_attrs = getattr(obj, "ul4_attrs", None) if ul4_attrs is not None: return ul4_attrs return frozenset({*self.plainattrs, *self.wrappeddataattrs, *self.wrappedmethattrs}) class InstantiableType(Type): def __call__(self, /, *args, **kwargs): return self.type(*args, **kwargs) class GenericType(Type): def __init__(self, cls): super().__init__(cls.__module__ if cls.__module__ != "builtins" else None, cls.__name__, cls.__doc__) self.type = cls class GenericExceptionType(GenericType): wrappeddataattrs = {"context"} @staticmethod def context(obj): if obj.__cause__ is not None: return obj.__cause__ elif obj.__context__ is not None and not obj.__suppress_context__: return obj.__context__ return None class NoneType(Type): def instancecheck(self, obj): return obj is None NoneType = NoneType(None, "None", "Nothing") BoolType = InstantiableType(None, "bool", "A boolean value (True or False)", type=bool) class IntType(Type): def __call__(self, obj=0, /, base=None): if base is None: return int(obj) else: return int(obj, base) def instancecheck(self, obj): return isinstance(obj, int) and not isinstance(obj, bool) IntType = IntType(None, "int", "An integer value") class FloatType(Type): def __call__(self, x=0.0, /): return float(x) def instancecheck(self, obj): return isinstance(obj, float) FloatType = FloatType(None, "float", "An floating point value") class StrType(Type): wrappedmethattrs = {"split", "rsplit", "splitlines", "strip", "lstrip", "rstrip", "upper", "lower", "capitalize", "startswith", "endswith", "replace", "count", "find", "rfind", "join"} def __call__(self, obj="", /): return _str(obj) def instancecheck(self, obj): return isinstance(obj, str) @staticmethod def split(obj, sep=None, maxsplit=None): return obj.split(sep, maxsplit if maxsplit is not None else -1) @staticmethod def rsplit(obj, sep=None, maxsplit=None): return obj.rsplit(sep, maxsplit if maxsplit is not None else -1) @staticmethod def splitlines(obj, keepends=False): return obj.splitlines(keepends) @staticmethod def strip(obj, chars=None, /): return obj.strip(chars) @staticmethod def lstrip(obj, chars=None, /): return obj.lstrip(chars) @staticmethod def rstrip(obj, chars=None, /): return obj.rstrip(chars) @staticmethod def count(obj, sub, start=None, end=None, /): return obj.count(sub, start, end) @staticmethod def find(obj, sub, start=None, end=None, /): return obj.find(sub, start, end) @staticmethod def rfind(obj, sub, start=None, end=None, /): return obj.rfind(sub, start, end) @staticmethod def startswith(obj, prefix, /): if isinstance(prefix, list): prefix = tuple(prefix) return obj.startswith(prefix) @staticmethod def endswith(obj, suffix, /): if isinstance(suffix, list): suffix = tuple(suffix) return obj.endswith(suffix) @staticmethod def upper(obj): return obj.upper() @staticmethod def lower(obj): return obj.lower() @staticmethod def capitalize(obj): return obj.capitalize() @staticmethod def replace(obj, old, new, count=-1, /): return obj.replace(old, new, count if count is not None else -1) @staticmethod def join(obj, iterable, /): return obj.join(iterable) StrType = StrType(None, "str", "A string") class ListType(Type): wrappedmethattrs = {"append", "insert", "pop", "count", "find", "rfind"} def __call__(self, iterable=(), /): return list(iterable) def instancecheck(self, obj): from ll import color return isinstance(obj, (list, tuple, abc.Sequence)) and not isinstance(obj, (str, color.Color)) @staticmethod def append(obj, *items): obj.extend(items) @staticmethod def insert(obj, pos, *items): obj[pos:pos] = items @staticmethod def pop(obj, pos=-1): return obj.pop(pos) @staticmethod def count(obj, sub, start=None, end=None, /): if start is None and end is None: return obj.count(sub) else: (start, stop, stride) = slice(start, end).indices(len(obj)) count = 0 for i in range(start, stop, stride): if obj[i] == sub: count += 1 return count @staticmethod def find(obj, sub, start=None, end=None, /): try: if end is None: if start is None: return obj.index(sub) return obj.index(sub, start) return obj.index(sub, start, end) except ValueError: return -1 @staticmethod def rfind(obj, sub, start=None, end=None, /): for i in reversed(range(*slice(start, end).indices(len(obj)))): if obj[i] == sub: return i return -1 ListType = ListType(None, "list", "A list") class DateType(Type): wrappedmethattrs = {"weekday", "yearday", "week", "calendar", "day", "month", "year", "date", "mimeformat", "isoformat"} def __call__(self, year, month, day): return datetime.date(year, month, day) def instancecheck(self, obj): return isinstance(obj, datetime.date) and not isinstance(obj, datetime.datetime) @staticmethod def weekday(obj): return obj.weekday() @staticmethod def calendar(obj, firstweekday=0, mindaysinfirstweek=4): """ Return the calendar year the date ``obj`` belongs to, the calendar week number and the week day. (A day might belong to a different calender year, if it is in week 1 but before January 1, or if belongs to week 1 of the following year). ``firstweekday`` defines what a week is (i.e. which weekday is considered the start of the week, ``0`` is Monday and ``6`` is Sunday). ``mindaysinfirstweek`` defines how many days must be in a week to be considered the first week in the year. For example for the ISO week number (according to https://en.wikipedia.org/wiki/ISO_week_date) the week starts on Monday (i.e. ``firstweekday == 0``) and a week is considered the first week if it's the first week that contains a Thursday (which means that this week contains at least four days in January, so ``mindaysinfirstweek == 4``). This is also the default for both parameters. For the US ``firstweekday == 6`` and ``mindaysinfirstweek == 1``, i.e. the week starts on Sunday and January the first is always in week 1. There's also the convention that the week 1 is the first complete week in January. For this ``mindaysinfirstweek == 7``. For example ``<?print repr(@(2000-02-29).calendar()?>`` prints ``[2000, 9, 1]``, i.e. this day is the Tuesday in week 9 of the year 2000. """ # Normalize parameters firstweekday %= 7 mindaysinfirstweek = max(1, min(mindaysinfirstweek, 7)) # ``obj`` might be in the first week of the next year, or last week of # the previous year, so we might have to try those too. for year in (obj.year+1, obj.year, obj.year-1): # ``refdate`` will always be in week 1 refdate = obj.__class__(year, 1, mindaysinfirstweek) # Go back to the start of ``refdate``\s week (i.e. day 1 of week 1) weekstartdate = refdate - datetime.timedelta((refdate.weekday() - firstweekday) % 7) # Is our date ``obj`` at or after day 1 of week 1? # (if not we have to recalculate based on the year before in the next loop iteration) if obj >= weekstartdate: # Add 1, because the first week is week 1, not week 0 return (year, (obj - weekstartdate).days//7 + 1, obj.weekday()) @staticmethod def week(obj, firstweekday=0, mindaysinfirstweek=4): """ Return the week number of the date ``obj``. For more info see :meth:`calendar`. """ return DateType.calendar(obj, firstweekday, mindaysinfirstweek)[1] @staticmethod def day(obj): return obj.day @staticmethod def month(obj): return obj.month @staticmethod def year(obj): return obj.year @staticmethod def date(obj): return obj @staticmethod def mimeformat(obj): weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") monthname = (None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") return f"{weekdayname[obj.weekday()]}, {obj.day:02d} {monthname[obj.month]:3} {obj.year:4}" @staticmethod def isoformat(obj): return obj.isoformat() @staticmethod def yearday(obj): return (obj - obj.__class__(obj.year, 1, 1)).days+1 DateType = DateType(None, "date", "A date") class DateTimeType(DateType.__class__): wrappedmethattrs = DateType.wrappedmethattrs.union({"hour", "minute", "second", "microsecond", "timestamp"}) def __call__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0): return datetime.datetime(year, month, day, hour, minute, second, microsecond) def instancecheck(self, obj): return isinstance(obj, datetime.datetime) @staticmethod def hour(obj): return obj.hour @staticmethod def minute(obj): return obj.minute @staticmethod def second(obj): return obj.second @staticmethod def microsecond(obj): return obj.microsecond @staticmethod def date(obj): return obj.date() @staticmethod def mimeformat(obj): weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") monthname = (None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") return f"{weekdayname[obj.weekday()]}, {obj.day:02d} {monthname[obj.month]:3} {obj.year:4} {obj.hour:02}:{obj.minute:02}:{obj.second:02} GMT" @staticmethod def timestamp(obj): """ Return the number of seconds since 1970-01-01 with microseconds precision. """ return obj.timestamp() DateTimeType = DateTimeType(None, "datetime", "A date and time value") class TimeDeltaType(Type): wrappedmethattrs = {"days", "seconds", "microseconds"} def __call__(self, days=0, seconds=0, microseconds=0): return datetime.timedelta(days, seconds, microseconds) def instancecheck(self, obj): return isinstance(obj, datetime.timedelta) @staticmethod def days(obj): return obj.days @staticmethod def seconds(obj): return obj.seconds @staticmethod def microseconds(obj): return obj.microseconds TimeDeltaType = TimeDeltaType(None, "timedelta", "A time span") class DictType(Type): plainattrs = {"keys", "items", "values", "clear", "pop"} wrappedmethattrs = {"get", "update"} def __call__(self, *args, **kwargs): return dict(*args, **kwargs) def instancecheck(self, obj): return isinstance(obj, (dict, abc.Mapping)) @staticmethod def get(obj, key, default=None, /): return obj.get(key, default) @staticmethod def update(obj, *others, **kwargs): for other in others: obj.update(other) obj.update(**kwargs) def missing(self, obj, name, default=None): if name in obj: return obj[name] return super().missing(obj, name) DictType = DictType(None, "dict", "A dictionary") class SetType(Type): plainattrs = {"clear"} wrappedmethattrs = {"add"} def __call__(self, iterable=(), /): return set(iterable) def instancecheck(self, obj): return isinstance(obj, (set, frozenset, abc.Set)) @staticmethod def add(obj, *items): obj.update(items) SetType = SetType(None, "set", "A set") class SliceType(Type): plainattrs = {"start", "stop"} def instancecheck(self, obj): return isinstance(obj, slice) SliceType = SliceType(None, "slice", "A slice") class AttrGetter: ul4_type = InstantiableType("operator", "attrgetter", "Return a callable object that fetches the given attribute(s) from its operand.") def __init__(self, *attrs): self.attrs = [a.split(".") for a in attrs] def _fetchattr(self, obj, attrnames): for name in attrnames: obj = _type(obj).getattr(obj, name) return obj def __call__(self, obj): if len(self.attrs) == 1: return self._fetchattr(obj, self.attrs[0]) return [self._fetchattr(obj, a) for a in self.attrs] class ItemGetter: ul4_type = InstantiableType("operator", "itemgetter", "Return a callable object that fetches the given items(s) from its operand.") def __init__(self, *items): self.itemgetter = operator.itemgetter(*items) def __call__(self, obj): return self.itemgetter(obj) ### ### Node classes for the abstract syntax tree ###
[docs] class AST: """ Base class for all UL4 syntax tree nodes. """ ul4_type = Type("ul4") # Set of attributes available to UL4 templates ul4_attrs = {"type", "template", "pos", "startpos", "startline", "startcol", "source", "startsource", "fullsource", "sourceprefix", "sourcesuffix", "startsourceprefix", "startsourcesuffix", "stopsourceprefix", "stopsourcesuffix"} def __init__(self, template=None, startpos=None): # ``template`` references the :class:`Template` object of which # ``self`` is a part. This mean that for a :class:`Template` object ``t`` # (which is an :class:`AST` object) ``t.template is t`` is true. self.template = template self._startpos = startpos self._startline = None self._startcol = None self._stoppos = None self._stopline = None self._stopcol = None @property def startpos(self): return self._startpos @startpos.setter def startpos(self, pos): self._startpos = pos self._startline = None self._startcol = None @property def startline(self): if self._startline is None: (self._startline, self._startcol) = _linecol(self.template._fullsource, self._startpos.start) return self._startline @property def startcol(self): if self._startcol is None: (self._startline, self._startcol) = _linecol(self.template._fullsource, self._startpos.start) return self._startcol @property def startsource(self): return self.template._fullsource[self._startpos] @property def sourceprefix(self): return _sourceprefix(self.template._fullsource, self._startpos.start) @property def sourcesuffix(self): return _sourcesuffix(self.template._fullsource, self._stoppos.stop if self._stoppos is not None else self._startpos.stop) startsourceprefix = sourceprefix @property def startsourcesuffix(self): return _sourcesuffix(self.template._fullsource, self._startpos.stop) @property def stopsourceprefix(self): return _sourceprefix(self.template._fullsource, self._stoppos.start) if self._stoppos is not None else None @property def stopsourcesuffix(self): return _sourcesuffix(self.template._fullsource, self._stoppos.stop) if self._stoppos is not None else None @property def stoppos(self): return self._stoppos @stoppos.setter def stoppos(self, pos): self._stoppos = pos self._stopline = None self._stopcol = None @property def stopline(self): if self._stopline is None: (self._stopline, self._stopcol) = _linecol(self.template._fullsource, self._stoppos.stop) return self._stopline @property def stopcol(self): if self._stopcol is None: (self._stopline, self._stopcol) = _linecol(self.template._fullsource, self._stoppos.stop) return self._stopcol @property def stopsource(self): return self.template._fullsource[self._stoppos] @property def pos(self): return self._startpos if self._stoppos is None else slice(self._startpos.start, self._stoppos.stop) @property def source(self): return self.template._fullsource[self.pos] @property def fullsource(self): return self.template._fullsource def __repr__(self): parts = [f"<{self.__class__.__module__}.{self.__class__.__qualname__}"] pos = self.pos parts.append(f"(offset {_offset(pos)}; line {self.startline:,}; col {self.startcol:,})") parts.extend(self._repr()) parts.append(f"at {id(self):#x}>") return " ".join(parts) def _repr(self): yield from () def _repr_pretty_(self, p, cycle): prefix = f"<{self.__class__.__module__}.{self.__class__.__qualname__}" pos = self.pos prefix += f" (offset {_offset(pos)}; line {self.startline:,}; col {self.startcol:,})" suffix = f"at {id(self):#x}" if cycle: p.text(f"{prefix} ... {suffix}>") else: with p.group(4, prefix, ">"): self._repr_pretty(p) p.breakable() p.text(suffix) def _repr_pretty(self, p): pass def __str__(self): # This uses :meth:`_str`, which is a generator and may output: # :const:`None`, which means: "add a line feed and an indentation here" # an int, which means: add the int to the indentation level # a string, which means: add this string to the output v = [] level = 0 needlf = False for code in self._str(): if code is None: needlf = True elif isinstance(code, int): level += code else: if needlf: v.append("\n") v.append(level*"\t") needlf = False v.append(code) if needlf: v.append("\n") return "".join(v)
[docs] def eval(self, context): """ This evaluates the node. For most nodes this is a normal function that returns the result of evaluating the node. (For these nodes the class attribute ``output`` is ``False``.). For nodes that produce output (like literal text, :class:`PrintAST`, :class:`PrintXAST` or :class:`RenderAST` etc.) it is a generator which yields the text output of the node. For blocks (which might contain nodes which produce output) this is also a generator. (For these nodes the class attribute ``output`` is ``True``.) """ pass
def ul4ondump(self, encoder): encoder.dump(self.template) _dumpslice(encoder, self._startpos) def ul4onload(self, decoder): self.template = decoder.load() self.startpos = _loadslice(decoder) self.stoppos = None
[docs] @register("text") class TextAST(AST): """ AST node for literal text (i.e. the stuff between tags). Attributes are: ``text`` : :class:`str` The text """ ul4_type = Type("ul4") ul4_attrs = AST.ul4_attrs.union({"text"}) def __init__(self, template=None, source="", startpos=0, stoppos=0): super().__init__(template, slice(startpos, stoppos)) self.text = source[startpos:stoppos] def _repr(self): yield repr(self.text) def _repr_pretty(self, p): p.breakable() p.text("text=") p.pretty(self.text) def _str(self): yield f"text {self.text!r}" def eval(self, context): context.write(self.text) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.text) def ul4onload(self, decoder): super().ul4onload(decoder) self.text = decoder.load()
[docs] @register("indent") class IndentAST(TextAST): """ AST node for literal text that is an indentation at the start of the line. Attributes are: ``text`` : :class:`str` The indentation text (i.e. a string that consists solely of whitespace). """ ul4_type = Type("ul4") # We don't define a setter, because the template should *not* be able to # set this attribute. However the attribute *will* be set by the code # compiling the template def _settext(self, text): self.text = text def _str(self): yield f"indent {self.text!r}" def eval(self, context): for indent in context.indents: context.write(indent) context.write(self.text)
[docs] @register("lineend") class LineEndAST(TextAST): r""" AST node for literal text that is the end of a line. Attributes are: ``text`` : :class:`str` The text of the linefeed (i.e. ``"\n"`` or ``"\r\n"``). """ ul4_type = Type("ul4") def _str(self): yield f"lineend {self.text!r}"
[docs] class Tag(AST): """ A :class:`Tag` object is the location of a template tag in a template. """ ul4_type = Type("ul4") def __init__(self, template=None, tag=None, tagpos=None, codepos=None): super().__init__(template, tagpos) self.tag = tag self.codepos = codepos def _repr(self): yield repr(self.source) def _repr_pretty(self, p): p.breakable() p.text("source=") p.pretty(self.source) def __str__(self): return f"{self.source!r} (offset {_offset(self.startpos)}; line {self.startline:,}; col {self.startcol:,})" @property def code(self): return self.template._fullsource[self.codepos]
[docs] class CodeAST(AST): """ The base class of all AST nodes that are not literal text. These nodes appear inside a :class:`Tag`. """ ul4_type = Type("ul4") def _str(self): yield " ".join(self.source.splitlines(False))
[docs] @register("const") class ConstAST(CodeAST): """ AST node for a constant value. Attributes are: ``value`` The constant to be loaded. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"value"}) def __init__(self, template=None, startpos=None, value=None): super().__init__(template, startpos) self.value = value def eval(self, context): # We don't need a decorator, because this can't fail anyway. return self.value def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.value = decoder.load() def _repr(self): yield repr(self.value) def _repr_pretty(self, p): p.breakable() p.text("value=") p.pretty(self.value)
### AST nodes for items in list, set and dict "literals"
[docs] @register("seqitem") class SeqItemAST(CodeAST): """ AST node for an item in a list/set "literal" (e.g. ``{x, y}`` or ``[x, y]``) Attributes are: ``value`` : :class:`AST` The list/set item (``x`` and ``y`` in the above examples). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"value"}) def __init__(self, template=None, startpos=None, value=None): super().__init__(template, startpos) self.value = value def _repr(self): yield f"{self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("value=") p.pretty(self.value) @_handleeval def eval_list(self, context, result): result.append(self.value.eval(context)) @_handleeval def eval_set(self, context, result): result.add(self.value.eval(context)) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.value = decoder.load()
[docs] @register("unpackseqitem") class UnpackSeqItemAST(CodeAST): """ AST node for an ``*`` unpacking expression in a list/set "literal" (e.g. the ``y`` in ``{x, *y}`` or ``[x, *y]``) Attributes are: ``value`` : :class:`AST` The item to be unpacked into list/set items (``y`` in the above examples). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"value"}) def __init__(self, template=None, startpos=None, value=None): super().__init__(template, startpos) self.value = value def _repr(self): yield f"*{self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("value=") p.pretty(self.value) @_handleeval def eval_list(self, context, result): # We're updating the result list here to get a proper location when the ``*`` argument isn't iterable for item in self.value.eval(context): result.append(item) @_handleeval def eval_set(self, context, result): # We're updating the result set here to get a proper location when the ``*`` argument isn't iterable for item in self.value.eval(context): result.add(item) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.value = decoder.load()
[docs] @register("dictitem") class DictItemAST(CodeAST): """ AST node for a dictionary entry in a dict expression (:class:`DictAST`). Attributes are: ``key`` : :class:`AST` The key of the entry. ``value`` : :class:`AST` The value of the entry. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"key", "value"}) def __init__(self, template=None, startpos=None, key=None, value=None): super().__init__(template, startpos) self.key = key self.value = value def _repr(self): yield f"{self.key!r}={self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("key=") p.pretty(self.key) p.breakable("") p.text("value=") p.pretty(self.value) @_handleeval def eval_dict(self, context, result): key = self.key.eval(context) value = self.value.eval(context) result[key] = value def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.key) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.key = decoder.load() self.value = decoder.load()
[docs] @register("unpackdictitem") class UnpackDictItemAST(CodeAST): """ AST node for ``**`` unpacking expressions in dict "literal" (e.g. the ``**u`` in ``{k: v, **u}``). Attributes are: ``item`` : :class:`AST` The argument that must evaluate to a dictionary or an iterable of (key, value) pairs. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item"}) def __init__(self, template=None, startpos=None, item=None): super().__init__(template, startpos) self.item = item def _repr(self): yield f"**{self.item!r}" def _repr_pretty(self, p): p.breakable() p.text("item=") p.pretty(self.item) @_handleeval def eval_dict(self, context, result): result.update(self.item.eval(context)) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load()
### AST nodes for call arguments
[docs] @register("posarg") class PositionalArgumentAST(CodeAST): """ AST node for a positional argument. (e.g. the ``x`` in ``f(x)``). Attributes are: ``value`` : :class:`AST` The value of the argument (``x`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"value"}) def __init__(self, template=None, startpos=None, value=None): super().__init__(template, startpos) self.value = value def _repr(self): yield f"{self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("value=") p.pretty(self.value) def append(self, call): for arg in call.args: if isinstance(arg, KeywordArgumentAST): raise SyntaxError("positional argument follows keyword argument") elif isinstance(arg, UnpackDictArgumentAST): raise SyntaxError("positional argument follows keyword argument unpacking") call.args.append(self) @_handleeval def eval_call(self, context, args, kwargs): args.append(self.value.eval(context)) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.value = decoder.load()
[docs] @register("keywordarg") class KeywordArgumentAST(CodeAST): """ AST node for a keyword argument in a :class:`CallAST` (e.g. the ``x=y`` in the function call ``f(x=y)``). Attributes are: ``name`` : :class:`str` The keyword argument name (``"x"`` in the above example). ``value`` : :class:`AST` The keyword argument value (``y`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"name", "value"}) def __init__(self, template=None, startpos=None, name=None, value=None): super().__init__(template, startpos) self.name = name self.value = value def _repr(self): yield f"{self.name}={self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("name=") p.pretty(self.name) p.breakable("") p.text("value=") p.pretty(self.value) def append(self, call): call.args.append(self) @_handleeval def eval_call(self, context, args, kwargs): if self.name in kwargs: raise SyntaxError(f"duplicate keyword argument {self.name!r}") kwargs[self.name] = self.value.eval(context) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.name) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.name = decoder.load() self.value = decoder.load()
[docs] @register("unpacklistarg") class UnpackListArgumentAST(CodeAST): """ AST node for an ``*`` unpacking expressions in a :class:`CallAST` (e.g. the ``*x`` in ``f(*x)``). Attributes are: ``item`` : :class:`AST` The argument that must evaluate an iterable. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item"}) def __init__(self, template=None, startpos=None, item=None): super().__init__(template, startpos) self.item = item def _repr(self): yield f"*{self.item!r}" def _repr_pretty(self, p): p.breakable() p.text("item=") p.pretty(self.item) def append(self, call): for arg in call.args: if isinstance(arg, UnpackDictArgumentAST): raise SyntaxError("iterable argument unpacking follows keyword argument unpacking") call.args.append(self) @_handleeval def eval_call(self, context, args, kwargs): for item in self.item.eval(context): args.append(item) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load()
[docs] @register("unpackdictarg") class UnpackDictArgumentAST(CodeAST): """ AST node for an ``**`` unpacking expressions in a :class:`CallAST` (e.g. the ``**x`` in ``f(**x)``). Attributes are: ``item`` : :class:`AST` The argument that must evaluate to a dictionary or an iterable of (key, value) pairs. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item"}) def __init__(self, template=None, startpos=None, item=None): super().__init__(template, startpos) self.item = item def _repr(self): yield f"**{self.item!r}" def _repr_pretty(self, p): p.breakable() p.text("item=") p.pretty(self.item) def append(self, call): call.args.append(self) @_handleeval def eval_call(self, context, args, kwargs): item = self.item.eval(context) if hasattr(item, "keys"): for key in item: if key in kwargs: raise SyntaxError(f"duplicate keyword argument {key!r}") kwargs[key] = item[key] else: for (key, value) in item: if key in kwargs: raise SyntaxError(f"duplicate keyword argument {key!r}") kwargs[key] = value def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load()
[docs] @register("list") class ListAST(CodeAST): """ AST node for creating a list object (e.g. ``[x, y, *z]``). Attributes are: ``items`` : :class:`list` The items that will be put into the newly created list as a list of :class:`SeqItemAST` (``x`` and ``y`` in the above example) and :class:`UnpackSeqItemAST` objects (``z`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"items"}) def __init__(self, template=None, startpos=None, *items): super().__init__(template, startpos) self.items = list(items) def _repr(self): yield f"with {len(self.items):,} items" def _repr_pretty(self, p): for item in self.items: p.breakable() p.pretty(item) @_handleeval def eval(self, context): result = [] for item in self.items: item.eval_list(context, result) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.items) def ul4onload(self, decoder): super().ul4onload(decoder) self.items = decoder.load()
[docs] @register("listcomp") class ListComprehensionAST(CodeAST): """ AST node for a list comprehension (e.g. ``[v for (a, b) in w if c]``. Attributes are: ``item`` : :class:`AST` The expression for the item in the newly created list (``v`` in the above example). ``varname`` : nested :class:`tuple` of :class:`VarAST` objects The loop variable (or variables) (``a`` and ``b`` in the above example). ``container`` : :class:`AST` The container or iterable object over which to loop (``w`` in the above example). ``condition`` : :class:`AST` or ``None`` The condition (as an :class:`AST` object if there is one, or ``None`` if there is not) (``c`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item", "varname", "container", "condition"}) def __init__(self, template=None, startpos=None, item=None, varname=None, container=None, condition=None): super().__init__(template, startpos) self.item = item self.varname = varname self.container = container self.condition = condition def _repr(self): yield f"item={self.item!r}" yield f"varname={self.varname!r}" yield f"container={self.container!r}" if self.container is not None: yield f"condition={self.condition!r}" def _repr_pretty(self, p): p.breakable("") p.text("item=") p.pretty(self.item) p.text(",") p.breakable() p.text("varname=") p.pretty(self.varname) p.text(",") p.breakable() p.text("container=") p.pretty(self.container) if self.condition is not None: p.text(",") p.breakable() p.text("condition=") p.pretty(self.condition) @_handleeval def eval(self, context): container = self.container.eval(context) with context.chainvars(): # Don't let loop variables leak into the surrounding scope result = [] for item in container: for (lvalue, value) in _unpackvar(self.varname, item): lvalue.evalset(context, value) if self.condition is None or self.condition.eval(context): result.append(self.item.eval(context)) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) encoder.dump(self.varname) encoder.dump(self.container) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load() self.varname = decoder.load() self.container = decoder.load() self.condition = decoder.load()
[docs] @register("set") class SetAST(CodeAST): """ AST node for creating a set object (e.g. ``{x, y, *z}``. Attributes are: ``items`` : :class:`list` The items that will be put into the newly created set as a list of :class:`SeqItemAST` (``x`` and ``y`` in the above example) and :class:`UnpackSeqItemAST` objects (``z`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"items"}) def __init__(self, template=None, startpos=None, *items): super().__init__(template, startpos) self.items = list(items) def _repr(self): yield f"with {len(self.items):,} items" def _repr_pretty(self, p): for item in self.items: p.breakable() p.pretty(item) @_handleeval def eval(self, context): result = set() for item in self.items: item.eval_set(context, result) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.items) def ul4onload(self, decoder): super().ul4onload(decoder) self.items = decoder.load()
[docs] @register("setcomp") class SetComprehensionAST(CodeAST): """ AST node for a set comprehension (e.g. ``{v for (a, b) in w if c}``. Attributes are: ``item`` : :class:`AST` The expression for the item in the newly created set (``v`` in the above example). ``varname`` : nested :class:`tuple` of :class:`VarAST` objects The loop variable (or variables) (``a`` and ``b`` in the above example). ``container`` : :class:`AST` The container or iterable object over which to loop (``w`` in the above example). ``condition`` : :class:`AST` or ``None`` The condition (as an :class:`AST` object if there is one, or ``None`` if there is not) (``c`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item", "varname", "container", "condition"}) def __init__(self, template=None, startpos=None, item=None, varname=None, container=None, condition=None): super().__init__(template, startpos) self.item = item self.varname = varname self.container = container self.condition = condition def _repr(self): yield f"item={self.item!r}" yield f"varname={self.varname!r}" yield f"container={self.container!r}" if self.container is not None: yield f"condition={self.condition!r}" def _repr_pretty(self, p): p.breakable("") p.text("item=") p.pretty(self.item) p.text(",") p.breakable() p.text("varname=") p.pretty(self.varname) p.text(",") p.breakable() p.text("container=") p.pretty(self.container) if self.condition is not None: p.text(",") p.breakable() p.text("condition=") p.pretty(self.condition) @_handleeval def eval(self, context): container = self.container.eval(context) with context.chainvars(): # Don't let loop variables leak into the surrounding scope result = set() for item in container: for (lvalue, value) in _unpackvar(self.varname, item): lvalue.evalset(context, value) if self.condition is None or self.condition.eval(context): result.add(self.item.eval(context)) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) encoder.dump(self.varname) encoder.dump(self.container) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load() self.varname = decoder.load() self.container = decoder.load() self.condition = decoder.load()
[docs] @register("dict") class DictAST(CodeAST): """ AST node for creating a dict object (e.g. `{k: v, **u}`. Attributes are: ``items`` : :class:`list` The items that will be put into the newly created dictionary as a list of :class:`DictItemAST` (for ``k`` and ``v`` in the above example) and :class:`UnpackDictItemAST` objects (for ``u`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"items"}) def __init__(self, template=None, startpos=None, *items): super().__init__(template, startpos) self.items = list(items) def _repr(self): yield f"with {len(self.items):,} items" def _repr_pretty(self, p): for item in self.items: p.breakable() p.pretty(item) @_handleeval def eval(self, context): result = {} for item in self.items: item.eval_dict(context, result) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.items) def ul4onload(self, decoder): super().ul4onload(decoder) self.items = decoder.load()
[docs] @register("dictcomp") class DictComprehensionAST(CodeAST): """ AST node for a dictionary comprehension (e.g. ``{k: v for (a, b) in w if c}``. Attributes are: ``key`` : :class:`AST` The expression for the keys in the newly created dictionary (``k`` in the above example). ``value`` : :class:`AST` The expression for the values in the newly created dictionary (``v`` in the above example). ``varname`` : nested :class:`tuple` of :class:`VarAST` objects The loop variable (or variables) (``a`` and ``b`` in the above example). ``container`` : :class:`AST` The container or iterable object over which to loop (``w`` in the above example). ``condition`` : :class:`AST` or ``None`` The condition (as an :class:`AST` object if there is one, or ``None`` if there is not) (``c`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"key", "value", "varname", "container", "condition"}) def __init__(self, template=None, startpos=None, key=None, value=None, varname=None, container=None, condition=None): super().__init__(template, startpos) self.key = key self.value = value self.varname = varname self.container = container self.condition = condition def _repr(self): yield f"key={self.key!r}" yield f"value={self.value!r}" yield f"varname={self.varname!r}" yield f"container={self.container!r}" if self.container is not None: yield f"condition={self.condition!r}" def _repr_pretty(self, p): p.breakable() p.text("key=") p.pretty(self.key) p.breakable() p.text("value=") p.pretty(self.value) p.breakable() p.text("varname=") p.pretty(self.varname) p.breakable() p.text("container=") p.pretty(self.container) if self.condition is not None: p.breakable() p.text("condition=") p.pretty(self.condition) @_handleeval def eval(self, context): container = self.container.eval(context) with context.chainvars(): # Don't let loop variables leak into the surrounding scope result = {} for item in container: for (lvalue, value) in _unpackvar(self.varname, item): lvalue.evalset(context, value) if self.condition is None or self.condition.eval(context): result[self.key.eval(context)] = self.value.eval(context) return result def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.key) encoder.dump(self.value) encoder.dump(self.varname) encoder.dump(self.container) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.key = decoder.load() self.value = decoder.load() self.varname = decoder.load() self.container = decoder.load() self.condition = decoder.load()
[docs] @register("genexpr") class GeneratorExpressionAST(CodeAST): """ AST node for a generator expression (e.g. ``(x for (a, b) in w if c)``). Attributes are: ``item`` : :class:`AST` An expression for the item that looping over the generator expression will produce (``x`` in the above example). ``varname`` : nested :class:`tuple` of :class:`VarAST` objects The loop variable (or variables) (``a`` and ``b`` in the above example). ``container`` : :class:`AST` The container or iterable object over which to loop (``w`` in the above example). ``condition`` : :class:`AST` or ``None`` The condition (as an :class:`AST` object if there is one, or ``None`` if there is not) (``c`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"item", "varname", "container", "condition"}) def __init__(self, template=None, startpos=None, item=None, varname=None, container=None, condition=None): super().__init__(template, startpos) self.item = item self.varname = varname self.container = container self.condition = condition def _repr(self): yield f"item={self.item!r}" yield f"varname={self.varname!r}" yield f"container={self.container!r}" if self.container is not None: yield f"condition={self.condition!r}" def _repr_pretty(self, p): p.breakable() p.text("item=") p.pretty(self.item) p.breakable() p.text("varname=") p.pretty(self.varname) p.breakable() p.text("container=") p.pretty(self.container) if self.condition is not None: p.breakable() p.text("condition=") p.pretty(self.condition) def eval(self, context): container = self.container.eval(context) try: with context.chainvars(): # Don't let loop variables leak into the surrounding scope for item in container: for (lvalue, value) in _unpackvar(self.varname, item): lvalue.evalset(context, value) if self.condition is None or self.condition.eval(context): yield self.item.eval(context) except LocationError: raise except Exception as exc: _decorateexception(exc, self) raise def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.item) encoder.dump(self.varname) encoder.dump(self.container) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.item = decoder.load() self.varname = decoder.load() self.container = decoder.load() self.condition = decoder.load()
[docs] @register("var") class VarAST(CodeAST): """ AST node for getting a variable. Attributes are: ``name`` : :class:`str` The name of the variable. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"name"}) def __init__(self, template=None, startpos=None, name=None): super().__init__(template, startpos) self.name = name def _repr(self): yield repr(self.name) def _repr_pretty(self, p): p.breakable() p.text("name=") p.pretty(self.name) @_handleeval def eval(self, context): try: return context.vars[self.name] except KeyError: return UndefinedVariable(self.name) @_handleeval def evalset(self, context, value): context.vars[self.name] = value @_handleeval def evalmodify(self, context, operator, value): context.vars[self.name] = operator.evalfoldaug(context.vars[self.name], value) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.name) def ul4onload(self, decoder): super().ul4onload(decoder) self.name = decoder.load()
[docs] class BlockAST(CodeAST): """ Base class for all AST nodes that are blocks. A block contains a sequence of AST nodes that are executed sequencially. A block may execute its content zero (e.g. an ``<?if?>`` block) or more times (e.g. a ``<?for?>`` block). Attributes are: ``content`` : :class:`list` of :class:`AST` objects The content of the block. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"stoppos", "stopline", "stopcol", "stopsource", "content"}) def __init__(self, template=None, startpos=None, stoppos=None): super().__init__(template, startpos) self._stoppos = stoppos self._stopline = None self._stopcol = None self.content = [] def append(self, item): self.content.append(item) def _pop_trailing_indent(self): if self.content and isinstance(self.content[-1], IndentAST): return self.content.pop() else: return None def finish(self, endtag): self.stoppos = endtag.startpos def _str(self): if self.content: for node in self.content: yield from node._str() yield None else: yield "pass" yield None @_handleeval def eval(self, context): for node in self.content: node.eval(context) def ul4ondump(self, encoder): super().ul4ondump(encoder) _dumpslice(encoder, self.stoppos) encoder.dump(self.content) def ul4onload(self, decoder): super().ul4onload(decoder) self.stoppos = _loadslice(decoder) self.content = decoder.load()
[docs] @register("condblock") class ConditionalBlocksAST(BlockAST): r""" AST node for a conditional ``<?if?>/<?elif?>/<?else?>`` block. Attributes are: ``content`` : :class:`list` The content of the :class:`ConditionalBlocksAST` block is one :class:`IfBlockAST` followed by zero or more :class:`ElIfBlockAST`\s followed by zero or one :class:`ElseBlockAST`. """ ul4_type = Type("ul4") def __init__(self, template=None, startpos=None, stoppos=None, condition=None): super().__init__(template, startpos, stoppos) if condition is not None: self.newblock(IfBlockAST(template, startpos, None, condition)) def _repr_pretty(self, p): p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def append(self, item): self.content[-1].append(item) def finish(self, endtag): super().finish(endtag) if self.content: self.content[-1].stoppos = slice(endtag.startpos.start, endtag.startpos.start) def _pop_trailing_indent(self): if self.content: return self.content[-1]._pop_trailing_indent() else: return None def newblock(self, block): if self.content: self.content[-1].stoppos = slice(block.startpos.start, block.startpos.start) self.content.append(block) def _str(self): for node in self.content: yield from node._str() @_handleeval def eval(self, context): for node in self.content: if isinstance(node, ElseBlockAST) or node.condition.eval(context): node.eval(context) break
[docs] @register("ifblock") class IfBlockAST(BlockAST): """ AST node for an ``<?if?>`` block in an ``<?if?>/<?elif?>/<?else?>`` block. Attributes are: ``condition`` : :class:`AST` The condition in the ``<?if?>`` block. ``content`` : :class:`list` of :class:`AST` objects The content of the ``<?if?>`` block. """ ul4_type = Type("ul4") ul4_attrs = BlockAST.ul4_attrs.union({"condition"}) def __init__(self, template=None, startpos=None, stoppos=None, condition=None): super().__init__(template, startpos, stoppos) self.condition = condition def _repr(self): yield f" condition={self.condition!r}" def _repr_pretty(self, p): p.breakable() p.text("condition=") p.pretty(self.condition) p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def _str(self): yield "if " yield from CodeAST._str(self) yield ":" yield None yield +1 yield from BlockAST._str(self) yield -1 def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.condition = decoder.load()
[docs] @register("elifblock") class ElIfBlockAST(BlockAST): """ AST node for an ``<?elif?>`` block. Attributes are: ``condition`` : :class:`AST` The condition in the ``<?elif?>`` block. ``content`` : :class:`list` of :class:`AST` objects The content of the ``<?elif?>`` block. """ ul4_type = Type("ul4") ul4_attrs = BlockAST.ul4_attrs.union({"condition"}) def __init__(self, template=None, startpos=None, stoppos=None, condition=None): super().__init__(template, startpos, stoppos) self.condition = condition def _repr(self): yield f" condition={self.condition!r}" def _repr_pretty(self, p): p.breakable() p.text("condition=") p.pretty(self.condition) p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def _str(self): yield "elif " yield from CodeAST._str(self) yield ":" yield None yield +1 yield from super()._str() yield -1 def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.condition = decoder.load()
[docs] @register("elseblock") class ElseBlockAST(BlockAST): """ AST node for an ``<?else?>`` block. Attributes are: ``content`` : :class:`list` of :class:`AST` objects The content of the ``<?else?>`` block. """ ul4_type = Type("ul4") def _repr_pretty(self, p): p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def _str(self): yield "else:" yield None yield +1 yield from super()._str() yield -1
[docs] @register("forblock") class ForBlockAST(BlockAST): """ AST node for a ``<?for?>`` loop. For example :: <?for (a, b) in w?> body <?end for?> Attributes are: ``varname`` : nested :class:`tuple` of :class:`VarAST` objects The loop variable (or variables) (``a`` and ``b`` in the above example). ``container`` : :class:`AST` The container or iterable object over which to loop (``w`` in the above example). ``content`` : :class:`list` of :class:`AST` objects The loop body (``body`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = BlockAST.ul4_attrs.union({"varname", "container"}) def __init__(self, template=None, startpos=None, stoppos=None, varname=None, container=None): super().__init__(template, startpos, stoppos) self.varname = varname self.container = container def _repr(self): yield f"varname={self.varname!r}" yield f"container={self.container!r}" def _repr_pretty(self, p): p.breakable() p.text("varname=") p.pretty(self.varname) p.breakable() p.text("container=") p.pretty(self.container) p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.varname) encoder.dump(self.container) def ul4onload(self, decoder): super().ul4onload(decoder) self.varname = decoder.load() self.container = decoder.load() def _str(self): yield "for " yield from CodeAST._str(self) yield ":" yield None yield +1 yield from super()._str() yield -1 @_handleeval def eval(self, context): container = self.container.eval(context) for item in container: for (lvalue, value) in _unpackvar(self.varname, item): lvalue.evalset(context, value) try: super().eval(context) except BreakException: break except ContinueException: pass
[docs] @register("whileblock") class WhileBlockAST(BlockAST): """ AST node for a ``<?while?>`` loop. For example :: <?while c?> body <?end for?> Attributes are: ``condition`` : :class:`AST` The condition which must be true to continue executing the loops booy (``c`` in the above example). ``content`` : :class:`list` of :class:`AST` objects The loop body (``body`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = BlockAST.ul4_attrs.union({"condition"}) def __init__(self, template=None, startpos=None, stoppos=None, condition=None): super().__init__(template, startpos, stoppos) self.condition = condition def _repr(self): yield f"condition={self.condition!r}" def _repr_pretty(self, p): p.breakable() p.text("condition=") p.pretty(self.condition) p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.condition) def ul4onload(self, decoder): super().ul4onload(decoder) self.condition = decoder.load() def _str(self): yield "while " yield from CodeAST._str(self) yield ":" yield None yield +1 yield from super()._str() yield -1 @_handleeval def eval(self, context): while 1: condition = self.condition.eval(context) if not condition: break try: super().eval(context) except BreakException: break except ContinueException: pass
[docs] @register("break") class BreakAST(CodeAST): """ AST node for a ``<?break?>`` tag inside a ``<?for?>`` loop. """ ul4_type = Type("ul4") def _str(self): yield "break" @_handleeval def eval(self, context): raise BreakException()
[docs] @register("continue") class ContinueAST(CodeAST): """ AST node for a ``<?continue?>`` tag inside a ``<?for?>`` block. """ ul4_type = Type("ul4") def _str(self): yield "continue" @_handleeval def eval(self, context): raise ContinueException()
[docs] @register("attr") class AttrAST(CodeAST): """ AST node for an expression that gets or sets an attribute of an object. (e.g. ``x.y``). Attributes are: ``obj`` : :class:`AST` The object from which to get the attribute (``x`` in the above example); ``attrname`` : :class:`str` The name of the attribute (``"y"`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = AST.ul4_attrs.union({"obj", "attrname"}) def __init__(self, template=None, startpos=None, obj=None, attrname=None): super().__init__(template, startpos) self.obj = obj self.attrname = attrname def _repr(self): yield f"obj={self.obj!r}" yield f"attrname={self.attrname!r}" def _repr_pretty(self, p): p.breakable() p.text("obj=") p.pretty(self.obj) p.breakable() p.text("attrname=") p.pretty(self.attrname) @_handleeval def eval(self, context): obj = self.obj.eval(context) try: return _type(obj).getattr(obj, self.attrname) except AttributeError: return UndefinedKey(obj, self.attrname) @_handleeval def evalset(self, context, value): obj = self.obj.eval(context) _type(obj).setattr(obj, self.attrname, value) @_handleeval def evalmodify(self, context, operator, value): obj = self.obj.eval(context) t = _type(obj) oldvalue = t.getattr(obj, self.attrname) newvalue = operator.evalfoldaug(oldvalue, value) t.setattr(obj, self.attrname, newvalue) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.obj) encoder.dump(self.attrname) def ul4onload(self, decoder): super().ul4onload(decoder) self.obj = decoder.load() self.attrname = decoder.load()
[docs] @register("slice") class SliceAST(CodeAST): """ AST node for creating a slice object (used in ``obj[index1:index2]``). Attributes are: ``index1`` : :class:`AST` or ``None`` The start index (``index1`` in the above example). ``index2`` : :class:`AST` or ``None`` The stop index (``index2`` in the above example). ``index1`` and ``index2`` may also be :const:`None` (for missing slice indices, which default to 0 for the start index and the length of the sequence for the stop index). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"index1", "index2"}) def __init__(self, template=None, startpos=None, index1=None, index2=None): super().__init__(template, startpos) self.index1 = index1 self.index2 = index2 def _repr(self): if self.index1 is not None: yield f"index1={self.index1!r}" if self.index2 is not None: yield f"index2={self.index2!r}" def _repr_pretty(self, p): if self.index1 is not None: p.breakable() p.text("index1=") p.pretty(self.index1) if self.index2 is not None: p.breakable() p.text("index2=") p.pretty(self.index2) @_handleeval def eval(self, context): index1 = None if self.index1 is not None: index1 = self.index1.eval(context) index2 = None if self.index2 is not None: index2 = self.index2.eval(context) return slice(index1, index2) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.index1) encoder.dump(self.index2) def ul4onload(self, decoder): super().ul4onload(decoder) self.index1 = decoder.load() self.index2 = decoder.load()
[docs] class UnaryAST(CodeAST): """ Base class for all AST nodes implementing unary expressions (i.e. operators with one operand). Atttributes are: ``obj`` : :class:`AST` The operand of the unary operator. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"obj"}) def __init__(self, template=None, startpos=None, obj=None): super().__init__(template, startpos) self.obj = obj def _repr(self): yield repr(self.obj) def _repr_pretty(self, p): p.breakable() p.pretty(self.obj) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.obj) def ul4onload(self, decoder): super().ul4onload(decoder) self.obj = decoder.load() @_handleeval def eval(self, context): obj = self.obj.eval(context) return self.evalfold(obj) @classmethod def make(cls, tag, pos, obj): if isinstance(obj, ConstAST): try: result = cls.evalfold(obj.value) if not isinstance(result, Undefined): return ConstAST(tag, pos, result) except Exception: pass # If constant folding doesn't work, return the original AST return cls(tag, pos, obj)
[docs] @register("not") class NotAST(UnaryAST): """ AST node for a unary "not" expression (e.g. ``not x``). Attributes are: ``obj`` : :class:`AST` The operand (``x`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj): return not obj
[docs] @register("neg") class NegAST(UnaryAST): """ AST node for a unary negation expression (e.g. ``-x``). Attributes are: ``obj`` : :class:`AST` The operand (``x`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj): return -obj
[docs] @register("bitnot") class BitNotAST(UnaryAST): """ AST node for a bitwise unary "not" expression that returns its operand with its bits inverted (e.g. ``~x``). Attributes are: ``obj`` : :class:`AST` The operand (``x`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj): return ~obj
[docs] @register("print") class PrintAST(UnaryAST): """ AST node for a ``<?print?>`` tag (e.g. ``<?print x?>``). Attributes are: ``obj`` : :class:`AST` The object to be printed (``x`` in the above example). """ ul4_type = Type("ul4") def _str(self): yield "print " yield from super()._str() @_handleeval def eval(self, context): context.write(_str(self.obj.eval(context)))
[docs] @register("printx") class PrintXAST(UnaryAST): """ AST node for a ``<?printx?>`` tag (e.g. ``<?printx x?>``). Attributes are: ``obj`` : :class:`AST` The object to be printed (``x`` in the above example). """ ul4_type = Type("ul4") def _str(self): yield "printx " yield from super()._str() @_handleeval def eval(self, context): context.write(_xmlescape(self.obj.eval(context)))
[docs] @register("return") class ReturnAST(UnaryAST): """ AST node for a ``<?return?>`` tag (e.g. ``<?return x?>``). Attributes are: ``obj`` : :class:`AST` The operand (``x`` in the above example). """ ul4_type = Type("ul4") def _str(self): yield "return " yield from super()._str() @_handleeval def eval(self, context): value = self.obj.eval(context) raise ReturnException(value)
[docs] class BinaryAST(CodeAST): """ Base class for all UL4 AST nodes implementing binary expressions (i.e. operators with two operands). ``obj1`` : :class:`AST` The first operand. ``obj2`` : :class:`AST` The second operand. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"obj1", "obj2"}) def __init__(self, template=None, startpos=None, obj1=None, obj2=None): super().__init__(template, startpos) self.obj1 = obj1 self.obj2 = obj2 def _repr(self): yield repr(self.obj1) yield repr(self.obj2) def _repr_pretty(self, p): p.breakable() p.pretty(self.obj1) p.breakable() p.pretty(self.obj2) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.obj1) encoder.dump(self.obj2) def ul4onload(self, decoder): super().ul4onload(decoder) self.obj1 = decoder.load() self.obj2 = decoder.load() @_handleeval def eval(self, context): obj1 = self.obj1.eval(context) obj2 = self.obj2.eval(context) return self.evalfold(obj1, obj2) @classmethod def make(cls, tag, pos, obj1, obj2): if isinstance(obj1, ConstAST) and isinstance(obj2, ConstAST): try: result = cls.evalfold(obj1.value, obj2.value) if not isinstance(result, Undefined): return ConstAST(tag, pos, result) except Exception: pass # If constant folding doesn't work, return the original AST return cls(tag, pos, obj1, obj2)
[docs] @register("item") class ItemAST(BinaryAST): """ AST node for subscripting expression (e.g. ``x[y]``). Attributes are: ``obj1`` : :class:`AST` The container object, which must be a list, string or dict (``x`` in the above example). ``obj2`` : :class:`AST` The index/key object (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): try: return obj1[obj2] except KeyError: return UndefinedKey(obj1, obj2) @_handleeval def evalset(self, context, value): obj1 = self.obj1.eval(context) obj2 = self.obj2.eval(context) obj1[obj2] = value @_handleeval def evalmodify(self, context, operator, value): obj1 = self.obj1.eval(context) obj2 = self.obj2.eval(context) oldvalue = obj1[obj2] newvalue = operator.evalfoldaug(oldvalue, value) obj1[obj2] = newvalue
[docs] @register("is") class IsAST(BinaryAST): """ AST node for a binary ``is`` comparison expression (e.g. ``x is y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 is obj2
[docs] @register("isnot") class IsNotAST(BinaryAST): """ AST node for a binary ``is not`` comparison expression (e.g. ``x is not y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 is not obj2
[docs] @register("eq") class EQAST(BinaryAST): """ AST node for the binary equality comparison (e.g. ``x == y``. Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 == obj2
[docs] @register("ne") class NEAST(BinaryAST): """ AST node for a binary inequality comparison (e.g. ``x != y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 != obj2
[docs] @register("lt") class LTAST(BinaryAST): """ AST node for the binary "less than" comparison (e.g. ``x < y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 < obj2
[docs] @register("le") class LEAST(BinaryAST): """ AST node for the binary "less than or equal" comparison (e.g. ``x <= y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 <= obj2
[docs] @register("gt") class GTAST(BinaryAST): """ AST node for the binary "greater than" comparison (e.g. ``x > y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 > obj2
[docs] @register("ge") class GEAST(BinaryAST): """ AST node for the binary "greater than or equal" comparison (e.g. ``x >= y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 >= obj2
[docs] @register("contains") class ContainsAST(BinaryAST): """ AST node for the binary containment testing operator (e.g. ``x in y``). Attributes are: ``obj1`` : :class:`AST` The item/key object (``x`` in the above example). ``obj2`` : :class:`AST` The container object (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 in obj2
[docs] @register("notcontains") class NotContainsAST(BinaryAST): """ AST node for an inverted containment testing expression (e.g. ``x not in y``). Attributes are: ``obj1`` : :class:`AST` The item/key object (``x`` in the above example). ``obj2`` : :class:`AST` The container object (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 not in obj2
[docs] @register("add") class AddAST(BinaryAST): """ AST node for a binary addition expression (e.g. ``x + y``). Attributes are: ``obj1`` : :class:`AST` The left summand (``x`` in the above example). ``obj2`` : :class:`AST` The right summand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 + obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 += obj2 return obj1
[docs] @register("sub") class SubAST(BinaryAST): """ AST node for the binary subtraction expression (e.g. ``x - y``). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 - obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 -= obj2 return obj1
[docs] @register("mul") class MulAST(BinaryAST): """ AST node for the binary multiplication expression (e.g. ``x * y``). Attributes are: ``obj1`` : :class:`AST` The left factor (``x`` in the above example). ``obj2`` : :class:`AST` The right factor (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 * obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 *= obj2 return obj1
[docs] @register("floordiv") class FloorDivAST(BinaryAST): """ AST node for a binary truncating division expression (e.g. ``x // y``). Attributes are: ``obj1`` : :class:`AST` The dividend (``x`` in the above example). ``obj2`` : :class:`AST` The divisor (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 // obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 //= obj2 return obj1
[docs] @register("truediv") class TrueDivAST(BinaryAST): """ AST node for a binary true division expression (e.g. ``x / y``). Attributes are: ``obj1`` : :class:`AST` The dividend (``x`` in the above example). ``obj2`` : :class:`AST` The divisor (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 / obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 /= obj2 return obj1
[docs] @register("mod") class ModAST(BinaryAST): """ AST node for a binary modulo expression (e.g. ``x % y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 % obj2 @classmethod def evalfoldaug(cls, obj1, obj2): obj1 %= obj2 return obj1
[docs] @register("shiftleft") class ShiftLeftAST(BinaryAST): """ AST node for a bitwise left shift expression (e.g. ``x << y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 << obj2 if obj2 >= 0 else obj1 >> -obj2 @classmethod def evalfoldaug(cls, obj1, obj2): if obj2 >= 0: obj1 <<= obj2 else: obj1 >>= -obj2 return obj1
[docs] @register("shiftright") class ShiftRightAST(BinaryAST): """ AST node for a bitwise right shift expression (e.g. ``x >> y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): return obj1 >> obj2 if obj2 >= 0 else obj1 << -obj2 @classmethod def evalfoldaug(cls, obj1, obj2): if obj2 >= 0: obj1 >>= obj2 else: obj1 <<= -obj2 return obj1
[docs] @register("bitand") class BitAndAST(BinaryAST): """ AST node for a binary bitwise "and" expression (e.g ``x & y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) return obj1 & obj2 @classmethod def evalfoldaug(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) obj1 &= obj2 return obj1
[docs] @register("bitxor") class BitXOrAST(BinaryAST): """ AST node for the binary bitwise "exclusive or" expression (e.g. ``x ^ y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) return obj1 ^ obj2 @classmethod def evalfoldaug(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) obj1 ^= obj2 return obj1
[docs] @register("bitor") class BitOrAST(BinaryAST): """ AST node for a binary bitwise "or" expression (e.g. ``x | y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) return obj1 | obj2 @classmethod def evalfoldaug(cls, obj1, obj2): if isinstance(obj1, bool): obj1 = int(obj1) if isinstance(obj2, bool): obj2 = int(obj2) obj1 |= obj2 return obj1
[docs] @register("and") class AndAST(BinaryAST): """ AST node for the binary "and" expression (i.e. ``x and y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): # This is not called from ``eval``, as it doesn't short-circuit return obj1 and obj2 @_handleeval def eval(self, context): obj1 = self.obj1.eval(context) if not obj1: return obj1 return self.obj2.eval(context)
[docs] @register("or") class OrAST(BinaryAST): """ AST node for a binary "or" expression (e.g. ``x or y``). Attributes are: ``obj1`` : :class:`AST` The item/key object (``x`` in the above example). ``obj2`` : :class:`AST` The container object (``y`` in the above example). """ ul4_type = Type("ul4") @classmethod def evalfold(cls, obj1, obj2): # This is not called from ``eval``, as it doesn't short-circuit return obj1 or obj2 @_handleeval def eval(self, context): obj1 = self.obj1.eval(context) if obj1: return obj1 return self.obj2.eval(context)
[docs] @register("if") class IfAST(CodeAST): """ AST node for the ternary inline ``if/else`` operator (e.g. ``x if y else z``). Attributes are: ``objif`` : :class:`AST` The value of the ``if/else`` expression when the condition is true (``x`` in the above example). ``objcond`` : :class:`AST` The condition (``y`` in the above example). ``objelse`` : :class:`AST` The value of the ``if/else`` expression when the condition is false (``z`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"objif", "objcond", "objelse"}) def __init__(self, template=None, startpos=None, objif=None, objcond=None, objelse=None): super().__init__(template, startpos) self.objif = objif self.objcond = objcond self.objelse = objelse def _repr(self): yield f"objif={self.objif!r}" yield f"objcond={self.objcond!r}" yield f"objelse={self.objelse!r}" def _repr_pretty(self, p): p.breakable() p.pretty(self.objif) p.breakable() p.pretty(self.objcond) p.breakable() p.pretty(self.objelse) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.objif) encoder.dump(self.objcond) encoder.dump(self.objelse) def ul4onload(self, decoder): super().ul4onload(decoder) self.objif = decoder.load() self.objcond = decoder.load() self.objelse = decoder.load() @classmethod def make(cls, tag, pos, objif, objcond, objelse): if isinstance(objcond, ConstAST) and not isinstance(objcond.value, Undefined): return objif if objcond.value else objelse return cls(tag, pos, objif, objcond, objelse) @_handleeval def eval(self, context): if self.objcond.eval(context): return self.objif.eval(context) else: return self.objelse.eval(context)
[docs] class ChangeVarAST(CodeAST): """ Base class for all AST nodes that are assignment operators, i.e. that set or modify a variable/attribute or item. Attributes are: ``lvalue`` : :class:`AST` The left hand side, i.e. the value that will be modified. ``value`` : :class:`AST` The right hand side, the value that will be assigned or be used to modify the intial value. """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"lvalue", "value"}) def __init__(self, template=None, startpos=None, lvalue=None, value=None): super().__init__(template, startpos) self.lvalue = lvalue self.value = value def _repr(self): yield f"lvalue={self.lvalue!r}" yield f"value={self.value!r}" def _repr_pretty(self, p): p.breakable() p.text("lvalue=") p.pretty(self.lvalue) p.breakable() p.text("value=") p.pretty(self.value) def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.lvalue) encoder.dump(self.value) def ul4onload(self, decoder): super().ul4onload(decoder) self.lvalue = decoder.load() self.value = decoder.load()
[docs] @register("setvar") class SetVarAST(ChangeVarAST): """ AST node for setting a variable, attribute or item to a value (e.g. ``x = y``). ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalset(context, value)
[docs] @register("addvar") class AddVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that adds a value to a variable (e.g. ``x += y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, AddAST, value)
[docs] @register("subvar") class SubVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that subtracts a value from a variable/attribute/item. (e.g. ``x -= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, SubAST, value)
[docs] @register("mulvar") class MulVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a multiplication to its left operand. (e.g. ``x *= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, MulAST, value)
[docs] @register("floordivvar") class FloorDivVarAST(ChangeVarAST): """ AST node for augmented assignment expression that divides a variable by a value, truncating to an integer value (e.g. ``x //= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, FloorDivAST, value)
[docs] @register("truedivvar") class TrueDivVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a truncation division to its left operand. (e.g. ``x //= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, TrueDivAST, value)
[docs] @register("modvar") class ModVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a modulo expression to its left operand. (e.g. ``x %= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, ModAST, value)
[docs] @register("shiftleftvar") class ShiftLeftVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a "shift left" expression to its left operand. (e.g. ``x <<= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, ShiftLeftAST, value)
[docs] @register("shiftrightvar") class ShiftRightVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a "shift right" expression to its left operand. (e.g. ``x >>= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, ShiftRightAST, value)
[docs] @register("bitandvar") class BitAndVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a binary bitwise "and" expression to its left operand. (e.g. ``x &= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, BitAndAST, value)
[docs] @register("bitxorvar") class BitXOrVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a binary bitwise "exclusive or" expression to its left operand. (e.g. ``x ^= y``). Attributes are: ``obj1`` : :class:`AST` The left operand (``x`` in the above example). ``obj2`` : :class:`AST` The right operand (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, BitXOrAST, value)
[docs] @register("bitorvar") class BitOrVarAST(ChangeVarAST): """ AST node for an augmented assignment expression that assigns the result of a binary bitwise "or" expression to its left operand. (e.g. ``x |= y``). Attributes are: ``lvalue`` : :class:`AST` The left hand side (``x`` in the above example). ``value`` : :class:`AST` The right hand side (``y`` in the above example). """ ul4_type = Type("ul4") @_handleeval def eval(self, context): value = self.value.eval(context) for (lvalue, value) in _unpackvar(self.lvalue, value): lvalue.evalmodify(context, BitOrAST, value)
[docs] @register("call") class CallAST(CodeAST): """ AST node for calling an object (e.g. ``f(x, y)``). Attributes are: ``obj`` : :class:`AST` The object to be called (``f`` in the above example) (or rendered/printed in the subclass :class:`RenderAST` and its subclasses); ``args`` : :class:`list` The arguments to the call as a :class:`list` of :class:`PositionalArgumentAST`, :class:`KeywordArgumentAST`, :class:`UnpackListArgumentAST` or :class:`UnpackDictArgumentAST` objects (``x`` and ``y`` in the above example). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"obj", "args"}) def __init__(self, template=None, startpos=None, obj=None): super().__init__(template, startpos) self.obj = obj self.args = [] def _repr(self): yield f"obj={self.obj!r}" for arg in self.args: yield from arg._repr() def _repr_pretty(self, p): p.breakable() p.text("obj=") p.pretty(self.obj) for arg in self.args: p.breakable() p.pretty(arg) @staticmethod def _call(context, obj, args, kwargs): ul4_call = getattr(obj, "ul4_call", None) if callable(ul4_call): obj = ul4_call needscontext = getattr(obj, "ul4_context", False) if needscontext: return obj(context, *args, **kwargs) else: return obj(*args, **kwargs) def eval(self, context): obj = self.obj.eval(context) args = [] kwargs = {} for arg in self.args: arg.eval_call(context, args, kwargs) try: return self._call(context, obj, args, kwargs) except Exception as exc: if inspect.ismethod(obj): _decorateexception(exc, self, obj.__self__) else: _decorateexception(exc, self, obj) raise @_handleeval def evalset(self, context, value): raise TypeError("can't use = on call result") @_handleeval def evalmodify(self, context, operator, value): raise TypeError("augmented assigment not allowed for call result") def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.obj) encoder.dump(self.args) def ul4onload(self, decoder): super().ul4onload(decoder) self.obj = decoder.load() self.args = decoder.load()
[docs] @register("render") class RenderAST(CallAST): """ AST node for rendering a template (e.g. ``<?render t(x)?>``. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") ul4_attrs = CallAST.ul4_attrs.union({"indent"}) def __init__(self, template=None, startpos=None, obj=None): super().__init__(template, startpos, obj) self.indent = None # The indentation before this ``<?render?>``/``<?renderx?>`` tag, i.e. the sibling AST node before ``self`` def _repr(self): yield f"indent={self.indent!r}" yield f"obj={self.obj!r}" for arg in self.args: yield from arg._repr() def _repr_pretty(self, p): p.breakable() p.text("indent=") p.pretty(self.indent) p.breakable() p.text("obj=") p.pretty(self.obj) for arg in self.args: p.breakable() p.pretty(arg) def _evalobjargs(self, context): obj = self.obj.eval(context) args = [] kwargs = {} for arg in self.args: arg.eval_call(context, args, kwargs) return (obj, args, kwargs) def _renderobject(self, context, obj, args, kwargs): if self.indent is not None: context.indents.append(self.indent.text) needscontext = getattr(obj, "ul4_context", False) if needscontext: obj(context, *args, **kwargs) else: obj(*args, **kwargs) if self.indent is not None: context.indents.pop() def _renderxobject(self, context, obj, args, kwargs): with context.replacestream(XMLEscapeStream(context.stream)): self._renderobject(context, obj, args, kwargs) def _dontprintobject(self, context, obj): from ll import misc if isinstance(obj, Undefined): raise TypeError(f"{obj!r} can't be rendered") else: raise TypeError(f"{misc.format_class(obj)} object can't be rendered") def _printobject(self, context, obj): if self.indent: context.write(self.indent) context.write(_str(obj)) def _printxobject(self, context, obj): if self.indent: context.write(self.indent) context.write(_xmlescape(obj)) _real_renderobject = _renderobject _real_printobject = _dontprintobject def _render(self, context, obj, args, kwargs): try: ul4_render = getattr(obj, "ul4_render", None) if callable(ul4_render): self._real_renderobject(context, ul4_render, args, kwargs) else: self._real_printobject(context, obj) except Exception as exc: if inspect.ismethod(obj): _decorateexception(exc, self, obj.__self__) else: _decorateexception(exc, self, obj) raise def eval(self, context): # We always evaluate the arguments, even if the object turns out # to not be renderable afterwards (obj, args, kwargs) = self._evalobjargs(context) self._render(context, obj, args, kwargs) @_handleeval def evalset(self, context, value): raise TypeError("can't use = on call result") @_handleeval def evalmodify(self, context, operator, value): raise TypeError("augmented assigment not allowed for call result") def _str(self): yield self.type yield " " yield from super()._str() if self.indent is not None: yield f" with indent {self.indent.text!r}" def ul4ondump(self, encoder): super().ul4ondump(encoder) encoder.dump(self.indent) def ul4onload(self, decoder): super().ul4onload(decoder) self.indent = decoder.load()
[docs] @register("renderx") class RenderXAST(RenderAST): """ AST node for rendering a template and XML-escaping the output (e.g. ``<?renderx t(x)?>``. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") _real_renderobject = RenderAST._renderxobject
[docs] @register("render_or_print") class RenderOrPrintAST(RenderAST): """ AST node for rendering a template or printing an object. .. sourcecode:: html+ul4 <?render_or_print t(x)?> is equivalent to .. sourcecode:: html+ul4 <?if istemplate(t)?> <?render t(x)?> <?else?> <?print t?> <?end if?> except that even if ``t`` is not renderable, all argument in the call will be evaluated. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") _real_printobject = RenderAST._printobject
[docs] @register("render_or_printx") class RenderOrPrintXAST(RenderAST): """ AST node for rendering a template or printing an object (e.g. ``<?render_or_printx t(x)?>``. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") _real_printobject = RenderAST._printxobject
[docs] @register("renderx_or_print") class RenderXOrPrintAST(RenderAST): """ AST node for rendering a template or printing an object (e.g. ``<?renderx_or_print t(x)?>``. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") _real_renderobject = RenderAST._renderxobject _real_printobject = RenderAST._printobject
[docs] @register("renderx_or_printx") class RenderXOrPrintXAST(RenderAST): """ AST node for rendering a template or printing an object (e.g. ``<?renderx_or_printx t(x)?>``. For a list of attribute see :class:`CallAST`. """ ul4_type = Type("ul4") _real_renderobject = RenderAST._renderxobject _real_printobject = RenderAST._printxobject
[docs] @register("renderblock") class RenderBlockAST(RenderAST): """ AST node for rendering a template via a ``<?renderblock?>`` block and passing the content of the block as one additional keyword argument named ``content``. For example :: <?renderblock t(a, b)?> content <?end renderblock?> Attributes are: ``obj`` : :class:`AST` The object to be rendered (``t`` in the above example); ``args`` : :class:`list` The arguments to the call as a :class:`list` of :class:`PositionalArgumentAST`, :class:`KeywordArgumentAST`, :class:`UnpackListArgumentAST` or :class:`UnpackDictArgumentAST` objects (``a`` and ``b`` in the above example). ``content`` : :class:`list` of :class:`AST` objects The content of the ``<?renderblock?>`` tag (``content`` in the above example) that will be passed as a signatureless template as the keyword argument ``content`` to the object. """ ul4_type = Type("ul4") ul4_attrs = RenderAST.ul4_attrs.union({"stoppos", "stopline", "stopcol", "stopsource", "stopsourceprefix", "stopsourcesuffix", "content"}) def __init__(self, template=None, startpos=None, stoppos=None, obj=None): super().__init__(template, startpos, obj) self._stoppos = None self.content = None def append(self, item): self.content.content.append(item) def finish(self, endtag): self.stoppos = endtag.startpos self.content.startpos = slice(self.content.startpos.start, self.content.startpos.start) self.content.stoppos = slice(endtag.startpos.start, endtag.startpos.start) def _pop_trailing_indent(self): if self.content is not None: return self.content._pop_trailing_indent() else: return None def eval(self, context): (obj, args, kwargs) = self._evalobjargs(context) # Check that the argument ``content`` hasn't been specified yet if "content" in kwargs: raise TypeError(f"multiple values for keyword argument 'content'") kwargs["content"] = TemplateClosure(self.content, context, None) self._render(context, obj, args, kwargs) def _str(self): yield self.type yield " " yield from CodeAST._str(self) if self.indent is not None: yield f" with indent {self.indent.text!r}" yield ":" yield None yield +1 yield from BlockAST._str(self.content) yield -1 def ul4ondump(self, encoder): super().ul4ondump(encoder) _dumpslice(encoder, self._stoppos) encoder.dump(self.content) def ul4onload(self, decoder): super().ul4onload(decoder) self.stoppos = _loadslice(decoder) self.content = decoder.load()
[docs] @register("renderblocks") class RenderBlocksAST(RenderAST): """ AST node for rendering a template and passing additional arguments via nested variable definitions, e.g.:: <?renderblocks t(a, b)?> <?code x = 42?> <?def n?> ... <?end def?> <?end renderblocks?> Attributes are: ``obj`` : :class:`AST` The object to be rendered (``t`` in the above example); ``args`` : :class:`list` The arguments to the call as a :class:`list` of :class:`PositionalArgumentAST`, :class:`KeywordArgumentAST`, :class:`UnpackListArgumentAST` or :class:`UnpackDictArgumentAST` objects (``a`` and ``b`` in the above example). ``content`` : :class:`list` of :class:`AST` objects The content of the ``<?renderblocks?>`` tag. These must be :class:`AST` nodes that define variables (e.g. :class:`SetVarAST` (the ``<?code x = 42?>`` in the above example), or :class:`Template` (the ``<?def n?>...<?end def?>`` in the above example)). """ ul4_type = Type("ul4") ul4_attrs = RenderAST.ul4_attrs.union({"stoppos", "stopline", "stopcol", "stopsource", "stopsourceprefix", "stopsourcesuffix", "content"}) def __init__(self, template=None, startpos=None, stoppos=None, obj=None): super().__init__(template, startpos, obj) self._stoppos = stoppos self.content = [] def append(self, item): self.content.append(item) def finish(self, endtag): self.stoppos = endtag.startpos def _pop_trailing_indent(self): if self.content and isinstance(self.content[-1], IndentAST): return self.content.pop() else: return None def _str(self): yield self.type yield " " yield from CodeAST._str(self) # Note that :class:`BlockAST` is *not* one of our base classes, but as long as be have the proper attributes... if self.indent is not None: yield f" with indent {self.indent.text!r}" yield ":" yield None yield +1 yield from BlockAST._str(self) yield -1 def _repr_pretty(self, p): p.breakable() p.text("indent=") p.pretty(self.indent) p.breakable() p.text("obj=") p.pretty(self.obj) for arg in self.args: p.breakable() p.pretty(arg) p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def _repr(self): yield f"indent={self.indent!r}" yield f"obj={self.obj!r}" for arg in self.args: yield from arg._repr() def eval(self, context): (obj, args, kwargs) = self._evalobjargs(context) # Open a new chained variable dict, so we can collect all variables defined inside the block with context.chainvars(): # Evaluate the block content and ignore output # (Note that we're not a subclass of :class:`BlockAST`, but have the correct attributes) with context.replacestream(NullStream()): BlockAST.eval(self, context) # Check that we have no duplicate arguments vars = context.vars.maps[0] for key in vars: if key in kwargs: raise TypeError(f"multiple values for keyword argument {key!r}") # Copy variables from the block into the keyword arguments (but only the outermost map from the chain) kwargs.update(vars) self._render(context, obj, args, kwargs) @_handleeval def evalset(self, context, value): raise TypeError("can't use = on call result") @_handleeval def evalmodify(self, context, operator, value): raise TypeError("augmented assigment not allowed for call result") def ul4ondump(self, encoder): super().ul4ondump(encoder) _dumpslice(encoder, self._stoppos) encoder.dump(self.content) def ul4onload(self, decoder): super().ul4onload(decoder) self.stoppos = _loadslice(decoder) self.content = decoder.load()
[docs] @register("template") class Template(BlockAST): """ A UL4 template. A template object is normally created by passing the template source to the constructor. It can also be loaded from the compiled format via the class methods :meth:`load` (from a stream) or :meth:`loads` (from a string). The compiled format can be generated with the methods :meth:`dump` (which dumps the format to a stream) or :meth:`dumps` (which returns a string with the compiled format). Rendering the template can be done with the methods :meth:`render` (which is a generator) or :meth:`renders` (which returns a string). A :class:`Template` can also be called as a function (returning the result of the first ``<?return?>`` tag encountered). In this case all output of the template will be ignored. For rendering and calling a template with global variables the following methods are available: :meth:`render_with_globals`, :meth:`renders_with_globals` and :meth:`call_with_globals`. A :class:`Template` object is itself an AST node. Evaluating it will store a :class:`TemplateClosure` object for this template under the template's name in the local variables. """ ul4_type = InstantiableType("ul4", "Template", "An UL4 template") ul4_attrs = BlockAST.ul4_attrs.union({"signature", "doc", "name", "namespace", "fullname", "whitespace", "parenttemplate", "fullsource", "renders"}) version = "52"
[docs] def __init__(self, source=None, name=None, *, namespace=None, whitespace="keep", signature=None): """ Create a :class:`Template` object. If ``source`` is :const:`None`, the :class:`Template` remains uninitialized, otherwise ``source`` will be compiled. ``name`` is the name of the template. It will be used in exception messages and should be a valid Python identifier. ``namespace`` is the namespace name. It defaults to None. ``whitespace`` specifies how whitespace is handled in the literal text in templates (i.e. the text between the tags): ``"keep"`` Whitespace is kept as it is. ``"strip"`` Strip linefeeds and the following indentation from the text. However trailing whitespace at the end of the line will still be honored. ``"smart"`` If a line contains only indentation and one tag that isn't a ``print`` or ``printx`` tag, the indentation and the linefeed after the tag will be stripped from the text. Furthermore the additional indentation that might be introduced by a ``for``, ``if``, ``elif``, ``else`` or ``def`` block will be ignored. So for example the output of:: <?code langs = ["Python", "Java", "Javascript"]?> <?if langs?> <?for lang in langs?> <?print lang?> <?end for?> <?end if?> will simply be:: Python Java Javascript without any additional empty lines or indentation. (Output will always be ignored when calling a template as a function.) ``signature`` is the signature of the template. For a top level template it can be: :const:`None` The template will accept all keyword arguments. An :class:`inspect.Signature` object This signature will be used as the signature of the template. A callable The signature of the callable will be used. A string The signature as a string, i.e. something like ``"x, y=42, *args, **kwargs"``. This string will be parsed and evaluated to create the signature for the template. If the template is a locally defined subtemplate (i.e. a template defined by another template via ``<?def t?>...<?end def?>``), ``signature`` can be: :const:`None` The template will accept all arguments. A :class:`SignatureAST` object This AST node will be evaluated at the point of definition of the subtemplate to create the final signature of the subtemplate. """ super().__init__(self, slice(0, 0), None) self.whitespace = whitespace self.name = name self.namespace = namespace self._fullsource = None self.docpos = None self.parenttemplate = None if isinstance(signature, str): # The parser needs a tag, and each tag references its template which contains the source. # So to make the source of the signature available in the source, we prepend an ``<?ul4?>`` tag source = f"<?ul4 {name or ''}({signature})?>{source}" signature = None elif callable(signature): signature = inspect.signature(signature) self.signature = signature # If we have source code compile it if source is not None: stop = len(source) self.stoppos = slice(stop, stop) self._compile(source) else: self._fullsource = "" self.stoppos = self._startpos
@property def fullname(self): if self.name is None: return None return f"{self.namespace}.{self.name}" if self.namespace is not None else self.name def _repr(self): yield f"fullname={self.fullname!r}" yield f"whitespace={self.whitespace!r}" if self.signature is not None: yield f"signature={self.signature}" def _repr_pretty(self, p): p.breakable() p.text("fullname=") p.pretty(self.fullname) p.breakable() p.text("whitespace=") p.pretty(self.whitespace) if self.signature is not None: p.breakable() if isinstance(self.signature, SignatureAST): p.text("signature=") p.pretty(self.signature) else: p.text(f"signature={self.signature}") p.breakable() with p.group(4, "content=[", "]"): for node in self.content: p.breakable() p.pretty(node) def _str(self): yield "def " yield self.fullname or "unnamed" if self.signature is not None: if isinstance(self.signature, SignatureAST): yield from self.signature._str() else: yield str(self.signature) yield ":" yield None yield +1 yield from super()._str() yield -1 @property def doc(self): return self._fullsource[self.docpos] if self.docpos is not None else None def ul4_getattr(self, name): if name == "renders": return self.ul4_renders else: return getattr(self, name) def ul4ondump(self, encoder): # Don't call ``super().ul4ondump()`` first, as we want the version to be first encoder.dump(self.version) encoder.dump(self.name) encoder.dump(self.namespace) encoder.dump(self._fullsource) encoder.dump(self.whitespace) _dumpslice(encoder, self.docpos) encoder.dump(self.parenttemplate) # Signature can be :const:`None` or an instance of :class:`inspect.Signature` or :class:`SignatureAST` if self.signature is None or isinstance(self.signature, SignatureAST): encoder.dump(self.signature) else: # Serialize an instance of :class:`inspect.Signature` as a flat list # e.g. ['x', 'pk', 'y', 'pk=', 42, args', '*', kwargs', '**'] for the signature ``(x, y=42, *args, **kwargs)`` dump = [] for param in self.signature.parameters.values(): dump.append(param.name) if param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.default is inspect.Parameter.empty: dump.append("pk") else: dump.append( "pk=") dump.append(param.default) elif param.kind is inspect.Parameter.POSITIONAL_ONLY: if param.default is inspect.Parameter.empty: dump.append("p") else: dump.append("p=") dump.append(param.default) elif param.kind is inspect.Parameter.KEYWORD_ONLY: if param.default is inspect.Parameter.empty: dump.append("k") else: dump.append("k=") dump.append(param.default) elif param.kind is inspect.Parameter.VAR_POSITIONAL: dump.append("*") elif param.kind is inspect.Parameter.VAR_KEYWORD: dump.append("**") else: raise ValueError(f"can't dump parameter {param.name} of type {param.kind}") encoder.dump(dump) super().ul4ondump(encoder) ul42signature = { "pk": inspect.Parameter.POSITIONAL_OR_KEYWORD, "pk=": inspect.Parameter.POSITIONAL_OR_KEYWORD, "p": inspect.Parameter.POSITIONAL_ONLY, "p=": inspect.Parameter.POSITIONAL_ONLY, "k": inspect.Parameter.KEYWORD_ONLY, "k=": inspect.Parameter.KEYWORD_ONLY, "*": inspect.Parameter.VAR_POSITIONAL, "**": inspect.Parameter.VAR_KEYWORD, } def ul4onload(self, decoder): version = decoder.load() # If the loaded version is :const:`None`, this is not a "compiled" version of the template, # but a "source" version. It only contains the info required to compile the template. # # Not all implementations (i.e. the Javascript one) support this mode. # # This is implemented so that the PL/SQL version can put templates into UL4ON dumps. if version is None: # dump is in "source" form self.name = decoder.load() self.namespace = decoder.load() source = decoder.load() signature = decoder.load() self.whitespace = decoder.load() if signature is not None: source = f"<?ul4 {self.name or ''}({signature})?>{source}" # Remove old content, before compiling the source self.startpos = slice(0, 0) stop = len(source) self.stoppos = slice(stop, stop) del self.content[:] self._compile(source) else: # dump is in compiled form if version != self.version: raise ValueError(f"invalid version, expected {self.version!r}, got {version!r}") self.name = decoder.load() self.namespace = decoder.load() self._fullsource = decoder.load() self.whitespace = decoder.load() self.docpos = _loadslice(decoder) self.parenttemplate = decoder.load() dump = decoder.load() if dump is None or isinstance(dump, SignatureAST): self.signature = dump else: params = [] paramname = None paramtype = None state = 0 for value in dump: if state == 0: paramname = value state = 1 elif state == 1: paramtype = value if paramtype.endswith("="): state = 2 else: params.append(inspect.Parameter(paramname, self.ul42signature[paramtype])) state = 0 else: params.append(inspect.Parameter(paramname, self.ul42signature[paramtype], default=value)) state = 0 self.signature = inspect.Signature(params) super().ul4onload(decoder)
[docs] @classmethod def loads(cls, data): """ Loads a template as an UL4ON dump from the string ``data``. """ from ll import ul4on return ul4on.loads(data)
[docs] @classmethod def load(cls, stream): """ Loads the template as an UL4ON dump from the stream ``stream``. format. """ from ll import ul4on return ul4on.load(stream)
[docs] def dump(self, stream): """ Dump the template in compiled UL4ON format to the stream ``stream``. """ from ll import ul4on ul4on.dump(self, stream)
[docs] def dumps(self): """ Return the template in compiled UL4ON format (as a string). """ from ll import ul4on return ul4on.dumps(self)
def _renderbound(self, context): # Helper method used by :meth:`render` and :meth:`TemplateClosure.render` # where arguments have already been bound try: # Bypass ``self.eval()`` which simply stores the object as a local variable # Also bypass ``super().eval()`` as this would add additional stackframe in exception messages for node in self.content: node.eval(context) except ReturnException: pass @withcontext def ul4_render(self, context, /, *args, **kwargs): vars = _makevars(self.signature, args, kwargs) with context.replacevars(vars): self._renderbound(context)
[docs] def render(self, stream, /, *args, **kwargs): """ Render the template into the output stream `stream`. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ context = Context(None, stream) self.ul4_render(context, *args, **kwargs) stream.flush()
[docs] def render_with_globals(self, stream, args, kwargs, globals): """ Render the template into the output stream `stream`. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ context = Context(globals, stream) self.ul4_render(context, *args, **kwargs) stream.flush()
# This will be exposed to UL4 as ``renders`` @withcontext def ul4_renders(self, context, /, *args, **kwargs): stream = io.StringIO() with context.replacestream(stream): vars = _makevars(self.signature, args, kwargs) with context.replacevars(vars): self._renderbound(context) return stream.getvalue()
[docs] def renders(self, /, *args, **kwargs): """ Render the template as a string. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ return self.ul4_renders(Context(), *args, **kwargs)
[docs] def renders_with_globals(self, args, kwargs, globals): """ Render the template as a string. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ return self.ul4_renders(Context(globals), *args, **kwargs)
def _callbound(self, context): # Helper method used by :meth:`__call__` and :meth:`TemplateClosure.__call__` # where arguments have already been bound try: with context.replacestream(NullStream()): # Ignore all output super().eval(context) # Bypass ``self.eval()`` which simply stores the object as a local variable except ReturnException as exc: return exc.value
[docs] @withcontext def ul4_call(self, context, /, *args, **kwargs): """ Call the template as a function and return the resulting value. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ vars = _makevars(self.signature, args, kwargs) with context.replacevars(vars): return self._callbound(context)
[docs] def __call__(self, /, *args, **kwargs): """ Call the template as a function and return the resulting value. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ return self.ul4_call(Context(), *args, **kwargs)
[docs] def call_with_globals(self, args, kwargs, globals): """ Call the template as a function and return the resulting value. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ return self.ul4_call(Context(globals), *args, **kwargs)
[docs] def jssource(self): """ Return the template as the source code of a Javascript function. """ return f"ul4.loads({_asjson(self.dumps())})"
[docs] def javasource(self): """ Return the template as Java source code. """ from ll import misc return f"com.livinglogic.ul4.Template.loads({misc.javaexpr(self.dumps())})"
def _tokenize(self, source): """ Tokenize the template source code in ``source`` into tags and non-tag text. This is a generator which produces :class:`Text`/:class:`Tag` objects for each tag or non-tag text. It will be called by :meth:`_compile` internally. """ pattern = r"<\?\s*(ul4|whitespace|printx|print|code|for|while|if|elif|else|end|break|continue|def|return|renderblocks|renderblock|renderx|render|renderx_or_printx|render_or_printx|renderx_or_print|render_or_print|note|doc|ignore)\b(\s*((.|\n)*?)\s*)?\?>" # Last position pos = 0 # Nesting level of ``<?ignore?>``, ``<?doc?>`` or ``<?note?>`` blocks nestinglevel = 0 # In which type of nested block we're in: "ignore", "doc" or "note" nestingtype = None # Location of the last active outermost nested block last_nested_tag_pos = None last_nested_code_pos = None for match in re.finditer(pattern, source): if not nestinglevel and match.start() != pos: yield TextAST(self, source, pos, match.start()) tagname = source[slice(*match.span(1))] if not nestinglevel: if tagname == "ignore" or (tagname in {"doc", "note"} and not match.group(3)): # Remember the initial outer nested block so we can complain # about it if it remains unclosed last_nested_tag_pos = slice(*match.span()) last_nested_code_pos = slice(*match.span(3)) nestinglevel += 1 nestingtype = tagname elif tagname not in {"ignore", "note"}: yield Tag(self, tagname, slice(*match.span()), slice(*match.span(3))) elif tagname == nestingtype and (tagname == "ignore" or not match.group(3)): nestinglevel += 1 elif tagname == "end" and match.group(3) == nestingtype: nestinglevel -= 1 if not nestinglevel and nestingtype == "doc": yield Tag(self, nestingtype, slice(last_nested_tag_pos.start, match.end()), slice(last_nested_tag_pos.stop, match.start())) pos = match.end() end = len(source) if not nestinglevel and pos != end: yield TextAST(self, source, pos, end) if nestinglevel: exc = BlockError(f"<?{nestingtype}?> block unclosed") _decorateexception(exc, Tag(self, nestingtype, last_nested_tag_pos, last_nested_code_pos)) raise exc def _tags2lines(self, tags): """ Transforms an iterable of tags into an iterable of lines by splitting the literal text between the tags into lines. A line is a list of nodes and will start with an :class:`Indent` node (containing the indenting whitespace if the line is indented, or an empty indentation if it isn't) and might end with a :class:`LineEnd` node (containing the line feed if the line is terminated (which most lines (except maybe the last one) are)). """ # a list of tags that are all part of one line tagline = [] def append(tag): # If this is a new line and it doesn't start with an indentation, # add an empty indentation at the start (We always add indentation, # as this is used by :class:`RenderAST` to reindent the output of one # template when called from inside another template) if not tagline and not isinstance(tag, IndentAST): tagline.append(IndentAST(tag.template, self._fullsource, tag._startpos.start, tag._startpos.start)) tagline.append(tag) # Yield lines by splitting literal text into multiple chunks (normal text, indentation and lineends) wastag = False for tag in tags: if isinstance(tag, TextAST): pos = tag._startpos.start for line in tag.text.splitlines(True): # Find out if the line ends with a lineend linelen = len(line) for lineend in self._whitespace_lineends: if line.endswith(lineend): lineendlen = len(lineend) line = line[:-lineendlen] linelen -= lineendlen break else: lineendlen = 0 # Find out how the line is indented if wastag: lineindentlen = 0 wastag = False # Done inside the loop, because all lines after the first one must always be checked for indentation else: lineindentlen = len(line)-len(line.lstrip()) linelen -= lineindentlen # Output the parts we found if lineindentlen: append(IndentAST(tag.template, self._fullsource, pos, pos+lineindentlen)) pos += lineindentlen if linelen: append(TextAST(tag.template, self._fullsource, pos, pos+linelen)) pos += linelen if lineendlen: append(LineEndAST(tag.template, self._fullsource, pos, pos+lineendlen)) pos += lineendlen yield tagline tagline = [] else: append(tag) wastag = True if tagline: yield tagline def _whitespace_keep(self, lines): for line in lines: yield from line def _whitespace_strip(self, lines): first = True for line in lines: for tag in line: if first or not isinstance(tag, (IndentAST, LineEndAST)): yield tag first = False _whitespace_lineends = ("\r\n", "\n") def _whitespace_smart(self, lines): def indent(tagline): # Return the indentation of the line if tagline: return tagline[0].text return "" def isempty(tagline): return all(isinstance(tag, (IndentAST, LineEndAST)) for tag in tagline) # Records the starting and ending line number of a block and its indentation class Block: def __init__(self, start): self.start = start self.stop = None self.indent = None # Return the length of the longest common prefix of all strings in ``indents`` def commonindentlen(indents): if not indents: return 0 indent1 = min(indents) indent2 = max(indents) for (i, c) in enumerate(indent1): if c != indent2[i]: return i return len(indent1) # Step 1: Determine the block structure of the lines blocks = [] # List of all blocks stack = [] # Stack of currently "open" blocks newlines = [] for (i, line) in enumerate(lines): linelen = len(line) if 2 <= linelen <= 3 and isinstance(line[0], IndentAST) and isinstance(line[1], Tag) and line[1].tag not in ("print", "printx", "render", "renderx", "render_or_print", "render_or_printx", "renderx_or_print", "renderx_or_printx") and (linelen == 2 or isinstance(line[2], LineEndAST)): tag = line[1] # Tags closing a block if tag.tag in ("elif", "else", "end"): if stack: stack[-1].stop = i # Previous block ends before this line stack.pop() newlines.append((line, stack[:])) # Tags opening a block if tag.tag in ("for", "if", "def", "elif", "else", "renderblock", "renderblocks"): block = Block(i+1) # Block starts on the next line stack.append(block) blocks.append(block) else: newlines.append((line, stack[:])) # Close open blocks (shouldn't be necessary for properly nested templates) for block in stack: block.stop = len(lines) # Step 2: Find the outer and inner indentation of all blocks for block in blocks: block.indent = range( # outer indent, i.e. the indentation of the start tag of the block len(indent(lines[block.start-1])) if block.start else 0, # inner indentation (ignoring lines that only contain whitespace) commonindentlen([indent(line) for line in itertools.islice(lines, block.start, block.stop) if not isempty(line)]), ) # Step 3: Fix the indentation allindents = {} for (line, blocks) in newlines: if line: # use all character for indentation that are not part of the "artificial" indentation introduced in each block newindent = "".join(c for (i, c) in enumerate(line[0].text) if not any(i in block.indent for block in blocks)) # Reuse previous indent string if we already have it (minizes memory usage and UL4ON dump size) newindent = allindents.setdefault(newindent, newindent) line[0]._settext(newindent) # Step 4: Drop whitespace from empty lines or lines that only contain indentation and block tags for line in lines: if len(line) == 2 and isinstance(line[0], IndentAST) and isinstance(line[1], LineEndAST): del line[0] elif len(line) == 2 and isinstance(line[0], IndentAST) and isinstance(line[1], Tag) and line[1].tag not in ("print", "printx", "render", "renderx", "render_or_print", "render_or_printx", "renderx_or_print", "renderx_or_printx"): del line[0] elif len(line) == 3 and isinstance(line[0], IndentAST) and isinstance(line[1], Tag) and line[1].tag not in ("print", "printx", "render", "renderx", "render_or_print", "render_or_printx", "renderx_or_print", "renderx_or_printx") and isinstance(line[2], LineEndAST): del line[2] del line[0] elif len(line) == 3 and isinstance(line[0], IndentAST) and isinstance(line[1], Tag) and line[1].tag in ("render", "renderx", "render_or_print", "render_or_printx", "renderx_or_print", "renderx_or_printx") and isinstance(line[2], LineEndAST): del line[2] # Step 5: Yield the individual :class:`Tag`/:class:`TextAST` objects for line in lines: yield from line def _parser(self, tag, error): from ll import UL4Lexer, UL4Parser source = tag.code if not source: raise ValueError(error) stream = antlr3.ANTLRStringStream(source) lexer = UL4Lexer.UL4Lexer(stream) lexer.tag = tag tokens = antlr3.CommonTokenStream(lexer) parser = UL4Parser.UL4Parser(tokens) parser.tag = tag return parser def _compile(self, source): """ Compile the template source code ``source`` into an AST. """ self._fullsource = source if source is None: return blockstack = [self] # This stack stores the nested for/if/elif/else/def blocks templatestack = [self] # This stack stores the nested templates def parsedeclaration(tag): try: return self._parser(tag, "declaration required").definition() except Exception as exc: _decorateexception(exc, self) raise def parseexpr(tag): return self._parser(tag, "expression required").expression() def parsestmt(tag): return self._parser(tag, "statement required").statement() def parsefor(tag): return self._parser(tag, "loop expression required").for_() def parsedef(tag): return self._parser(tag, "definition required").definition() def parserender(tag): call = self._parser(tag, "render call required").expression() if not isinstance(call, CallAST): raise TypeError("render call required") tags = dict( render=RenderAST, renderx=RenderXAST, renderblock=RenderBlockAST, renderblocks=RenderBlocksAST, render_or_print=RenderOrPrintAST, render_or_printx=RenderOrPrintXAST, renderx_or_print=RenderXOrPrintAST, renderx_or_printx=RenderXOrPrintXAST, ) render = tags[tag.tag](template=tag.template, startpos=tag.startpos, obj=call.obj) render.args = call.args if tag.tag == "renderblock": # We create the sub template without source so there won't be any compilation done ... render.content = Template(None, name="content", whitespace=self.whitespace) # ... but then we have to fix the ``fullsource`` and ``startpos`` attributes ourselves render.content._fullsource = self._fullsource # The stop position will be updated by :meth:`RenderBlock.finish`. render.content.startpos = slice(tag.startpos.stop, tag.startpos.stop) return render tags = self._tokenize(source) lines = list(self._tags2lines(tags)) # Find template declarations and whitespace specification for line in lines: for tag in line: if isinstance(tag, Tag): if tag.tag == "ul4": (name, signature) = parsedeclaration(tag) self.name = name if signature is not None: signature = signature.eval(Context()) self.signature = signature elif tag.tag == "whitespace": whitespace = tag.code if whitespace in {"keep", "strip", "smart"}: self.whitespace = whitespace else: try: raise ValueError(f"whitespace mode {whitespace!r} unknown") except Exception as exc: _decorateexception(exc, tag) raise # Flatten lines and update whitespace according to the whitespace mode specified if self.whitespace == "keep": tags = self._whitespace_keep(lines) elif self.whitespace == "strip": tags = self._whitespace_strip(lines) elif self.whitespace == "smart": tags = self._whitespace_smart(lines) else: raise ValueError(f"whitespace mode {self.whitespace!r} unknown") for tag in tags: # Update ``tag.template`` to reference the innermost template # (Originally it referenced the outermost one) tag.template = templatestack[-1] try: if isinstance(tag, TextAST): blockstack[-1].append(tag) elif tag.tag == "doc": # Only use the first ``<?doc?>`` tag in each template, ignore all later ones if templatestack[-1].docpos is None: templatestack[-1].docpos = tag.codepos elif tag.tag == "print": blockstack[-1].append(PrintAST(templatestack[-1], tag.startpos, parseexpr(tag))) elif tag.tag == "printx": blockstack[-1].append(PrintXAST(templatestack[-1], tag.startpos, parseexpr(tag))) elif tag.tag == "code": blockstack[-1].append(parsestmt(tag)) elif tag.tag == "if": block = ConditionalBlocksAST(templatestack[-1], tag.startpos, None, parseexpr(tag)) blockstack[-1].append(block) blockstack.append(block) elif tag.tag == "elif": if not isinstance(blockstack[-1], ConditionalBlocksAST): raise BlockError("<?elif?> doesn't match any <?if?>") elif isinstance(blockstack[-1].content[-1], ElseBlockAST): raise BlockError("<?else?> already seen in <?if?>") blockstack[-1].newblock(ElIfBlockAST(templatestack[-1], tag.startpos, None, parseexpr(tag))) elif tag.tag == "else": if not isinstance(blockstack[-1], ConditionalBlocksAST): raise BlockError("<?else?> doesn't match any <?if?>") elif isinstance(blockstack[-1].content[-1], ElseBlockAST): raise BlockError("<?else?> already seen in <?if?>") blockstack[-1].newblock(ElseBlockAST(templatestack[-1], tag.startpos, None)) elif tag.tag == "end": if len(blockstack) <= 1: raise BlockError("not in any block") code = tag.code if code: if code == "if": if not isinstance(blockstack[-1], ConditionalBlocksAST): raise BlockError("<?end if?> doesn't match any <?if?>") elif code == "for": if not isinstance(blockstack[-1], ForBlockAST): raise BlockError("<?end for?> doesn't match any <?for?>") elif code == "while": if not isinstance(blockstack[-1], WhileBlockAST): raise BlockError("<?end while?> doesn't match any <?while?>") elif code == "def": if not isinstance(blockstack[-1], Template): raise BlockError("<?end def?> doesn't match any <?def?>") templatestack.pop() elif code == "renderblock": if not isinstance(blockstack[-1], RenderBlockAST): raise BlockError("<?end renderblock?> doesn't match any <?renderblock?>") elif code == "renderblocks": if not isinstance(blockstack[-1], RenderBlocksAST): raise BlockError("<?end renderblocks?> doesn't match any <?renderblocks?>") elif code == "ignore": raise BlockError("not in any <?ignore?> block") else: raise BlockError(f"illegal end value {code!r}") last = blockstack.pop() last.finish(tag) # Set end position of block elif tag.tag == "for": block = parsefor(tag) blockstack[-1].append(block) blockstack.append(block) elif tag.tag == "while": block = WhileBlockAST(templatestack[-1], tag.startpos, None, parseexpr(tag)) blockstack[-1].append(block) blockstack.append(block) elif tag.tag == "break": for block in reversed(blockstack): if isinstance(block, (ForBlockAST, WhileBlockAST)): break elif isinstance(block, Template): raise BlockError("<?break?> outside of <?for?> loop") blockstack[-1].append(BreakAST(templatestack[-1], tag.startpos)) elif tag.tag == "continue": for block in reversed(blockstack): if isinstance(block, (ForBlockAST, WhileBlockAST)): break elif isinstance(block, Template): raise BlockError("<?continue?> outside of <?for?> loop") blockstack[-1].append(ContinueAST(templatestack[-1], tag.startpos)) elif tag.tag == "def": (name, signature) = parsedef(tag) block = Template(None, name=name, whitespace=self.whitespace, signature=signature) block.template = block block.parenttemplate = templatestack[-1] tag.template = block templatestack.append(block) # The source is always the complete source of the top level template # (so that the offsets in all :class:`AST` objects are correct) block._fullsource = self._fullsource block.startpos = tag.startpos blockstack[-1].append(block) blockstack.append(block) elif tag.tag == "return": blockstack[-1].append(ReturnAST(templatestack[-1], tag.startpos, parseexpr(tag))) elif tag.tag in {"render", "renderx", "renderblock", "renderblocks", "render_or_print", "render_or_printx", "renderx_or_print", "renderx_or_printx"}: render = parserender(tag) # Find innermost block innerblock = blockstack[-1] # If we have an indentation before the ``<?render?>`` tag, move it # into the ``indent`` attribute of the :class`Render` object, # because this indentation must be added to every line that the # rendered template outputs. render.indent = innerblock._pop_trailing_indent() blockstack[-1].append(render) if tag.tag in {"renderblock", "renderblocks"}: blockstack.append(render) elif tag.tag in ("ul4", "whitespace", "note", "doc"): # Don't copy declarations, whitespace specification, comments or docstrings over into the syntax tree pass else: # Can't happen raise ValueError(f"unknown tag {tag.tag!r}") except Exception as exc: _decorateexception(exc, tag) raise if len(blockstack) > 1: exc = BlockError("block unclosed") _decorateexception(exc, blockstack[-1]) raise exc # @_handleeval def eval(self, context): signature = self.signature # If our signature is an AST, we have to evaluate it to get the final :class:`inspect.Signature` object if isinstance(signature, SignatureAST): signature = signature.eval(context) context.vars[self.name] = TemplateClosure(self, context, signature)
[docs] @register("signature") class SignatureAST(CodeAST): """ AST node for the signature of a locally defined subtemplate. Attributes are: ``params`` : :class:`list` The parameter. Each list item is a :class:`tuple` with three items: ``name`` : :class:`str` The name of the argument. ``type`` : :class:`str` The type of the argument. One of: - ``pk`` (positional or keyword argument without default) - ``pk=`` (positional or keyword argument with default) - ``p`` (positional-only argument without default) - ``p=`` (positional-only argument with default) - ``k`` (keyword-only argument without default) - ``k=`` (keyword-only argument with default) - ``*`` (argument that collects addition positional arguments) - ``**`` (argument that collects addition keyword arguments) ``default`` : :class:`AST` or ``None`` The default value for the argument (or ``None`` if the argument has no default value). """ ul4_type = Type("ul4") ul4_attrs = CodeAST.ul4_attrs.union({"params"}) def __init__(self, template=None, startpos=None): super().__init__(template, startpos) self.params = [] def __repr__(self): params = [] lastparamtype = None for (paramname, paramtype, default) in self.params: sep = self._sep(lastparamtype, paramtype) lastparamtype = paramtype params.append(sep) if paramtype in {"*", "**"}: params.append(paramtype) params.append(paramname) if paramtype.endswith("="): params.append(f"={default!r}") params = "".join(params) return f"<{self.__class__.__module__}.{self.__class__.__qualname__} {_offset(self.pos)} ({params}) at {id(self):#x}>" def _repr_pretty(self, p): for (paramname, default) in self.params: p.breakable() if default is None: p.text(paramname) else: p.text(f"{paramname}=") p.pretty(default) def _str(self): yield "(" lastparamtype = None for (i, (paramname, paramtype, default)) in enumerate(self.params): sep = self._sep(lastparamtype, paramtype) lastparamtype = paramtype if sep: yield sep yield paramname if paramtype.endswith("="): yield "=" yield from default._str() yield ")" def _sep(self, lastparamtype, paramtype): if lastparamtype is None: return "*, " if paramtype in {"k", "k="} else "" elif lastparamtype in {"pk", "pk="}: return ", *, " if paramtype in {"k", "k="} else ", " elif lastparamtype in {"p", "p="}: if paramtype in {"pk", "pk="}: return ", /, " elif paramtype in {"k", "k="}: return ", /, *, " else: return ", " else: return ", " @_handleeval def eval(self, context): params = [] for (paramname, paramtype, default) in self.params: if paramtype == "*": kind = inspect.Parameter.VAR_POSITIONAL default = inspect.Parameter.empty elif paramtype == "**": kind = inspect.Parameter.VAR_KEYWORD default = inspect.Parameter.empty elif paramtype == "pk": kind = inspect.Parameter.POSITIONAL_OR_KEYWORD default = inspect.Parameter.empty elif paramtype == "pk=": kind = inspect.Parameter.POSITIONAL_OR_KEYWORD default = default.eval(context) elif paramtype == "p": kind = inspect.Parameter.POSITIONAL_ONLY default = inspect.Parameter.empty elif paramtype == "p=": kind = inspect.Parameter.POSITIONAL_ONLY default = default.eval(context) elif paramtype == "k": kind = inspect.Parameter.KEYWORD_ONLY default = inspect.Parameter.empty elif paramtype == "k=": kind = inspect.Parameter.KEYWORD_ONLY default = default.eval(context) params.append(inspect.Parameter(paramname, kind, default=default)) return inspect.Signature(params) def ul4ondump(self, encoder): super().ul4ondump(encoder) dump = [] for (paramname, paramtype, default) in self.params: dump.append(paramname) dump.append(paramtype) if paramtype.endswith("="): dump.append(default) encoder.dump(dump) def ul4onload(self, decoder): super().ul4onload(decoder) dump = decoder.load() state = 0 for value in dump: if state == 0: paramname = value state = 1 elif state == 1: paramtype = value if paramtype.endswith("="): state = 2 else: self.params.append((paramname, paramtype, None)) state = 0 else: self.params.append((paramname, paramtype, value)) state = 0
### ### Various versions of undefined objects ### class Undefined: ul4_type = Type(None, "undefined") def __bool__(self): return False def __iter__(self): raise TypeError(f"{self!r} is not iterable") def __len__(self): raise AttributeError(f"{self!r} has no len()") def __call__(self, *args, **kwargs): raise TypeError(f"{self!r} is not callable") def __getattr__(self, key): raise AttributeError(f"{self!r} has no attribute {key!r}") def __getitem__(self, key): raise TypeError(f"{self!r} is not subscriptable (key={key!r})") class UndefinedKey(Undefined): ul4_type = Type(None, "undefinedkey") def __init__(self, object, key): self.object = object self.key = key def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__} object={self.object!r} key={self.key!r} at {id(self):#x}>" class UndefinedVariable(Undefined): ul4_type = Type(None, "undefinedvariable") def __init__(self, name): self.name = name def __repr__(self): return f"UndefinedVariable({self.name!r})" ### ### Functions ### def _now(): # Wrap this in our own function, because :meth:`datateimt.datetime.now` supports a ``tz`` argument. return datetime.datetime.now() def _csv(obj, /): if obj is None: return "" elif isinstance(obj, Undefined): return "" elif not isinstance(obj, str): obj = _repr(obj) if any(c in obj for c in ',"\n'): text = obj.replace('"', '""') return f'"{text}"' return obj def _fromjson(string, /): # Wrap this in our own function, because :func:`json.loads` supports a many more arguments. return json.loads(string) def _fromul4on(dump, /): # Wrap this in our own function, because we don't want to support the ``registry`` argument from ll import ul4on return ul4on.loads(dump) def _enumfl(iterable, /, start=0): lastitem = None first = True i = start it = iter(iterable) try: item = next(it) except StopIteration: return while True: try: (lastitem, item) = (item, next(it)) except StopIteration: yield (i, first, True, item) # Items haven't been swapped yet return else: yield (i, first, False, lastitem) first = False i += 1 def _isundefined(obj, /): return isinstance(obj, Undefined) def _isdefined(obj, /): return not isinstance(obj, Undefined) def _isnone(obj, /): return obj is None def _isstr(obj, /): return isinstance(obj, str) def _isint(obj, /): return isinstance(obj, int) and not isinstance(obj, bool) def _isfloat(obj, /): return isinstance(obj, float) def _isbool(obj, /): return isinstance(obj, bool) def _isdate(obj, /): return isinstance(obj, datetime.date) and not isinstance(obj, datetime.datetime) def _isdatetime(obj, /): return isinstance(obj, datetime.datetime) def _istimedelta(obj, /): return isinstance(obj, datetime.timedelta) def _ismonthdelta(obj, /): from ll import misc return isinstance(obj, misc.monthdelta) def _isexception(obj, /): return isinstance(obj, BaseException) def _islist(obj, /): from ll import color return isinstance(obj, abc.Sequence) and not isinstance(obj, (str, color.Color)) def _isset(obj, /): return isinstance(obj, (set, frozenset)) def _isdict(obj, /): return isinstance(obj, abc.Mapping) and not isinstance(obj, Template) def _iscolor(obj, /): from ll import color return isinstance(obj, color.Color) def _istemplate(obj, /): return isinstance(obj, (Template, TemplateClosure)) def _isfunction(obj, /): return (callable(obj) and not isinstance(obj, Undefined)) or callable(getattr(obj, "ul4_call", None)) def _isinstance(obj, type, /): return type.instancecheck(obj) def _makekeyfunction(context, key): if key is not None: if callable(getattr(key, "ul4_call", None)): key = key.ul4_call elif callable(key): key = key.__call__ if getattr(key, "ul4_context", False): key = functools.partial(key, context) return key @withcontext def _min(context, *args, default=_defaultitem, key=None): if default is _defaultitem: return min(*args, key=_makekeyfunction(context, key)) else: return min(*args, default=default, key=_makekeyfunction(context, key)) @withcontext def _max(context, *args, default=_defaultitem, key=None): if default is _defaultitem: return max(*args, key=_makekeyfunction(context, key)) else: return max(*args, default=default, key=_makekeyfunction(context, key)) @withcontext def _sorted(context, iterable, /, key=None, reverse=False): return sorted(iterable, key=_makekeyfunction(context, key), reverse=reverse) _py_classes_2_ul4_types = {} def _type(obj, /): ul4type = getattr(obj, "ul4_type", None) if ul4type is not None: return ul4type elif obj is None: return NoneType elif isinstance(obj, bool): return BoolType elif isinstance(obj, int): return IntType elif isinstance(obj, float): return FloatType elif isinstance(obj, str): return StrType elif isinstance(obj, (set, frozenset, abc.Set)): return SetType elif isinstance(obj, (list, tuple, abc.Sequence)): return ListType elif isinstance(obj, (dict, abc.Mapping)): return DictType elif isinstance(obj, slice): return SliceType elif isinstance(obj, datetime.datetime): return DateTimeType elif isinstance(obj, datetime.date): return DateType elif isinstance(obj, datetime.timedelta): return TimeDeltaType else: cls = type(obj) try: return _py_classes_2_ul4_types[cls] except KeyError: ul4cls = GenericExceptionType if issubclass(cls, BaseException) else GenericType result = _py_classes_2_ul4_types[cls] = ul4cls(cls) return result def _randrange(*args): return random.randrange(*args) def _format(obj, fmt, lang=None): if isinstance(obj, (datetime.date, datetime.time, datetime.timedelta)): if lang is None: lang = "en" oldlocale = locale.getlocale() try: for candidate in (locale.normalize(lang), locale.normalize("en"), ""): try: locale.setlocale(locale.LC_ALL, candidate) return format(obj, fmt) except locale.Error: if not candidate: return format(obj, fmt) finally: try: locale.setlocale(locale.LC_ALL, oldlocale) except locale.Error: pass elif isinstance(obj, (float, decimal.Decimal)): if lang is None: lang = "en" result = format(obj, fmt) if lang == "en": return result # Shortcut: we know that these languages use commas as the decimal separator elif lang in {"de", "fr", "it", "es", "nl", "no", "pl", "ru", "sv"}: return result.replace(".", ",") oldlocale = locale.getlocale() try: for candidate in (locale.normalize(lang), locale.normalize("en"), ""): try: locale.setlocale(locale.LC_ALL, candidate) decimalpoint = locale.localeconv()["decimal_point"] return result.replace(".", decimalpoint) except locale.Error: if not candidate: return result finally: try: locale.setlocale(locale.LC_ALL, oldlocale) except locale.Error: pass return result else: return format(obj, fmt) def _urlquote(string): return urlparse.quote(string) def _urlunquote(string): return urlparse.unquote(string) def _md5(string, /): import hashlib return hashlib.md5(string.encode("utf-8")).hexdigest() def _scrypt(string, /, salt): import scrypt return scrypt.hash(string, salt, N=16384, r=8, p=1, buflen=128).hex() def _round(number, /, digits=0): result = round(number, digits) if digits <= 0: result = int(result) return result def _floor(number, /, digits=0): if digits: if isinstance(number, int): if digits > 0: return number base = 10 ** -digits return (number // base) * base else: base = 10**digits result = math.floor(number*base)/base if digits < 0: return int(result) return result else: return math.floor(number) def _ceil(number, /, digits=0): if digits: if isinstance(number, int): if digits > 0: return number base = 10 ** -digits return (number + base - 1) // base * base else: base = 10**digits result = math.ceil(number*base)/base if digits < 0: return int(result) return result else: return math.ceil(number) def _getattr(obj, attrname, default=object): try: return _type(obj).getattr(obj, attrname, default) except AttributeError: return UndefinedKey(obj, attrname) def _setattr(obj, attrname, value): return _type(obj).setattr(obj, attrname, value) def _hasattr(obj, attrname): return _type(obj).hasattr(obj, attrname) def _dir(obj, /): return _type(obj).dir(obj)
[docs] class TemplateClosure(BlockAST): """ A locally defined subtemplate. A :class:`!TemplateClosure` is a closure, i.e. it can use the local variables of the template it is defined in. """ ul4_type = Type("ul4", "TemplateClosure", "A locally defined UL4 template") ul4_attrs = Template.ul4_attrs def __init__(self, template, context, signature): self.template = template self.vars = context.vars.maps[0] self.signature = signature @withcontext def ul4_render(self, context, /, *args, **kwargs): vars = _makevars(self.signature, args, kwargs) vars = collections.ChainMap(vars, self.vars) with context.replacevars(vars): # Call :meth:`_renderbound` to bypass binding the arguments again # (which wouldn't work anyway as ``self.template.signature`` is an :class:`AST` object) self.template._renderbound(context) # This will be exposed to UL4 as ``renders`` @withcontext def ul4_renders(self, context, /, *args, **kwargs): vars = _makevars(self.signature, args, kwargs) vars = collections.ChainMap(vars, self.vars) stream = io.StringIO() with context.replacestream(stream): with context.replacevars(vars): # Call :meth:`_renderbound` to bypass binding the arguments again # (which wouldn't work anyway as ``self.template.signature`` is an :class:`AST` object) self.template._renderbound(context) return stream.getvalue() @withcontext def ul4_call(self, context, /, *args, **kwargs): vars = _makevars(self.signature, args, kwargs) vars = collections.ChainMap(vars, self.vars) with context.replacevars(vars): # Call :meth:`_callbound` to bypass binding the arguments again # (which wouldn't work anyway as ``self.template.signature`` is an :class:`AST` object) return self.template._callbound(context) def __getattr__(self, name): if name == "renders": return self.ul4_renders else: return getattr(self.template, name) def ul4_getattr(self, name): if name == "renders": return self.ul4_renders else: return getattr(self, name) def _repr(self): yield f"fullname={self.fullname!r}" yield f"whitespace={self.whitespace!r}" if self.signature is not None: yield f"signature={self.signature}" def _repr_pretty(self, p): p.breakable() p.text("fullname=") p.pretty(self.fullname) p.breakable() p.text("whitespace=") p.pretty(self.whitespace) if self.signature is not None: p.breakable() p.text(f"signature={self.signature}") for node in self.content: p.breakable() p.pretty(node)
[docs] class BoundTemplate: """ A template bound to an object. Calling or rendering a :class:`BoundTemplate` instance passes the object to which the template is bound as the first positional argument. """ def __init__(self, object, template): self.object = object self.template = template def __repr__(self): return f"<{self.__class__.__module__}.{self.__class__.__qualname__} object={self.object!r} template={self.template!r} at {id(self):#x}>"
[docs] def render(self, stream, /, *args, **kwargs): """ Render the template into the output stream `stream`. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ context = Context(None, stream) self.ul4_render(context, *args, **kwargs)
[docs] def render_with_globals(self, stream, args, kwargs, globals): """ Render the template into the output stream `stream`. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ context = Context(globals, stream) self.ul4_render(context, *args, **kwargs)
[docs] def renders(self, /, *args, **kwargs): """ Render the template as a string. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ stream = io.StringIO() context = Context(None, stream) self.ul4_render(context, *args, **kwargs) return stream.getvalue()
[docs] def renders_with_globals(self, args, kwargs, globals): """ Render the template as a string. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ stream = io.StringIO() context = Context(globals, stream) self.ul4_renders(context, *args, **kwargs) return stream.getvalue()
[docs] def __call__(self, /, *args, **kwargs): """ Call the template as a function and return the resulting value. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. Positional arguments will only be supported if the template has a signature. """ context = Context(None, NullStream()) return self.ul4_call(context, *args, **kwargs)
[docs] def call_with_globals(self, args, kwargs, globals): """ Call the template as a function and return the resulting value. ``args`` and ``kwargs`` contain the top level positional and keyword arguments available to the template code. ``globals`` contains global variables. Positional arguments will only be supported if the template has a signature. """ context = Context(globals, NullStream()) return self.ul4_call(context, *args, **kwargs)
@withcontext def ul4_render(self, context, /, *args, **kwargs): self.template.ul4_render(context, *(self.object,) + args, **kwargs) @withcontext def ul4_renders(self, context, /, *args, **kwargs): return self.template.ul4_renders(context, *(self.object,) + args, **kwargs) @withcontext def ul4_call(self, context, /, *args, **kwargs): return self.template.ul4_call(context, *(self.object,) + args, **kwargs)