diff --git a/muppet/__main__.py b/muppet/__main__.py index 46aecc412d8bfca086ba60a5250c10bc0f254fdb..cd2cfefe5284b0467bf9582cfb95b67d74b97dbc 100644 --- a/muppet/__main__.py +++ b/muppet/__main__.py @@ -16,12 +16,18 @@ logger.setLevel(logging.DEBUG) ch = colorlog.StreamHandler() -ch.setLevel(logging.INFO) +ch.setLevel(logging.DEBUG) # formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') -formatter = colorlog.ColoredFormatter('%(log_color)s%(name)s - %(levelname)s - %(message)s') +formatter = colorlog.ColoredFormatter('%(log_color)s%(name)s:%(lineno)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) +formatter2 = colorlog.ColoredFormatter('%(log_color)s%(name)s - %(file)s - %(message)s') +ch2 = colorlog.StreamHandler() +ch2.setLevel(logging.DEBUG) +ch2.setFormatter(formatter2) +logging.getLogger('muppet.puppet-string').addHandler(ch2) + def __main() -> None: parser = argparse.ArgumentParser( diff --git a/muppet/format.py b/muppet/format.py index 26c0b8fd1c32fb982059158161e4485654ef7ee0..4726258cbced4df7d96df1314b6b1a88da2d632a 100644 --- a/muppet/format.py +++ b/muppet/format.py @@ -29,8 +29,8 @@ from .puppet.strings import ( DocStringExampleTag, ) from muppet.puppet.ast import build_ast -from muppet.puppet.format import serialize -from muppet.puppet.format.html import HTMLFormatter +from muppet.puppet.format import to_string +from muppet.puppet.format.parser import ParserFormatter, ParseError logger = logging.getLogger(__name__) @@ -127,7 +127,7 @@ def format_class(d_type: DefinedType | PuppetClass) -> Tuple[str, str]: """Format Puppet class.""" out = '' name = d_type.name - logger.debug("Formatting class %s", name) + logger.info("Formatting class %s", name) # print(name, file=sys.stderr) name, body = format_docstring(name, d_type.docstring) out += body @@ -140,7 +140,11 @@ def format_class(d_type: DefinedType | PuppetClass) -> Tuple[str, str]: # out += render(renderer, data) # ------ New --------------------------------------- ast = build_ast(puppet_parser(d_type.source)) - out += serialize(ast, HTMLFormatter) + try: + out += to_string(ParserFormatter(d_type.source).serialize(ast)) + except ParseError as e: + logger.error("Parsing %(name)s failed: %(err)s", + {'name': d_type.name, 'err': e}) out += '</code></pre>' return name, out @@ -154,7 +158,7 @@ def format_type_alias(d_type: DataTypeAlias) -> Tuple[str, str]: """Format Puppet type alias.""" out = '' name = d_type.name - logger.debug("Formatting type alias %s", name) + logger.info("Formatting type alias %s", name) # print(name, file=sys.stderr) title, body = format_docstring(name, d_type.docstring) out += body @@ -162,7 +166,7 @@ def format_type_alias(d_type: DataTypeAlias) -> Tuple[str, str]: out += '<pre class="highlight-muppet"><code class="puppet">' t = puppet_parser(d_type.alias_of) data = build_ast(t) - out += serialize(data, HTMLFormatter) + out += to_string(ParserFormatter(d_type.alias_of).serialize(data)) out += '</code></pre>\n' return title, out @@ -172,13 +176,14 @@ def format_defined_type(d_type: DefinedType) -> Tuple[str, str]: # renderer = HTMLRenderer(build_param_dict(d_type.docstring)) out = '' name = d_type.name - logger.debug("Formatting defined type %s", name) + logger.info("Formatting defined type %s", name) # print(name, file=sys.stderr) title, body = format_docstring(name, d_type.docstring) out += body out += '<pre class="highlight-muppet"><code class="puppet">' - out += serialize(build_ast(puppet_parser(d_type.source)), HTMLFormatter) + ast = build_ast(puppet_parser(d_type.source)) + out += to_string(ParserFormatter(d_type.source).serialize(ast)) out += '</code></pre>\n' return title, out @@ -186,7 +191,7 @@ def format_defined_type(d_type: DefinedType) -> Tuple[str, str]: def format_resource_type(r_type: ResourceType) -> str: """Format Puppet resource type.""" name = r_type.name - logger.debug("Formatting resource type %s", name) + logger.info("Formatting resource type %s", name) out = '' out += f'<h2>{name}</h2>\n' out += str(r_type.docstring) @@ -224,7 +229,7 @@ def format_puppet_function(function: Function) -> str: """Format Puppet function.""" out = '' name = function.name - logger.debug("Formatting puppet function %s", name) + logger.info("Formatting puppet function %s", name) out += f'<h2>{name}</h2>\n' t = function.type # docstring = function.docstring @@ -242,7 +247,8 @@ def format_puppet_function(function: Function) -> str: try: # source = parse_puppet(function.source) # out += str(build_ast(source)) - out += serialize(build_ast(puppet_parser(function.source)), HTMLFormatter) + ast = build_ast(puppet_parser(function.source)) + out += to_string(ParserFormatter(function.source).serialize(ast)) except CalledProcessError as e: print(e, file=sys.stderr) print(f"Failed on function: {name}", file=sys.stderr) diff --git a/muppet/output.py b/muppet/output.py index c3a24cab6240a380da99d172a91803caa4f37f0d..fe1be7d8ea3f68b1c697309109410e383e926673 100644 --- a/muppet/output.py +++ b/muppet/output.py @@ -500,7 +500,7 @@ def setup_module(base: str, module: ModuleEntry, *, path_base: str) -> None: data = module.strings_output for puppet_class in data.puppet_classes + data.defined_types: - logger.debug('Formamting %s', puppet_class.name) + logger.info('Formamting %s', puppet_class.name) # localpath = puppet_class['name'].split('::') localpath, _ = os.path.splitext(puppet_class.file) dir = os.path.join(path, localpath) diff --git a/muppet/parser_combinator.py b/muppet/parser_combinator.py new file mode 100644 index 0000000000000000000000000000000000000000..439b3ef7c783a359be0aa400893826d86f6d52c5 --- /dev/null +++ b/muppet/parser_combinator.py @@ -0,0 +1,507 @@ +"""A basic parser combinator for Python.""" + +# import html +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + Optional, + Sequence, + TypeAlias, + Union, +) + + +@dataclass +class MatchObject: + """A matched item, similar to a regex match.""" + + # start: int + # end: int + type: str + matched: str | list['MatchObject'] + + def __init__(self, type: str, matched: str | list['MatchObject']): + self.type = type + self.matched = matched + # logger.debug(repr(self)) + + # def __str__(self) -> str: + # match self.matched: + # case str(s): + # return s + # case xs: + # return ''.join(str(s) for s in xs) + + # def __repr__(self) -> str: + # match self: + # case MatchObject('', str(s)): + # return repr(s) + # case _: + # return f'MatchedObject({repr(self.type)}, {repr(self.matched)})' + + # def serialize(self) -> str: + # """Seralize into HTML.""" + # match self.matched: + # case str(s): + # if self.type: + # return f'<span class="{self.type}">{html.escape(s)}</span>' + # else: + # return html.escape(s) + # case _: + # out = '' + # if self.type: + # out += f'<span class="{self.type}">' + # for item in self.matched: + # out += item.serialize() + # if self.type: + # out += '</span>' + # return out + + +def stringify_match(items: list[MatchObject]) -> str: + """Serialize a list of match objects back into their string form.""" + out: str = '' + for item in items: + match item.matched: + case str(s): + out += s + case other: + out += stringify_match(other) + return out + + +@dataclass +class ParseError(Exception): + """ + Errors encountered while parsing. + + This should only appear with optional fields, since we don't know + if the next token is the "expected" one. It should be captured + internally by all exported procedures and methods. + """ + + msg: Optional[str] = None + + stk: list['Items'] = field(default_factory=list) + + def __str__(self) -> str: + s = f"{self.msg}\nTraceback of called parsers:\n" + for item in self.stk: + s += f'• {item}\n' + return s + + +class ParseDirective: + """ + Common type for special parsing directives. + + This is used for optional parsers, alternative parsers, and the like. + """ + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: + """ + Execute this directive. + + :param parser: + The formatter which holds the context to use. + """ + raise NotImplementedError(f'Missing run() for {self.__class__.__name__}') + + def __and__(self, other: Any) -> 'ParseDirective': + return and_(self, other) + + def __or__(self, other: Any) -> 'ParseDirective': + return or_(self, other) + + +Items: TypeAlias = Union[ + str, + None, + ParseDirective, + Callable[[], list[MatchObject]], + Sequence['Items'] +] + + +@dataclass +class s(ParseDirective): + """ + Wraps a thingy in a parser directive. + + This directive exactly matches the given string, and is mostly + here to wrap strings to allow infix operators. + """ + + s: Items + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102): + return parser.get(self.s) + + +@dataclass +class name(ParseDirective): + """Attach a name to a parser, purely for debugging.""" + + name: str + form: 'Items' + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + return parser.get(self.form) + + def __repr__(self) -> str: + return f'{self.name}' + + def __str__(self) -> str: + return repr(self) + + +@dataclass +class optional(ParseDirective): + """An optional parameter.""" + + form: 'Items' + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + try: + return parser.get(self.form) + except ParseError: + return [] + + def __repr__(self) -> str: + return f'optional({repr(self.form)})' + + +@dataclass +class and_(ParseDirective): + """Parse a group in order.""" + + items: list['Items'] + + def __init__(self, *items: 'Items'): + self.items = list(items) + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + snapshot = parser.snapshot() + out = [] + try: + for item in self.items: + out += parser.get(item) + except ParseError as e: + parser.restore(snapshot) + raise e + return out + + def __repr__(self) -> str: + return f'and_({", ".join(repr(x) for x in self.items)})' + + +@dataclass +class or_(ParseDirective): + """ + A set of multiple parse forms to try in order. + + The first matched one will be used. + + (Alias Variant) + """ + + alternatives: list['Items'] + + def __init__(self, *alternatives: Items): + """Retrun a new OptionParse object.""" + self.alternatives = list(alternatives) + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + save = parser.snapshot() + for alternative in self.alternatives: + try: + return parser.get(alternative) + except ParseError: + parser.restore(save) + else: + msg = f"No alternative suceeded, cases={self.alternatives}, seek={parser.seek}" + raise ParseError(msg) + + def __repr__(self) -> str: + return f'or_({", ".join(repr(x) for x in self.alternatives)})' + + +@dataclass +class count(ParseDirective): + """Run the given parser between min and max times.""" + + min: int + max: int + parser: Items + + def __init__(self, + parser: Items, + arg: int, + *args: int, + **kwargs: int): + """ + Run parser between ``min`` and ``max`` times. + + If only one numeric argument is given, then that's used as the + maximum count, but if two values are given, then the minimum count + preceeds the maximum. + + Both max and min can also be given as keyword arguments. + + :param parser: + :param min: + :param max: + + .. code-block:: python + + count(p, min, max) + + .. code-block:: python + + count(p, max) + """ + min_ = 0 + max_ = 1 + + match args: + case []: + max_ = arg + case [v]: + min_ = arg + max_ = v + + if 'min' in kwargs: + min_ = kwargs['min'] + if 'max' in kwargs: + max_ = kwargs['max'] + + self.min = min_ + self.max = max_ + self.parser = parser + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + snapshot = parser.snapshot() + out = [] + try: + # If any of these fails, then the whole operation was a fail. + for _ in range(self.min): + out += parser.get(self.parser) + + # These are optional, so an error here is "expected" + for _ in range(self.min, self.max): + try: + out += parser.get(self.parser) + except ParseError: + break + except Exception as e: + out = [] + parser.restore(snapshot) + raise e + return out + + def __repr__(self) -> str: + return f'count({repr(self.parser)}, {self.min}, {self.max})' + + +@dataclass +class CharParser(ParseDirective): + """Parse a single character.""" + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + out = [MatchObject('', parser.__source[parser.seek])] + parser.seek += 1 + return out + + def __repr__(self) -> str: + return 'char' + + +char = CharParser() + + +@dataclass +class many(ParseDirective): + """Many a parser as many times as possible.""" + + parser: 'Items' + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + out = [] + try: + while True: + entry = parser.get(self.parser) + if not entry: + raise ParseError() + # logger.info("seek = %s, parser = %s", + # parser.seek, self.parser) + out += entry + except ParseError: + return out + + def __repr__(self) -> str: + return f'many({repr(self.parser)})' + + +@dataclass +class complement(ParseDirective): + """Parses any character which isn't given.""" + + chars: str + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + match parser.peek(char): + case [MatchObject(matched=str(c))]: + if c not in self.chars: + return parser.get(char) + else: + raise ParseError() + case _: + raise ParseError() + + def __repr__(self) -> str: + return f'complement({repr(self.chars)})' + + +@dataclass +class tag(ParseDirective): + """Tag value returned by parser.""" + + tag: str + parser: 'Items' + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + result = parser.get(self.parser) + return [MatchObject(self.tag, result)] + + +@dataclass +class delimited(ParseDirective): + """ + Read an infix delimited list of items. + + If a optional trailing "comma" is wanted: + + .. code-block:: python + + and_(delimited(and_(ws, ',', ws), + ITEM), + optional(ws, ',')) + """ + + delim: 'Items' + parser: 'Items' + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + return parser.get(and_(self.parser, many(and_(self.delim, self.parser)))) + + +hexdig = name('hexdig', + or_(*([chr(x + ord('0')) for x in range(0, 10)] + + [chr(x + ord('A')) for x in range(0, 6)]))) + +space = or_(' ', '\t', '\n', '\r') + +ws = tag('ws', + name('ws', + # TODO tagging space with 'ws' locks us into an infinite loop + many(or_(many(tag('space', space)), + and_(tag('comment', and_('#', many(complement('\n')))), + '\n'))))) + + +class ParserCombinatorSnapshot: + """ + An opaque object storing the state of the ParserCombinator. + + This allows undoing any number of operations. Each parser snapshot + can only be used with the parser formatter which created it. + + A snapshot can be restored multiple times. + """ + + def __init__(self, seek: int, creator: 'ParserCombinator'): + self._seek = seek + self._creator = creator + + +class ParserCombinator: + """ + A basic parser combinator for Python. + + :param source: + The string which should be parsed. + :param file: + Optional name of the file being parsed. + """ + + # Required fields + # __source: str + # __seek: int = 0 + + # # Fields useful for better debugging info + # file: Optional[str] = None + + def __init__(self, source: str, file: Optional[str] = None): + self.__source = source + self.seek = 0 + self.file = file + + def peek(self, item: Items) -> list[MatchObject]: + """Run the parser without updating the state.""" + snapshot = self.snapshot() + result = self.get(item) + self.restore(snapshot) + return result + + def get(self, item: Items) -> list[MatchObject]: + """Like get, but dosn't seek through whitespace or comments.""" + out: list[MatchObject] = [] + try: + match item: + case [*entries]: + for entry in entries: + out += self.get(entry) + case str(s): + substr = self.__source[self.seek:][:len(s)] + # Always case fold when matching + if substr.lower() == s.lower(): + self.seek += len(s) + out += [MatchObject('', s)] + else: + raise ParseError(f'Expected {item!r}, got {substr!r} (char {self.seek})') + case None: + pass + case ParseDirective(): + out += item.run(self) + # TODO case Puppet(): + case other: + if callable(item): + out += item() + else: + raise ValueError(f"Unexpected item: {other}") + except ParseError as e: + e.stk.append(item) + raise e + return out + + def snapshot(self) -> ParserCombinatorSnapshot: + """Create a snapshot of the parsers current state.""" + return ParserCombinatorSnapshot( + seek=self.seek, + creator=self) + + def restore(self, snapshot: ParserCombinatorSnapshot) -> None: + """Restore a snapshot, altering the parsers state.""" + assert snapshot._creator == self + self.seek = snapshot._seek + + def peek_string(self, max_len: int) -> str: + """ + Return the upcomming string, for debugging purposes. + + :param max_len: + Maximum length to return, may be shorter if there isn't + enough characters left + """ + return self.__source[self.seek:][:max_len] diff --git a/muppet/puppet/ast.py b/muppet/puppet/ast.py index b527c23d1c8c1535a8fec20fd763c1d38046ef29..63127b94b7d14bdd5c8c552c886b3a1f6c109159 100644 --- a/muppet/puppet/ast.py +++ b/muppet/puppet/ast.py @@ -159,6 +159,7 @@ class PuppetClass(Puppet): name: str params: Optional[list[PuppetDeclarationParameter]] = None body: list[Puppet] = field(default_factory=list) + parent: Optional[str] = None @dataclass @@ -181,25 +182,28 @@ class PuppetCollect(Puppet): @dataclass -class PuppetIf(Puppet): +class PuppetIfChain(Puppet): """ - A puppet if expression. + Puppet if expressions. .. code-block:: puppet - if condition { - consequent + if a { + 1 + } elsif b { + 2 } else { - alretnative + 3 } - ``elsif`` is parsed as an else block with a single if expression - in it. + .. code-block:: python + + [('a', [1]), + ('b', [2]), + ('else', [3])] """ - condition: Puppet - consequent: list[Puppet] - alternative: Optional[list[Puppet]] = None + clauses: list[tuple[Puppet | Literal['else'], list[Puppet]]] @dataclass @@ -361,6 +365,7 @@ class PuppetNumber(Puppet): """A puppet numeric literal.""" x: int | float + radix: Optional[int] = None @dataclass @@ -402,6 +407,12 @@ class PuppetResourceOverride(Puppet): """ A resource override. + The resource attribute will most likely be a PuppetAccess object. + + .. code-block:: python + + PuppetAccess(how=PuppetQr(name='File'), args=[PuppetString(s='name')]) + .. code-block:: puppet File["name"] { @@ -462,6 +473,14 @@ class PuppetNode(Puppet): body: list[Puppet] +@dataclass +class PuppetTypeAlias(Puppet): + """A type alias.""" + + name: str + implementation: Puppet + + @dataclass class PuppetParseError(Puppet): """Anything we don't know how to handle.""" @@ -493,8 +512,8 @@ def parse_puppet_declaration_params(data: dict[str, dict[str, Any]]) \ value: Optional[Puppet] = None if t := definition.get('type'): type = build_ast(t) - if v := definition.get('value'): - value = build_ast(v) + if 'value' in definition: + value = build_ast(definition['value']) parameters.append(PuppetDeclarationParameter(k=name, v=value, type=type)) @@ -592,6 +611,9 @@ def build_ast(form: Any) -> Puppet: if b := rest.get('body'): args['body'] = [build_ast(x) for x in b] + if p := rest.get('parent'): + args['parent'] = p + # This is only valid for 'function', and simply will # never be true for the other cases. if r := rest.get('returns'): @@ -636,13 +658,19 @@ def build_ast(form: Any) -> Puppet: return build_ast(form) case ['if', {'test': test, **rest}]: - consequent = [] - alternative = None - if then := rest.get('then'): - consequent = [build_ast(x) for x in then] + clauses = [] + + clauses.append((build_ast(test), + [build_ast(x) for x in rest.get('then', [])])) + if els := rest.get('else'): - alternative = [build_ast(x) for x in els] - return PuppetIf(build_ast(test), consequent, alternative) + match [build_ast(x) for x in els]: + case [PuppetIfChain(cls)]: + clauses += cls + case xs: + clauses.append(('else', xs)) + + return PuppetIfChain(clauses) case ['unless', {'test': test, 'then': forms}]: return PuppetUnless(build_ast(test), [build_ast(x) for x in forms]) @@ -713,9 +741,15 @@ def build_ast(form: Any) -> Puppet: return PuppetNode([build_ast(x) for x in matches], [build_ast(x) for x in body]) + case ['type-alias', str(name), implementation]: + return PuppetTypeAlias(name, build_ast(implementation)) + case str(s): return PuppetString(s) + case ['int', {'radix': radix, 'value': value}]: + return PuppetNumber(value, radix) + case int(x): return PuppetNumber(x) case float(x): return PuppetNumber(x) diff --git a/muppet/puppet/format/__init__.py b/muppet/puppet/format/__init__.py index d40f9b4252cdd252593b75dadd4673269d422447..d30eb7d3977dde8d3e852a9e412753bfe661c5ef 100644 --- a/muppet/puppet/format/__init__.py +++ b/muppet/puppet/format/__init__.py @@ -1,8 +1,9 @@ """Fromat Puppet AST's into something useful.""" from .base import Serializer +from muppet.parser_combinator import MatchObject from muppet.puppet.ast import Puppet -from typing import TypeVar +from typing import Any, TypeVar T = TypeVar('T') @@ -10,4 +11,17 @@ T = TypeVar('T') def serialize(ast: Puppet, serializer: type[Serializer[T]]) -> T: """Run the given serializer on the given data.""" - return serializer().serialize(ast, 0) + return serializer().serialize(ast) + + +def to_string(t: Any) -> str: + """Turn a serialized structure into a string.""" + match t: + case str(s): + return s + case MatchObject(): + return t.serialize() + case [*xs]: + return ''.join(to_string(x) for x in xs) + case _: + raise ValueError() diff --git a/muppet/puppet/format/base.py b/muppet/puppet/format/base.py index 9afad2b92e0bae75a79db04ba44fe332b199a6ea..b24fd3346f3f2729f3d3e3604be44306466294d6 100644 --- a/muppet/puppet/format/base.py +++ b/muppet/puppet/format/base.py @@ -14,14 +14,14 @@ from muppet.puppet.ast import ( PuppetUnaryOperator, PuppetArray, PuppetCallMethod, PuppetCase, PuppetDeclarationParameter, PuppetInstanciationParameter, PuppetClass, PuppetConcat, - PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword, + PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword, PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction, PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar, PuppetLambda, PuppetQn, PuppetQr, PuppetRegex, PuppetResource, PuppetDefine, PuppetString, PuppetNumber, PuppetInvoke, PuppetResourceDefaults, PuppetResourceOverride, PuppetDeclaration, PuppetSelector, - PuppetBlock, PuppetNode, + PuppetBlock, PuppetNode, PuppetTypeAlias, PuppetCall, PuppetParenthesis, PuppetNop, # HashEntry, @@ -33,6 +33,9 @@ T = TypeVar('T') logger = logging.getLogger(__name__) +# TODO flag everything as @abstractmethod + + class Serializer(Generic[T]): """ Base class for serialization. @@ -44,253 +47,221 @@ class Serializer(Generic[T]): by each instance. """ - @classmethod - def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> T: - raise NotImplementedError("puppet_literal must be implemented by subclass") + def __raise(self, proc: str) -> T: + msg = f"{proc} must be implemented by subclass {self.__class__.__name__}" + raise NotImplementedError(msg) + + def _puppet_literal(self, it: PuppetLiteral) -> T: + return self.__raise("puppet_literal") + + def _puppet_access(self, it: PuppetAccess) -> T: + return self.__raise("puppet_access") - @classmethod - def _puppet_access(cls, it: PuppetAccess, indent: int) -> T: - raise NotImplementedError("puppet_access must be implemented by subclass") + def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> T: + return self.__raise("puppet_binary_operator") - @classmethod - def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> T: - raise NotImplementedError("puppet_binary_operator must be implemented by subclass") + def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> T: + return self.__raise("puppet_unary_operator") - @classmethod - def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> T: - raise NotImplementedError("puppet_unary_operator must be implemented by subclass") + def _puppet_array(self, it: PuppetArray) -> T: + return self.__raise("puppet_array") - @classmethod - def _puppet_array(cls, it: PuppetArray, indent: int) -> T: - raise NotImplementedError("puppet_array must be implemented by subclass") + def _puppet_call(self, it: PuppetCall) -> T: + return self.__raise("puppet_call") - @classmethod - def _puppet_call(cls, it: PuppetCall, indent: int) -> T: - raise NotImplementedError("puppet_call must be implemented by subclass") + def _puppet_call_method(self, it: PuppetCallMethod) -> T: + return self.__raise("puppet_call_method") - @classmethod - def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> T: - raise NotImplementedError("puppet_call_method must be implemented by subclass") + def _puppet_case(self, it: PuppetCase) -> T: + return self.__raise("puppet_case") - @classmethod - def _puppet_case(cls, it: PuppetCase, indent: int) -> T: - raise NotImplementedError("puppet_case must be implemented by subclass") + def _puppet_declaration_parameter(self, it: PuppetDeclarationParameter) -> T: + return self.__raise("puppet_declaration_parameter") - @classmethod - def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> T: - raise NotImplementedError("puppet_declaration_parameter must be implemented by subclass") + def _puppet_instanciation_parameter(self, it: PuppetInstanciationParameter) -> T: + return self.__raise("puppet_instanciation_parameter") - @classmethod - def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> T: - raise NotImplementedError("puppet_instanciation_parameter must be implemented by subclass") + def _puppet_class(self, it: PuppetClass) -> T: + return self.__raise("puppet_class") - @classmethod - def _puppet_class(cls, it: PuppetClass, indent: int) -> T: - raise NotImplementedError("puppet_class must be implemented by subclass") + def _puppet_concat(self, it: PuppetConcat) -> T: + return self.__raise("puppet_concat") - @classmethod - def _puppet_concat(cls, it: PuppetConcat, indent: int) -> T: - raise NotImplementedError("puppet_concat must be implemented by subclass") + def _puppet_collect(self, it: PuppetCollect) -> T: + return self.__raise("puppet_collect") - @classmethod - def _puppet_collect(cls, it: PuppetCollect, indent: int) -> T: - raise NotImplementedError("puppet_collect must be implemented by subclass") + def _puppet_if_chain(self, it: PuppetIfChain) -> T: + return self.__raise("puppet_if_chain") - @classmethod - def _puppet_if(cls, it: PuppetIf, indent: int) -> T: - raise NotImplementedError("puppet_if must be implemented by subclass") + def _puppet_unless(self, it: PuppetUnless) -> T: + return self.__raise("puppet_unless") - @classmethod - def _puppet_unless(cls, it: PuppetUnless, indent: int) -> T: - raise NotImplementedError("puppet_unless must be implemented by subclass") + def _puppet_keyword(self, it: PuppetKeyword) -> T: + return self.__raise("puppet_keyword") - @classmethod - def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> T: - raise NotImplementedError("puppet_keyword must be implemented by subclass") + def _puppet_exported_query(self, it: PuppetExportedQuery) -> T: + return self.__raise("puppet_exported_query") - @classmethod - def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> T: - raise NotImplementedError("puppet_exported_query must be implemented by subclass") + def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> T: + return self.__raise("puppet_virtual_query") - @classmethod - def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> T: - raise NotImplementedError("puppet_virtual_query must be implemented by subclass") + def _puppet_function(self, it: PuppetFunction) -> T: + return self.__raise("puppet_function") - @classmethod - def _puppet_function(cls, it: PuppetFunction, indent: int) -> T: - raise NotImplementedError("puppet_function must be implemented by subclass") + def _puppet_hash(self, it: PuppetHash) -> T: + return self.__raise("puppet_hash") - @classmethod - def _puppet_hash(cls, it: PuppetHash, indent: int) -> T: - raise NotImplementedError("puppet_hash must be implemented by subclass") + def _puppet_heredoc(self, it: PuppetHeredoc) -> T: + return self.__raise("puppet_heredoc") - @classmethod - def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> T: - raise NotImplementedError("puppet_heredoc must be implemented by subclass") + def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> T: + return self.__raise("puppet_literal_heredoc") - @classmethod - def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> T: - raise NotImplementedError("puppet_literal_heredoc must be implemented by subclass") + def _puppet_var(self, it: PuppetVar) -> T: + return self.__raise("puppet_var") - @classmethod - def _puppet_var(cls, it: PuppetVar, indent: int) -> T: - raise NotImplementedError("puppet_var must be implemented by subclass") + def _puppet_lambda(self, it: PuppetLambda) -> T: + return self.__raise("puppet_lambda") - @classmethod - def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> T: - raise NotImplementedError("puppet_lambda must be implemented by subclass") + def _puppet_qn(self, it: PuppetQn) -> T: + return self.__raise("puppet_qn") - @classmethod - def _puppet_qn(cls, it: PuppetQn, indent: int) -> T: - raise NotImplementedError("puppet_qn must be implemented by subclass") + def _puppet_qr(self, it: PuppetQr) -> T: + return self.__raise("puppet_qr") - @classmethod - def _puppet_qr(cls, it: PuppetQr, indent: int) -> T: - raise NotImplementedError("puppet_qr must be implemented by subclass") + def _puppet_regex(self, it: PuppetRegex) -> T: + return self.__raise("puppet_regex") - @classmethod - def _puppet_regex(cls, it: PuppetRegex, indent: int) -> T: - raise NotImplementedError("puppet_regex must be implemented by subclass") + def _puppet_resource(self, it: PuppetResource) -> T: + return self.__raise("puppet_resource") - @classmethod - def _puppet_resource(cls, it: PuppetResource, indent: int) -> T: - raise NotImplementedError("puppet_resource must be implemented by subclass") + def _puppet_define(self, it: PuppetDefine) -> T: + return self.__raise("puppet_define") - @classmethod - def _puppet_define(cls, it: PuppetDefine, indent: int) -> T: - raise NotImplementedError("puppet_define must be implemented by subclass") + def _puppet_string(self, it: PuppetString) -> T: + return self.__raise("puppet_string") - @classmethod - def _puppet_string(cls, it: PuppetString, indent: int) -> T: - raise NotImplementedError("puppet_string must be implemented by subclass") + def _puppet_number(self, it: PuppetNumber) -> T: + return self.__raise("puppet_number") - @classmethod - def _puppet_number(cls, it: PuppetNumber, indent: int) -> T: - raise NotImplementedError("puppet_number must be implemented by subclass") + def _puppet_invoke(self, it: PuppetInvoke) -> T: + return self.__raise("puppet_invoke") - @classmethod - def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> T: - raise NotImplementedError("puppet_invoke must be implemented by subclass") + def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> T: + return self.__raise("puppet_resource_defaults") - @classmethod - def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> T: - raise NotImplementedError("puppet_resource_defaults must be implemented by subclass") + def _puppet_resource_override(self, it: PuppetResourceOverride) -> T: + return self.__raise("puppet_resource_override") - @classmethod - def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> T: - raise NotImplementedError("puppet_resource_override must be implemented by subclass") + def _puppet_declaration(self, it: PuppetDeclaration) -> T: + return self.__raise("puppet_declaration") - @classmethod - def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> T: - raise NotImplementedError("puppet_declaration must be implemented by subclass") + def _puppet_selector(self, it: PuppetSelector) -> T: + return self.__raise("puppet_selector") - @classmethod - def _puppet_selector(cls, it: PuppetSelector, indent: int) -> T: - raise NotImplementedError("puppet_selector must be implemented by subclass") + def _puppet_block(self, it: PuppetBlock) -> T: + return self.__raise("puppet_block") - @classmethod - def _puppet_block(cls, it: PuppetBlock, indent: int) -> T: - raise NotImplementedError("puppet_block must be implemented by subclass") + def _puppet_node(self, it: PuppetNode) -> T: + return self.__raise("puppet_node") - @classmethod - def _puppet_node(cls, it: PuppetNode, indent: int) -> T: - raise NotImplementedError("puppet_node must be implemented by subclass") + def _puppet_type_alias(self, it: PuppetTypeAlias) -> T: + return self.__raise("type_alias") - @classmethod - def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> T: - raise NotImplementedError("puppet_parenthesis must be implemented by subclass") + def _puppet_parenthesis(self, it: PuppetParenthesis) -> T: + return self.__raise("puppet_parenthesis") - @classmethod - def _puppet_nop(cls, it: PuppetNop, indent: int) -> T: - raise NotImplementedError("puppet_nop must be implemented by subclass") + def _puppet_nop(self, it: PuppetNop) -> T: + return self.__raise("puppet_nop") @final - @classmethod - def serialize(cls, form: Puppet, indent: int) -> T: + def serialize(self, form: Puppet) -> T: """Dispatch depending on type.""" match form: case PuppetLiteral(): - return cls._puppet_literal(form, indent) + return self._puppet_literal(form) case PuppetAccess(): - return cls._puppet_access(form, indent) + return self._puppet_access(form) case PuppetBinaryOperator(): - return cls._puppet_binary_operator(form, indent) + return self._puppet_binary_operator(form) case PuppetUnaryOperator(): - return cls._puppet_unary_operator(form, indent) + return self._puppet_unary_operator(form) case PuppetUnaryOperator(): - return cls._puppet_unary_operator(form, indent) + return self._puppet_unary_operator(form) case PuppetArray(): - return cls._puppet_array(form, indent) + return self._puppet_array(form) case PuppetCall(): - return cls._puppet_call(form, indent) + return self._puppet_call(form) case PuppetCallMethod(): - return cls._puppet_call_method(form, indent) + return self._puppet_call_method(form) case PuppetCase(): - return cls._puppet_case(form, indent) + return self._puppet_case(form) case PuppetDeclarationParameter(): - return cls._puppet_declaration_parameter(form, indent) + return self._puppet_declaration_parameter(form) case PuppetInstanciationParameter(): - return cls._puppet_instanciation_parameter(form, indent) + return self._puppet_instanciation_parameter(form) case PuppetClass(): - return cls._puppet_class(form, indent) + return self._puppet_class(form) case PuppetConcat(): - return cls._puppet_concat(form, indent) + return self._puppet_concat(form) case PuppetCollect(): - return cls._puppet_collect(form, indent) - case PuppetIf(): - return cls._puppet_if(form, indent) + return self._puppet_collect(form) + case PuppetIfChain(): + return self._puppet_if_chain(form) case PuppetUnless(): - return cls._puppet_unless(form, indent) + return self._puppet_unless(form) case PuppetKeyword(): - return cls._puppet_keyword(form, indent) + return self._puppet_keyword(form) case PuppetExportedQuery(): - return cls._puppet_exported_query(form, indent) + return self._puppet_exported_query(form) case PuppetVirtualQuery(): - return cls._puppet_virtual_query(form, indent) + return self._puppet_virtual_query(form) case PuppetFunction(): - return cls._puppet_function(form, indent) + return self._puppet_function(form) case PuppetHash(): - return cls._puppet_hash(form, indent) + return self._puppet_hash(form) case PuppetHeredoc(): - return cls._puppet_heredoc(form, indent) + return self._puppet_heredoc(form) case PuppetLiteralHeredoc(): - return cls._puppet_literal_heredoc(form, indent) + return self._puppet_literal_heredoc(form) case PuppetVar(): - return cls._puppet_var(form, indent) + return self._puppet_var(form) case PuppetLambda(): - return cls._puppet_lambda(form, indent) + return self._puppet_lambda(form) case PuppetQn(): - return cls._puppet_qn(form, indent) + return self._puppet_qn(form) case PuppetQr(): - return cls._puppet_qr(form, indent) + return self._puppet_qr(form) case PuppetRegex(): - return cls._puppet_regex(form, indent) + return self._puppet_regex(form) case PuppetResource(): - return cls._puppet_resource(form, indent) + return self._puppet_resource(form) case PuppetDefine(): - return cls._puppet_define(form, indent) + return self._puppet_define(form) case PuppetString(): - return cls._puppet_string(form, indent) + return self._puppet_string(form) case PuppetNumber(): - return cls._puppet_number(form, indent) + return self._puppet_number(form) case PuppetInvoke(): - return cls._puppet_invoke(form, indent) + return self._puppet_invoke(form) case PuppetResourceDefaults(): - return cls._puppet_resource_defaults(form, indent) + return self._puppet_resource_defaults(form) case PuppetResourceOverride(): - return cls._puppet_resource_override(form, indent) + return self._puppet_resource_override(form) case PuppetDeclaration(): - return cls._puppet_declaration(form, indent) + return self._puppet_declaration(form) case PuppetSelector(): - return cls._puppet_selector(form, indent) + return self._puppet_selector(form) case PuppetBlock(): - return cls._puppet_block(form, indent) + return self._puppet_block(form) case PuppetNode(): - return cls._puppet_node(form, indent) + return self._puppet_node(form) case PuppetParenthesis(): - return cls._puppet_parenthesis(form, indent) + return self._puppet_parenthesis(form) + case PuppetTypeAlias(): + return self._puppet_type_alias(form) case PuppetNop(): - return cls._puppet_nop(form, indent) + return self._puppet_nop(form) case _: logger.warn("Unexpected form: %s", form) raise ValueError(f'Unexpected: {form}') diff --git a/muppet/puppet/format/html.py b/muppet/puppet/format/html.py index 9719b534910ec6d72f5fd36be7231a202c236a1b..d7b887b485821b8d458db95a9ec5f5f0bff87563 100644 --- a/muppet/puppet/format/html.py +++ b/muppet/puppet/format/html.py @@ -17,7 +17,7 @@ from muppet.puppet.ast import ( PuppetUnaryOperator, PuppetArray, PuppetCallMethod, PuppetCase, PuppetDeclarationParameter, PuppetInstanciationParameter, PuppetClass, PuppetConcat, - PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword, + PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword, PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction, PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar, PuppetLambda, PuppetQn, PuppetQr, PuppetRegex, @@ -34,7 +34,6 @@ import html from .text import ( override, find_heredoc_delimiter, - ind, string_width, ) @@ -80,25 +79,39 @@ def number(x: str) -> str: class HTMLFormatter(Serializer[str]): """AST formatter returning source code.""" - @classmethod + def __init__(self, indent: int = 0): + self.__indent = indent + + def indent(self, change: int) -> 'HTMLFormatter': + """Return the current context, with an updated indentation level.""" + return self.__class__(indent=(self.__indent + change)) + + def ind(self, change: int = 0) -> str: + """ + Return indentation for current context. + + :param change: + Extra indentation level to add to this output. + """ + return (self.__indent + change) * 2 * ' ' + def format_declaration_parameter( - cls, + self, param: PuppetDeclarationParameter, - indent: int) -> str: + ) -> str: """Format a single declaration parameter.""" out: str = '' if param.type: - out += f'{cls.serialize(param.type, indent + 1)} ' + out += f'{self.indent(1).serialize(param.type)} ' out += var(f'${param.k}') if param.v: - out += f' = {cls.serialize(param.v, indent + 1)}' + out += f' = {self.indent(1).serialize(param.v)}' return out - @classmethod def format_declaration_parameters( - cls, + self, lst: list[PuppetDeclarationParameter], - indent: int) -> str: + ) -> str: """ Print declaration parameters. @@ -109,121 +122,112 @@ class HTMLFormatter(Serializer[str]): out = ' (\n' for param in lst: - out += ind(indent + 1) + cls.format_declaration_parameter(param, indent + 1) + ',\n' - out += ind(indent) + ')' + out += self.ind(1) + self.indent(1).format_declaration_parameter(param) + ',\n' + out += self.ind() + ')' return out - @classmethod def serialize_hash_entry( - cls, + self, entry: HashEntry, - indent: int) -> str: + ) -> str: """Return a hash entry as a string.""" - return f'{cls.serialize(entry.k, indent + 1)} => {cls.serialize(entry.v, indent + 2)}' + out = f'{self.indent(1).serialize(entry.k)}' + out += f' => {self.indent(2).serialize(entry.v)}' + return out @override - @classmethod - def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> str: + def _puppet_literal(self, it: PuppetLiteral) -> str: return literal(it.literal) @override - @classmethod - def _puppet_access(cls, it: PuppetAccess, indent: int) -> str: - args = ', '.join(cls.serialize(x, indent) for x in it.args) + def _puppet_access(self, it: PuppetAccess) -> str: + args = ', '.join(self.serialize(x) for x in it.args) - return f'{cls.serialize(it.how, indent)}[{args}]' + return f'{self.serialize(it.how)}[{args}]' @override - @classmethod - def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> str: - out = cls.serialize(it.lhs, indent) + def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> str: + out = self.serialize(it.lhs) out += f' {op(it.op)} ' - out += cls.serialize(it.rhs, indent) + out += self.serialize(it.rhs) return out @override - @classmethod - def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> str: - return f'{op(it.op)} {cls.serialize(it.x, indent)}' + def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> str: + return f'{op(it.op)} {self.serialize(it.x)}' @override - @classmethod - def _puppet_array(cls, it: PuppetArray, indent: int) -> str: + def _puppet_array(self, it: PuppetArray) -> str: if not it.items: return '[]' else: out = '[\n' for item in it.items: - out += ind(indent + 1) + cls.serialize(item, indent + 2) + ',\n' - out += ind(indent) + ']' + out += self.ind(1) + self.indent(2).serialize(item) + ',\n' + out += self.ind() + ']' return out @override - @classmethod - def _puppet_call(cls, it: PuppetCall, indent: int) -> str: - args = ', '.join(cls.serialize(x, indent) for x in it.args) - return f'{cls.serialize(it.func, indent)}({args})' + def _puppet_call(self, it: PuppetCall) -> str: + args = ', '.join(self.serialize(x) for x in it.args) + return f'{self.serialize(it.func)}({args})' @override - @classmethod - def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> str: - out: str = cls.serialize(it.func, indent) + def _puppet_call_method(self, it: PuppetCallMethod) -> str: + out: str = self.serialize(it.func) if it.args: - args = ', '.join(cls.serialize(x, indent) for x in it.args) + args = ', '.join(self.serialize(x) for x in it.args) out += f' ({args})' if it.block: - out += cls.serialize(it.block, indent) + out += self.serialize(it.block) return out @override - @classmethod - def _puppet_case(cls, it: PuppetCase, indent: int) -> str: - out: str = f'{keyword("case")} {cls.serialize(it.test, indent)} {{\n' + def _puppet_case(self, it: PuppetCase) -> str: + out: str = f'{keyword("case")} {self.serialize(it.test)} {{\n' for (when, body) in it.cases: - out += ind(indent + 1) - out += ', '.join(cls.serialize(x, indent + 1) for x in when) + out += self.ind(1) + out += ', '.join(self.indent(1).serialize(x) for x in when) out += ': {\n' for item in body: - out += ind(indent + 2) + cls.serialize(item, indent + 2) + '\n' - out += ind(indent + 1) + '}\n' - out += ind(indent) + '}' + out += self.ind(2) + self.indent(2).serialize(item) + '\n' + out += self.ind(1) + '}\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> str: + def _puppet_declaration_parameter( + self, it: PuppetDeclarationParameter) -> str: out: str = '' if it.type: - out += f'{cls.serialize(it.type, indent + 1)} ' + out += f'{self.indent(1).serialize(it.type)} ' out += var(f'${it.k}') if it.v: - out += f' = {cls.serialize(it.v, indent + 1)}' + out += f' = {self.indent(1).serialize(it.v)}' return out @override - @classmethod - def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> str: - return f'{it.k} {it.arrow} {cls.serialize(it.v, indent)}' + def _puppet_instanciation_parameter( + self, it: PuppetInstanciationParameter) -> str: + return f'{it.k} {it.arrow} {self.serialize(it.v)}' @override - @classmethod - def _puppet_class(cls, it: PuppetClass, indent: int) -> str: + def _puppet_class(self, it: PuppetClass) -> str: out: str = f'{keyword("class")} {it.name}' if it.params: - out += cls.format_declaration_parameters(it.params, indent) + out += self.format_declaration_parameters(it.params) out += ' {\n' for form in it.body: - out += ind(indent+1) + cls.serialize(form, indent+1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_concat(cls, it: PuppetConcat, indent: int) -> str: + def _puppet_concat(self, it: PuppetConcat) -> str: out = '"' for item in it.fragments: match item: @@ -232,97 +236,94 @@ class HTMLFormatter(Serializer[str]): case PuppetVar(x): out += var(f"${{{x}}}") case puppet: - out += f"${{{cls.serialize(puppet, indent)}}}" + out += f"${{{self.serialize(puppet)}}}" out += '"' # Don't escape `out`, since it contains sub-expressions return f'<span class="string">{out}</span>' @override - @classmethod - def _puppet_collect(cls, it: PuppetCollect, indent: int) -> str: - return f'{cls.serialize(it.type, indent)} {cls.serialize(it.query, indent + 1)}' - - @override - @classmethod - def _puppet_if(cls, it: PuppetIf, indent: int) -> str: - out: str = f'{keyword("if")} {cls.serialize(it.condition, indent)} {{\n' - for item in it.consequent: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' - if alts := it.alternative: - # TODO elsif - out += f' {keyword("else")} {{\n' - for item in alts: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' + def _puppet_collect(self, it: PuppetCollect) -> str: + return f'{self.indent(1).serialize(it.type)} {self.serialize(it.query)}' + + @override + def _puppet_if(self, it: PuppetIfChain) -> str: + (test1, body1), *rest = it.clauses + assert test1 != 'else' + out: str = f'{keyword("if")} {self.serialize(test1)} {{\n' + for item in body1: + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' + for (testn, bodyn) in rest: + out += ' ' + if testn == 'else': + out += keyword('else') + else: + out += f'{keyword("elsif")} {self.serialize(testn)}' + out += ' {' + for item in bodyn: + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_unless(cls, it: PuppetUnless, indent: int) -> str: - out: str = f'{keyword("unless")} {cls.serialize(it.condition, indent)} {{\n' + def _puppet_unless(self, it: PuppetUnless) -> str: + out: str = f'{keyword("unless")} {self.serialize(it.condition)} {{\n' for item in it.consequent: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> str: + def _puppet_keyword(self, it: PuppetKeyword) -> str: return it.name @override - @classmethod - def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> str: + def _puppet_exported_query(self, it: PuppetExportedQuery) -> str: out: str = op('<<|') if f := it.filter: - out += ' ' + cls.serialize(f, indent) + out += ' ' + self.serialize(f) out += ' ' + op('|>>') return out @override - @classmethod - def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> str: + def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> str: out: str = op('<|') if f := it.q: - out += ' ' + cls.serialize(f, indent) + out += ' ' + self.serialize(f) out += ' ' + op('|>') return out @override - @classmethod - def _puppet_function(cls, it: PuppetFunction, indent: int) -> str: + def _puppet_function(self, it: PuppetFunction) -> str: out: str = f'{keyword("function")} {it.name}' if it.params: - out += cls.format_declaration_parameters(it.params, indent) + out += self.format_declaration_parameters(it.params) if ret := it.returns: - out += f' {op(">>")} {cls.serialize(ret, indent + 1)}' + out += f' {op(">>")} {self.indent(1).serialize(ret)}' out += ' {\n' for item in it.body: - out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_hash(cls, it: PuppetHash, indent: int) -> str: + def _puppet_hash(self, it: PuppetHash) -> str: if not it.entries: return '{}' else: out: str = '{\n' for item in it.entries: - out += ind(indent + 1) - out += cls.serialize_hash_entry(item, indent + 1) + out += self.ind(1) + out += self.indent(1).serialize_hash_entry(item) out += ',\n' - out += ind(indent) + '}' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> str: + def _puppet_heredoc(self, it: PuppetHeredoc) -> str: """ Serialize heredoc with interpolation. @@ -347,7 +348,7 @@ class HTMLFormatter(Serializer[str]): case PuppetVar(x): body += f'${{{x}}}' case p: - body += cls.serialize(p, indent + 2) + body += self.indent(2).serialize(p) # Check if string ends with a newline match it.fragments[-1]: @@ -362,8 +363,7 @@ class HTMLFormatter(Serializer[str]): return f'@("EOF"{syntax}/$rt)\n{body}\n|{eol_marker} EOF' @override - @classmethod - def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> str: + def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> str: syntax: str = '' if it.syntax: syntax = f':{it.syntax}' @@ -371,7 +371,7 @@ class HTMLFormatter(Serializer[str]): out: str = '' if not it.content: out += f'@(EOF{syntax})\n' - out += ind(indent) + '|- EOF' + out += self.ind() + '|- EOF' return out delimiter = find_heredoc_delimiter(it.content) @@ -385,9 +385,9 @@ class HTMLFormatter(Serializer[str]): eol = True for line in lines: - out += ind(indent + 1) + line + '\n' + out += self.ind(1) + line + '\n' - out += ind(indent + 1) + '|' + out += self.ind(1) + '|' if not eol: out += '-' @@ -397,153 +397,135 @@ class HTMLFormatter(Serializer[str]): return out @override - @classmethod - def _puppet_var(cls, it: PuppetVar, indent: int) -> str: + def _puppet_var(self, it: PuppetVar) -> str: return var(f'${it.name}') @override - @classmethod - def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> str: + def _puppet_lambda(self, it: PuppetLambda) -> str: out: str = '|' for item in it.params: out += 'TODO' out += '| {' for form in it.body: - out += ind(indent + 1) + cls.serialize(form, indent + 1) - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + out += self.ind() + '}' return out @override - @classmethod - def _puppet_qn(cls, it: PuppetQn, indent: int) -> str: + def _puppet_qn(self, it: PuppetQn) -> str: return span('qn', it.name) @override - @classmethod - def _puppet_qr(cls, it: PuppetQr, indent: int) -> str: + def _puppet_qr(self, it: PuppetQr) -> str: return span('qn', it.name) @override - @classmethod - def _puppet_regex(cls, it: PuppetRegex, indent: int) -> str: + def _puppet_regex(self, it: PuppetRegex) -> str: return span('regex', f'/{it.s}/') @override - @classmethod - def _puppet_resource(cls, it: PuppetResource, indent: int) -> str: - out = f'{cls.serialize(it.type, indent + 1)} {{' + def _puppet_resource(self, it: PuppetResource) -> str: + out = f'{self.indent(1).serialize(it.type)} {{' match it.bodies: case [(name, values)]: - out += f' {cls.serialize(name, indent + 1)}:\n' + out += f' {self.indent(1).serialize(name)}:\n' for v in values: - out += ind(indent + 1) + cls.serialize(v, indent + 2) + ',\n' + out += self.ind(1) + self.indent(2).serialize(v) + ',\n' case bodies: out += '\n' for (name, values) in bodies: - out += f'{ind(indent + 1)}{cls.serialize(name, indent + 1)}:\n' + out += f'{self.ind(1)}{self.indent(1).serialize(name)}:\n' for v in values: - out += ind(indent + 2) + cls.serialize(v, indent + 3) + ',\n' - out += ind(indent + 2) + ';\n' - out += ind(indent) + '}' + out += self.ind(2) + self.indent(3).serialize(v) + ',\n' + out += self.ind(2) + ';\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_define(cls, it: PuppetDefine, indent: int) -> str: + def _puppet_define(self, it: PuppetDefine) -> str: out: str = f'{keyword("define")} {it.name}' if params := it.params: - out += cls.format_declaration_parameters(params, indent) + out += self.format_declaration_parameters(params) out += ' {\n' for form in it.body: - out += ind(indent + 1) + cls.serialize(form, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_string(cls, it: PuppetString, indent: int) -> str: + def _puppet_string(self, it: PuppetString) -> str: # TODO escaping return string(f"'{it.s}'") @override - @classmethod - def _puppet_number(cls, it: PuppetNumber, indent: int) -> str: + def _puppet_number(self, it: PuppetNumber) -> str: return number(str(it.x)) @override - @classmethod - def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> str: - invoker = f'{cls.serialize(it.func, indent)}' + def _puppet_invoke(self, it: PuppetInvoke) -> str: + invoker = f'{self.serialize(it.func)}' out: str = invoker template: str if invoker == keyword('include'): template = ' {}' else: template = '({})' - out += template.format(', '.join(cls.serialize(x, indent + 1) for x in it.args)) + out += template.format(', '.join(self.indent(1).serialize(x) for x in it.args)) return out @override - @classmethod - def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> str: - out: str = f'{cls.serialize(it.type, indent)} {{\n' + def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> str: + out: str = f'{self.serialize(it.type)} {{\n' for op in it.ops: - out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(op) + ',\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> str: - out: str = f'{cls.serialize(it.resource, indent)} {{\n' + def _puppet_resource_override(self, it: PuppetResourceOverride) -> str: + out: str = f'{self.serialize(it.resource)} {{\n' for op in it.ops: - out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(op) + ',\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> str: - return f'{cls.serialize(it.k, indent)} = {cls.serialize(it.v, indent)}' + def _puppet_declaration(self, it: PuppetDeclaration) -> str: + return f'{self.serialize(it.k)} = {self.serialize(it.v)}' @override - @classmethod - def _puppet_selector(cls, it: PuppetSelector, indent: int) -> str: - out: str = f'{cls.serialize(it.resource, indent)} ? {{\n' - rendered_cases = [(cls.serialize(test, indent + 1), - cls.serialize(body, indent + 2)) + def _puppet_selector(self, it: PuppetSelector) -> str: + out: str = f'{self.serialize(it.resource)} ? {{\n' + rendered_cases = [(self.indent(1).serialize(test), + self.indent(2).serialize(body)) for (test, body) in it.cases] - case_width = max(string_width(c[0], indent + 1) for c in rendered_cases) + case_width = max(string_width(c[0], self.__indent + 1) for c in rendered_cases) for (test, body) in rendered_cases: - out += ind(indent + 1) + test - out += ' ' * (case_width - string_width(test, indent + 1)) + out += self.ind(1) + test + out += ' ' * (case_width - string_width(test, self.__indent + 1)) out += f' => {body},\n' - out += ind(indent) + '}' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_block(cls, it: PuppetBlock, indent: int) -> str: - return '\n'.join(cls.serialize(x, indent) for x in it.entries) + def _puppet_block(self, it: PuppetBlock) -> str: + return '\n'.join(self.serialize(x) for x in it.entries) @override - @classmethod - def _puppet_node(cls, it: PuppetNode, indent: int) -> str: + def _puppet_node(self, it: PuppetNode) -> str: out: str = keyword('node') + ' ' - out += ', '.join(cls.serialize(x, indent) for x in it.matches) + out += ', '.join(self.serialize(x) for x in it.matches) out += ' {\n' for item in it.body: - out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> str: - return f'({cls.serialize(it.form, indent)})' + def _puppet_parenthesis(self, it: PuppetParenthesis) -> str: + return f'({self.serialize(it.form)})' @override - @classmethod - def _puppet_nop(cls, it: PuppetNop, indent: int) -> str: + def _puppet_nop(self, it: PuppetNop) -> str: return '' diff --git a/muppet/puppet/format/parser.py b/muppet/puppet/format/parser.py new file mode 100644 index 0000000000000000000000000000000000000000..ad4819137dfd05b9583b50776824d5f46e49ebb1 --- /dev/null +++ b/muppet/puppet/format/parser.py @@ -0,0 +1,566 @@ +""" +"Parser" which instead "parses" the source code, and annotates it. + +This is basically a parser combinator, implementing the grammar for +the exact file we were created from. While this might seem worthless, +this allows *really* good syntax highlighting. +""" + +import logging +from .base import Serializer +from muppet.puppet.ast import ( + Puppet, + PuppetAccess, + PuppetArray, + PuppetBinaryOperator, + PuppetBlock, + PuppetCall, + PuppetCallMethod, + PuppetCase, + PuppetClass, + PuppetCollect, + PuppetConcat, + PuppetDeclaration, + PuppetDefine, + PuppetExportedQuery, + PuppetFunction, + PuppetHash, + PuppetHeredoc, + PuppetIfChain, + PuppetInstanciationParameter, + PuppetInvoke, + PuppetKeyword, + PuppetLambda, + PuppetLiteral, + PuppetLiteralHeredoc, + PuppetNode, + PuppetNop, + PuppetNumber, + PuppetParenthesis, + PuppetParseError, + PuppetQn, + PuppetQr, + PuppetRegex, + PuppetResource, + PuppetResourceDefaults, + PuppetResourceOverride, + PuppetSelector, + PuppetString, + PuppetUnaryOperator, + PuppetUnless, + PuppetVar, + PuppetVirtualQuery, + + PuppetDeclarationParameter, +) + +from muppet.parser_combinator import ( + MatchObject, + ParseError, + ParseDirective, + # Items, + # name, + optional, + count, + # char, + many, + complement, + tag, + hexdig, + # space, + ws, + ParserCombinator, + s, + stringify_match, +) + +from typing import ( + Callable, + TypeVar, + Optional, +) + +from dataclasses import dataclass + + +F = TypeVar('F', bound=Callable[..., object]) + +# TODO replace this decorator with +# from typing import override +# once the target python version is changed to 3.12 + + +def override(f: F) -> F: + """ + Return function unchanged. + + Placeholder @override annotator if the actual annotation isn't + implemented in the current python version. + """ + return f + + +logger = logging.getLogger(__name__) + + +@dataclass +class rich_char(ParseDirective): + """A single character character in a string with full escaping.""" + + c: str + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + snapshot = parser.snapshot() + try: + return parser.get(s(rf"\u{ord(self.c):04X}") | [r'\u{', count(hexdig, 2, 6), '}']) + except ParseError: + parser.restore(snapshot) + + match self.c: + case '\\': + return parser.get(r'\\') + case '\n': + return parser.get(s('\n') | r'\n') + case '\r': + return parser.get(s('\r') | r'\r') + case '\t': + return parser.get(s('\t') | r'\t') + case ' ': + return parser.get(s(' ') | r'\s') + case '"': + return parser.get(r'\"') + case "'": + return parser.get(s("'") | r"\'") + case '$': + return parser.get(s('$') | r'\$') + case _: + return parser.get(self.c) + + +@dataclass +class known_array(ParseDirective): + """Parse an array where all the values are known beforehand.""" + + delim: str + in_items: list[Puppet] + + def run(self, parser: 'ParserCombinator') -> list[MatchObject]: # noqa: D102 + """ + Read a delimted, comma separated, array. + + Reads the starting delimiter, a comma separated list of Puppet + items, an optional ending comma, and the ending delimiter. + + :param delim: + A string of length two, containing the starting and ending delimiter. + :param in_items: + """ + assert len(self.delim) == 2, "Delimiter should be the start and end character used." + items: list[MatchObject] = [] + items += parser.get(ws & self.delim[0]) + match self.in_items: + case []: + items += parser.get(ws & self.delim[1]) + case [x, *xs]: + items += parser.get(ws & x) + for item in xs: + parser.get(ws & ',' & ws & item) + items += parser.get(ws & optional(',') & ws & self.delim[1]) + return items + + +class ParserFormatter(Serializer[MatchObject]): + """ + Reserialize AST by highlighting the original source code. + + :param source: + The original source code. *Must* be the exact same source as + used to construct the corresponinding Puppet ast object. + :param seek: + Current parsing position in the string. + + TODO make this private. + """ + + parser: ParserCombinator + + def __init__(self, source: str, file: Optional[str] = None): + self.parser = ParserCombinator(source=source, file=file) + + def get_declaration_parameter(self, item: PuppetDeclarationParameter) -> list[MatchObject]: + """Parse a single declaration parameter.""" + return self.parser.get(ws & optional(item.type) & + ws & '$' & item.k & + optional(ws & '=' & ws & item.v)) + + def get_declaration_parameters( + self, + delim: str, + in_items: list[PuppetDeclarationParameter] | None) -> list[MatchObject]: + """See get_arr.""" + items: list[MatchObject] = [] + items += self.parser.get(ws & delim[0]) + match in_items: + case [] | None: + items += self.parser.get(ws & delim[1]) + case [x, *xs]: + items += self.parser.get_declaration_parameter(x) + for item in xs: + items += self.parser.get(ws & ',') + items += self.parser.get_declaration_parameter(item) + items += self.parser.get(ws & optional(',') & ws & delim[1]) + return items + + # -------------------------------------------------- + + @override + def _puppet_access(self, it: PuppetAccess) -> MatchObject: + return MatchObject('access', self.parser.get( + ws & it.how & ws & known_array('[]', it.args))) + + @override + def _puppet_array(self, it: PuppetArray) -> MatchObject: + return MatchObject('array', self.parser.get( + ws & known_array('[]', it.items))) + + @override + def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.lhs & ws & it.op & ws & it.rhs)) + + @override + def _puppet_block(self, it: PuppetBlock) -> MatchObject: + return MatchObject('', self.parser.get(ws & it.entries)) + + @override + def _puppet_call(self, it: PuppetCall) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.func & + ws & known_array('()', it.args))) + + @override + def _puppet_call_method(self, it: PuppetCallMethod) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.func & + optional(ws & known_array('()', it.args)) & + optional(ws & it.block))) + + @override + def _puppet_case(self, it: PuppetCase) -> MatchObject: + items: list[MatchObject] = [] + items += self.parser.get( + ws & tag('keyword', 'case') & ws & it.test & ws & '{') + + for ((x, *xs), body) in it.cases: + items += self.parser.get(ws & x) + for x in xs: + items += self.parser.get(ws & ',' & ws & x) + items += self.parser.get(ws & ':' & ws & '{' & ws & body & ws & '}') + items += self.parser.get(ws & '}') + return MatchObject('', items) + + @override + def _puppet_class(self, it: PuppetClass) -> MatchObject: + return MatchObject('', self.parser.get( + ws & tag('keyword', 'class') & ws & it.name & + optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) & + optional(ws & tag('inherits', it.parent)) & + ws & '{' & ws & it.body & ws & '}')) + + @override + def _puppet_collect(self, it: PuppetCollect) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.type & ws & it.query)) + + @override + def _puppet_concat(self, it: PuppetConcat) -> MatchObject: + out = [] + out += self.parser.get(ws & '"') + for fragment in it.fragments: + match fragment: + case PuppetVar(x): + f = ws & '$' & optional('{') & ws & optional('$') & x & ws & optional('}') + out += self.parser.get(f) + case PuppetString(st): + try: + out += self.parser.get(st) + except ParseError: + for c in st: + out += self.parser.get(rich_char(c)) + case _: + # TODO "${x[10][20]}" + out += self.parser.get(ws & "${" & ws & fragment & ws & "}") + out += self.parser.get(s('"') & ws) + return MatchObject('string', out) + + @override + def _puppet_declaration(self, it: PuppetDeclaration) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.k & ws & '=' & ws & it.v)) + + @override + def _puppet_define(self, it: PuppetDefine) -> MatchObject: + return MatchObject('', self.parser.get( + ws & tag('keyword', 'define') & ws & it.name & + optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) & + ws & '{' & ws & it.body & ws & '}')) + + @override + def _puppet_exported_query(self, it: PuppetExportedQuery) -> MatchObject: + return MatchObject('', self.parser.get( + ws & '<<|' & ws & it.filter & ws & '|>>')) + + @override + def _puppet_function(self, it: PuppetFunction) -> MatchObject: + return MatchObject('', self.parser.get( + ws & tag('keyword', 'function') & ws & it.name & + optional(ws & (lambda: self.get_declaration_parameters('()', it.params))) & + optional(ws & '>>' & it.returns) & + ws & '{' & ws & it.body & ws & '}')) + + @override + def _puppet_hash(self, it: PuppetHash) -> MatchObject: + out = [] + out += self.parser.get(ws & '{') + for entry in it.entries: + out += self.parser.get( + ws & entry.k & + ws & '=>' & + ws & entry.v & + optional(ws & ',')) + out += self.parser.get(ws & '}') + return MatchObject('', out) + + @override + def _puppet_if_chain(self, it: PuppetIfChain) -> MatchObject: + logger.debug(it) + logger.debug("remaining = %a…", self.parser.peek_string(100)) + (test1, body1), *rest = it.clauses + out = [] + out += self.parser.get(ws & 'if' & ws & test1 & ws & '{' & ws & body1 & ws & '}') + while True: + logger.debug("seek = %s, rem = %a…, len = %s", + self.parser.seek, self.parser.peek_string(100), len(rest)) + match rest: + case []: + break + case [('else', body), *xs]: + out += self.parser.get( + ws & tag('keyword', 'else') & ws & '{' & ws & body & ws & '}') + rest = xs + case [(test, body), *xs]: + out += self.parser.get(ws) + try: + out += self.parser.get( + ws & tag('keyword', 'elsif') & + ws & test & ws & '{' & ws & body & '}') + rest = xs + except ParseError: + out += self.parser.get( + ws & tag('keyword', 'else') & + ws & '{' & ws & PuppetIfChain(rest) & ws & '}') + rest = [] + return MatchObject('', out) + + @override + def _puppet_instanciation_parameter(self, it: PuppetInstanciationParameter) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.k & ws & it.arrow & ws & it.v & optional(ws & ' &'))) + + @override + def _puppet_invoke(self, it: PuppetInvoke) -> MatchObject: + out = self.parser.get(ws & it.func & optional(ws & '(')) + match it.args: + case [x, *xs]: + out += self.parser.get(ws & x) + for x in xs: + out += self.parser.get(ws & ',' & ws & x) + out += self.parser.get(optional(ws & ')')) + return MatchObject('', out) + + @override + def _puppet_keyword(self, it: PuppetKeyword) -> MatchObject: + return MatchObject('keyword', self.parser.get(ws & it.name)) + + @override + def _puppet_lambda(self, it: PuppetLambda) -> MatchObject: + return MatchObject('lambda', self.parser.get( + s(lambda: self.get_declaration_parameters('||', it.params)) & + '{' & it.body & '}')) + + @override + def _puppet_literal(self, it: PuppetLiteral) -> MatchObject: + return MatchObject('literal', self.parser.get(ws & it.literal)) + + @override + def _puppet_heredoc(self, it: PuppetHeredoc) -> MatchObject: + # TODO + return MatchObject('', []) + + @override + def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> MatchObject: + out: list[MatchObject] = [] + out += self.parser.get(ws & '@(' & ws) + escape_switches = s('/') & many(s('n') | 'r' | 't' | 's' | '$' | 'u' | 'L') + try: + # Delimiter + out += self.parser.get('"') + delim_parts = self.parser.get(many(complement('"'))) + out += delim_parts + out += self.parser.get('"') + # Syntax note + if it.syntax: + out += self.parser.get(ws & ':' & ws & it.syntax) + # escape switches + out += self.parser.get(optional(ws & escape_switches)) + # end delimiter + out += self.parser.get(ws & ')') + + except ParseError: + # Delimiter + delim_parts = self.parser.get(many(complement("):/"))) + out += delim_parts + if it.syntax: + out += self.parser.get(ws & ':' & ws & it.syntax) + # escape switches + out += self.parser.get(optional(ws & escape_switches)) + # end delimiter + out += self.parser.get(ws & ')') + + delim = stringify_match(delim_parts) + + for line in it.content.split('\n'): + out += self.parser.get(ws & line.lstrip() & '\n') + out += self.parser.get(ws & '|' & optional('-') & ws & delim) + + # get_until("|-? *{delim}") + return MatchObject('', out) + + @override + def _puppet_node(self, it: PuppetNode) -> MatchObject: + return MatchObject('', self.parser.get( + ws & 'node' & + # TODO non-wrapped list with optional trailing comma + ws & "{" & ws & it.body & "}")) + + @override + def _puppet_nop(self, it: PuppetNop) -> MatchObject: + # Should match nothing + return MatchObject('', []) + + @override + def _puppet_number(self, it: PuppetNumber) -> MatchObject: + out: list[MatchObject] = self.parser.get(ws) + match (it.x, it.radix): + case int(x), 8: + out += self.parser.get(s('0') & oct(x)[2:]) + case int(x), 16: + out += self.parser.get(s('0') & 'x' & hex(x)[2:]) + case x, None: + out += self.parser.get(str(it.x)) + case _: + raise ValueError(f"Unexpected radix: {it.radix}") + + return MatchObject('', out) + + @override + def _puppet_parenthesis(self, it: PuppetParenthesis) -> MatchObject: + return MatchObject('', self.parser.get(ws & '(' & ws & it.form & ws & ')')) + + @override + def _puppet_qn(self, it: PuppetQn) -> MatchObject: + return MatchObject('qn', self.parser.get(ws & it.name)) + + @override + def _puppet_qr(self, it: PuppetQr) -> MatchObject: + return MatchObject('qr', self.parser.get(ws & it.name)) + + @override + def _puppet_regex(self, it: PuppetRegex) -> MatchObject: + return MatchObject('rx', self.parser.get(ws & '/' & it.s.replace('/', r'\/') & '/')) + + @override + def _puppet_resource(self, it: PuppetResource) -> MatchObject: + out = self.parser.get(ws & it.type & ws & '{') + for key, body in it.bodies: + out += self.parser.get(ws & key & ws & ':' & ws & body & ws & optional(';')) + out = self.parser.get(ws & '}') + return MatchObject('', out) + + @override + def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> MatchObject: + return MatchObject('', self.parser.get(ws & it.type & ws & '{' & ws & it.ops & ws & '}')) + + @override + def _puppet_resource_override(self, it: PuppetResourceOverride) -> MatchObject: + return MatchObject('', self.parser.get( + ws & it.resource & ws & '{' & ws & it.ops & ws & '}')) + + @override + def _puppet_selector(self, it: PuppetSelector) -> MatchObject: + out = self.parser.get(ws & it.resource & ws & '?' & ws & '{') + for key, body in it.cases: + out += self.parser.get(ws & key & ws & '=>' & ws & body & ws & optional(',')) + out += self.parser.get('}') + return MatchObject('', out) + + @override + def _puppet_string(self, it: PuppetString) -> MatchObject: + # get one char to find delimiter + # Then read chars until matching delimiter (or parse expected + # string) + out: list[MatchObject] = [] + try: + out += self.parser.get(ws & it.s) + except ParseError: + out += self.parser.get(ws) + match self.parser.get(s('"') | "'"): + case [MatchObject(matched="'") as match]: + # Single quoted string + out.append(match) + for c in it.s: + match c: + case "'": + out += self.parser.get(r"\'") + case '\\': + out += self.parser.get(s(r'\\') | '\\') + case _: + out += self.parser.get(c) + # print([str(x) for x in out]) + out += self.parser.get("'") + + case [MatchObject(matched='"') as match]: + # Double quoted string + out.append(match) + for c in it.s: + out += self.parser.get(rich_char(c)) + + out += self.parser.get('"') + case err: + logger.error("Unknown match object: %s", err) + + return MatchObject('string', out) + + @override + def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> MatchObject: + return MatchObject('', self.parser.get(ws & it.op & ws & it.x)) + + @override + def _puppet_unless(self, it: PuppetUnless) -> MatchObject: + return MatchObject('', self.parser.get( + ws & 'unless' & ws & it.condition & ws & '{' & + ws & it.consequent & ws & '}')) + + @override + def _puppet_var(self, it: PuppetVar) -> MatchObject: + return MatchObject('', self.parser.get(ws & '$' & it.name)) + + @override + def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> MatchObject: + return MatchObject('', self.parser.get(ws & '<|' & ws & it.q & ws & '|>')) + + @override + def _puppet_parse_error(self, it: PuppetParseError) -> MatchObject: + logger.fatal(it) + raise Exception(it) + # return MatchObject('', self.parser.get()) diff --git a/muppet/puppet/format/text.py b/muppet/puppet/format/text.py index a3e772dfcba5c2a3073e79c7becf6b85626e6024..9d89042ea44ee0e207e57d573028a26beda84687 100644 --- a/muppet/puppet/format/text.py +++ b/muppet/puppet/format/text.py @@ -13,7 +13,7 @@ from muppet.puppet.ast import ( PuppetUnaryOperator, PuppetArray, PuppetCallMethod, PuppetCase, PuppetDeclarationParameter, PuppetInstanciationParameter, PuppetClass, PuppetConcat, - PuppetCollect, PuppetIf, PuppetUnless, PuppetKeyword, + PuppetCollect, PuppetIfChain, PuppetUnless, PuppetKeyword, PuppetExportedQuery, PuppetVirtualQuery, PuppetFunction, PuppetHash, PuppetHeredoc, PuppetLiteralHeredoc, PuppetVar, PuppetLambda, PuppetQn, PuppetQr, PuppetRegex, @@ -90,11 +90,6 @@ def find_heredoc_delimiter( return delim -def ind(level: int) -> str: - """Return indentation string of given depth.""" - return ' ' * level * 2 - - def string_width(s: str, indent: int) -> int: """ Return the width of a rendered puppet expression. @@ -129,25 +124,39 @@ def string_width(s: str, indent: int) -> int: class TextFormatter(Serializer[str]): """AST formatter returning source code.""" - @classmethod + def __init__(self, indent: int = 0): + self.__indent: int = indent + + def indent(self, change: int) -> 'TextFormatter': + """Return the current context, with an updated indentation level.""" + return self.__class__(indent=self.__indent + change) + + def ind(self, change: int = 0) -> str: + """ + Return indentation for current context. + + :param change: + Extra indentation level to add to this output. + """ + return (self.__indent + change) * 2 * ' ' + def format_declaration_parameter( - cls, + self, param: PuppetDeclarationParameter, - indent: int) -> str: + ) -> str: """Format a single declaration parameter.""" out: str = '' if param.type: - out += f'{cls.serialize(param.type, indent + 1)} ' + out += f'{self.indent(1).serialize(param.type)} ' out += f'${param.k}' if param.v: - out += f' = {cls.serialize(param.v, indent + 1)}' + out += f' = {self.indent(1).serialize(param.v)}' return out - @classmethod def format_declaration_parameters( - cls, + self, lst: list[PuppetDeclarationParameter], - indent: int) -> str: + ) -> str: """ Print declaration parameters. @@ -158,118 +167,107 @@ class TextFormatter(Serializer[str]): out = ' (\n' for param in lst: - out += ind(indent + 1) + cls.format_declaration_parameter(param, indent + 1) + ',\n' - out += ind(indent) + ')' + out += self.ind(1) + self.indent(1).format_declaration_parameter(param) + ',\n' + out += self.ind() + ')' return out - @classmethod def serialize_hash_entry( - cls, + self, entry: HashEntry, - indent: int) -> str: + ) -> str: """Return a hash entry as a string.""" - return f'{cls.serialize(entry.k, indent + 1)} => {cls.serialize(entry.v, indent + 2)}' + return f'{self.indent(1).serialize(entry.k)} => {self.indent(2).serialize(entry.v)}' @override - @classmethod - def _puppet_literal(cls, it: PuppetLiteral, indent: int) -> str: + def _puppet_literal(self, it: PuppetLiteral) -> str: return it.literal @override - @classmethod - def _puppet_access(cls, it: PuppetAccess, indent: int) -> str: - args = ', '.join(cls.serialize(x, indent) for x in it.args) + def _puppet_access(self, it: PuppetAccess) -> str: + args = ', '.join(self.serialize(x) for x in it.args) - return f'{cls.serialize(it.how, indent)}[{args}]' + return f'{self.serialize(it.how)}[{args}]' @override - @classmethod - def _puppet_binary_operator(cls, it: PuppetBinaryOperator, indent: int) -> str: - return f'{cls.serialize(it.lhs, indent)} {it.op} {cls.serialize(it.rhs, indent)}' + def _puppet_binary_operator(self, it: PuppetBinaryOperator) -> str: + return f'{self.serialize(it.lhs)} {it.op} {self.serialize(it.rhs)}' @override - @classmethod - def _puppet_unary_operator(cls, it: PuppetUnaryOperator, indent: int) -> str: - return f'{it.op} {cls.serialize(it.x, indent)}' + def _puppet_unary_operator(self, it: PuppetUnaryOperator) -> str: + return f'{it.op} {self.serialize(it.x)}' @override - @classmethod - def _puppet_array(cls, it: PuppetArray, indent: int) -> str: + def _puppet_array(self, it: PuppetArray) -> str: if not it.items: return '[]' else: out = '[\n' for item in it.items: - out += ind(indent + 1) + cls.serialize(item, indent + 2) + ',\n' - out += ind(indent) + ']' + out += self.ind(1) + self.indent(2).serialize(item) + ',\n' + out += self.ind() + ']' return out @override - @classmethod - def _puppet_call(cls, it: PuppetCall, indent: int) -> str: - args = ', '.join(cls.serialize(x, indent) for x in it.args) - return f'{cls.serialize(it.func, indent)}({args})' + def _puppet_call(self, it: PuppetCall) -> str: + args = ', '.join(self.serialize(x) for x in it.args) + return f'{self.serialize(it.func)}({args})' @override - @classmethod - def _puppet_call_method(cls, it: PuppetCallMethod, indent: int) -> str: - out: str = cls.serialize(it.func, indent) + def _puppet_call_method(self, it: PuppetCallMethod) -> str: + out: str = self.serialize(it.func) if it.args: - args = ', '.join(cls.serialize(x, indent) for x in it.args) + args = ', '.join(self.serialize(x) for x in it.args) out += f' ({args})' if it.block: - out += cls.serialize(it.block, indent) + out += self.serialize(it.block) return out @override - @classmethod - def _puppet_case(cls, it: PuppetCase, indent: int) -> str: - out: str = f'case {cls.serialize(it.test, indent)} {{\n' + def _puppet_case(self, it: PuppetCase) -> str: + out: str = f'case {self.serialize(it.test)} {{\n' for (when, body) in it.cases: - out += ind(indent + 1) - out += ', '.join(cls.serialize(x, indent + 1) for x in when) + out += self.ind(1) + out += ', '.join(self.indent(1).serialize(x) for x in when) out += ': {\n' for item in body: - out += ind(indent + 2) + cls.serialize(item, indent + 2) + '\n' - out += ind(indent + 1) + '}\n' - out += ind(indent) + '}' + out += self.ind(2) + self.indent(2).serialize(item) + '\n' + out += self.ind(1) + '}\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_declaration_parameter(cls, it: PuppetDeclarationParameter, indent: int) -> str: + def _puppet_declaration_parameter( + self, it: PuppetDeclarationParameter) -> str: out: str = '' if it.type: - out += f'{cls.serialize(it.type, indent + 1)} ' + out += f'{self.indent(1).serialize(it.type)} ' out += f'${it.k}' if it.v: - out += f' = {cls.serialize(it.v, indent + 1)}' + out += f' = {self.indent(1).serialize(it.v)}' return out @override - @classmethod - def _puppet_instanciation_parameter(cls, it: PuppetInstanciationParameter, indent: int) -> str: - return f'{it.k} {it.arrow} {cls.serialize(it.v, indent)}' + def _puppet_instanciation_parameter( + self, it: PuppetInstanciationParameter) -> str: + return f'{it.k} {it.arrow} {self.serialize(it.v)}' @override - @classmethod - def _puppet_class(cls, it: PuppetClass, indent: int) -> str: + def _puppet_class(self, it: PuppetClass) -> str: out: str = f'class {it.name}' if it.params: - out += cls.format_declaration_parameters(it.params, indent) + out += self.format_declaration_parameters(it.params) out += ' {\n' for form in it.body: - out += ind(indent+1) + cls.serialize(form, indent+1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_concat(cls, it: PuppetConcat, indent: int) -> str: + def _puppet_concat(self, it: PuppetConcat) -> str: out = '"' for item in it.fragments: match item: @@ -278,96 +276,93 @@ class TextFormatter(Serializer[str]): case PuppetVar(x): out += f"${{{x}}}" case puppet: - out += f"${{{cls.serialize(puppet, indent)}}}" + out += f"${{{self.serialize(puppet)}}}" out += '"' return out @override - @classmethod - def _puppet_collect(cls, it: PuppetCollect, indent: int) -> str: - return f'{cls.serialize(it.type, indent)} {cls.serialize(it.query, indent + 1)}' - - @override - @classmethod - def _puppet_if(cls, it: PuppetIf, indent: int) -> str: - out: str = f'if {cls.serialize(it.condition, indent)} {{\n' - for item in it.consequent: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' - if alts := it.alternative: - # TODO elsif - out += ' else {\n' - for item in alts: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' + def _puppet_collect(self, it: PuppetCollect) -> str: + return f'{self.indent(1).serialize(it.type)} {self.serialize(it.query)}' + + @override + def _puppet_if_chain(self, it: PuppetIfChain) -> str: + (test1, body1), *rest = it.clauses + assert test1 != 'else' + out: str = f'if {self.serialize(test1)} {{\n' + for item in body1: + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' + for (testn, bodyn) in rest: + out += ' ' + if testn == 'else': + out += 'else' + else: + out += f'elsif {self.serialize(testn)}' + out += ' {' + for item in bodyn: + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_unless(cls, it: PuppetUnless, indent: int) -> str: - out: str = f'unless {cls.serialize(it.condition, indent)} {{\n' + def _puppet_unless(self, it: PuppetUnless) -> str: + out: str = f'unless {self.serialize(it.condition)} {{\n' for item in it.consequent: - out += ind(indent+1) + cls.serialize(item, indent+1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_keyword(cls, it: PuppetKeyword, indent: int) -> str: + def _puppet_keyword(self, it: PuppetKeyword) -> str: return it.name @override - @classmethod - def _puppet_exported_query(cls, it: PuppetExportedQuery, indent: int) -> str: + def _puppet_exported_query(self, it: PuppetExportedQuery) -> str: out: str = '<<|' if f := it.filter: - out += ' ' + cls.serialize(f, indent) + out += ' ' + self.serialize(f) out += ' |>>' return out @override - @classmethod - def _puppet_virtual_query(cls, it: PuppetVirtualQuery, indent: int) -> str: + def _puppet_virtual_query(self, it: PuppetVirtualQuery) -> str: out: str = '<|' if f := it.q: - out += ' ' + cls.serialize(f, indent) + out += ' ' + self.serialize(f) out += ' |>' return out @override - @classmethod - def _puppet_function(cls, it: PuppetFunction, indent: int) -> str: + def _puppet_function(self, it: PuppetFunction) -> str: out: str = f'function {it.name}' if it.params: - out += cls.format_declaration_parameters(it.params, indent) + out += self.format_declaration_parameters(it.params) if ret := it.returns: - out += f' >> {cls.serialize(ret, indent + 1)}' + out += f' >> {self.indent(1).serialize(ret)}' out += ' {\n' for item in it.body: - out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_hash(cls, it: PuppetHash, indent: int) -> str: + def _puppet_hash(self, it: PuppetHash) -> str: if not it.entries: return '{}' else: out: str = '{\n' for item in it.entries: - out += ind(indent + 1) - out += cls.serialize_hash_entry(item, indent + 1) + out += self.ind(1) + out += self.indent(1).serialize_hash_entry(item) out += ',\n' - out += ind(indent) + '}' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_heredoc(cls, it: PuppetHeredoc, indent: int) -> str: + def _puppet_heredoc(self, it: PuppetHeredoc) -> str: """ Serialize heredoc with interpolation. @@ -392,7 +387,7 @@ class TextFormatter(Serializer[str]): case PuppetVar(x): body += f'${{{x}}}' case p: - body += cls.serialize(p, indent + 2) + body += self.indent(2).serialize(p) # Check if string ends with a newline match it.fragments[-1]: @@ -407,8 +402,7 @@ class TextFormatter(Serializer[str]): return f'@("EOF"{syntax}/$rt)\n{body}\n|{eol_marker} EOF' @override - @classmethod - def _puppet_literal_heredoc(cls, it: PuppetLiteralHeredoc, indent: int) -> str: + def _puppet_literal_heredoc(self, it: PuppetLiteralHeredoc) -> str: syntax: str = '' if it.syntax: syntax = f':{it.syntax}' @@ -416,7 +410,7 @@ class TextFormatter(Serializer[str]): out: str = '' if not it.content: out += f'@(EOF{syntax})\n' - out += ind(indent) + '|- EOF' + out += self.ind() + '|- EOF' return out delimiter = find_heredoc_delimiter(it.content) @@ -430,9 +424,9 @@ class TextFormatter(Serializer[str]): eol = True for line in lines: - out += ind(indent + 1) + line + '\n' + out += self.ind(1) + line + '\n' - out += ind(indent + 1) + '|' + out += self.ind(1) + '|' if not eol: out += '-' @@ -442,153 +436,135 @@ class TextFormatter(Serializer[str]): return out @override - @classmethod - def _puppet_var(cls, it: PuppetVar, indent: int) -> str: + def _puppet_var(self, it: PuppetVar) -> str: return f'${it.name}' @override - @classmethod - def _puppet_lambda(cls, it: PuppetLambda, indent: int) -> str: + def _puppet_lambda(self, it: PuppetLambda) -> str: out: str = '|' for item in it.params: out += 'TODO' out += '| {' for form in it.body: - out += ind(indent + 1) + cls.serialize(form, indent + 1) - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + out += self.ind() + '}' return out @override - @classmethod - def _puppet_qn(cls, it: PuppetQn, indent: int) -> str: + def _puppet_qn(self, it: PuppetQn) -> str: return it.name @override - @classmethod - def _puppet_qr(cls, it: PuppetQr, indent: int) -> str: + def _puppet_qr(self, it: PuppetQr) -> str: return it.name @override - @classmethod - def _puppet_regex(cls, it: PuppetRegex, indent: int) -> str: + def _puppet_regex(self, it: PuppetRegex) -> str: return f'/{it.s}/' @override - @classmethod - def _puppet_resource(cls, it: PuppetResource, indent: int) -> str: - out = f'{cls.serialize(it.type, indent + 1)} {{' + def _puppet_resource(self, it: PuppetResource) -> str: + out = f'{self.indent(1).serialize(it.type)} {{' match it.bodies: case [(name, values)]: - out += f' {cls.serialize(name, indent + 1)}:\n' + out += f' {self.indent(1).serialize(name)}:\n' for v in values: - out += ind(indent + 1) + cls.serialize(v, indent + 2) + ',\n' + out += self.ind(1) + self.indent(2).serialize(v) + ',\n' case bodies: out += '\n' for (name, values) in bodies: - out += f'{ind(indent + 1)}{cls.serialize(name, indent + 1)}:\n' + out += f'{self.ind(1)}{self.indent(1).serialize(name)}:\n' for v in values: - out += ind(indent + 2) + cls.serialize(v, indent + 3) + ',\n' - out += ind(indent + 2) + ';\n' - out += ind(indent) + '}' + out += self.ind(2) + self.indent(3).serialize(v) + ',\n' + out += self.ind(2) + ';\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_define(cls, it: PuppetDefine, indent: int) -> str: + def _puppet_define(self, it: PuppetDefine) -> str: out: str = f'define {it.name}' if params := it.params: - out += cls.format_declaration_parameters(params, indent) + out += self.format_declaration_parameters(params) out += ' {\n' for form in it.body: - out += ind(indent + 1) + cls.serialize(form, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(form) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_string(cls, it: PuppetString, indent: int) -> str: + def _puppet_string(self, it: PuppetString) -> str: # TODO escaping return f"'{it.s}'" @override - @classmethod - def _puppet_number(cls, it: PuppetNumber, indent: int) -> str: + def _puppet_number(self, it: PuppetNumber) -> str: return str(it.x) @override - @classmethod - def _puppet_invoke(cls, it: PuppetInvoke, indent: int) -> str: - invoker = f'{cls.serialize(it.func, indent)}' + def _puppet_invoke(self, it: PuppetInvoke) -> str: + invoker = f'{self.serialize(it.func)}' out: str = invoker template: str if invoker == 'include': template = ' {}' else: template = '({})' - out += template.format(', '.join(cls.serialize(x, indent + 1) for x in it.args)) + out += template.format(', '.join(self.indent(1).serialize(x) for x in it.args)) return out @override - @classmethod - def _puppet_resource_defaults(cls, it: PuppetResourceDefaults, indent: int) -> str: - out: str = f'{cls.serialize(it.type, indent)} {{\n' + def _puppet_resource_defaults(self, it: PuppetResourceDefaults) -> str: + out: str = f'{self.serialize(it.type)} {{\n' for op in it.ops: - out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(op) + ',\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_resource_override(cls, it: PuppetResourceOverride, indent: int) -> str: - out: str = f'{cls.serialize(it.resource, indent)} {{\n' + def _puppet_resource_override(self, it: PuppetResourceOverride) -> str: + out: str = f'{self.serialize(it.resource)} {{\n' for op in it.ops: - out += ind(indent + 1) + cls.serialize(op, indent + 1) + ',\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(op) + ',\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_declaration(cls, it: PuppetDeclaration, indent: int) -> str: - return f'{cls.serialize(it.k, indent)} = {cls.serialize(it.v, indent)}' + def _puppet_declaration(self, it: PuppetDeclaration) -> str: + return f'{self.serialize(it.k)} = {self.serialize(it.v)}' @override - @classmethod - def _puppet_selector(cls, it: PuppetSelector, indent: int) -> str: - out: str = f'{cls.serialize(it.resource, indent)} ? {{\n' - rendered_cases = [(cls.serialize(test, indent + 1), - cls.serialize(body, indent + 2)) + def _puppet_selector(self, it: PuppetSelector) -> str: + out: str = f'{self.serialize(it.resource)} ? {{\n' + rendered_cases = [(self.indent(1).serialize(test), + self.indent(2).serialize(body)) for (test, body) in it.cases] - case_width = max(string_width(c[0], indent + 1) for c in rendered_cases) + case_width = max(string_width(c[0], self.__indent + 1) for c in rendered_cases) for (test, body) in rendered_cases: - out += ind(indent + 1) + test - out += ' ' * (case_width - string_width(test, indent + 1)) + out += self.ind(1) + test + out += ' ' * (case_width - string_width(test, self.__indent + 1)) out += f' => {body},\n' - out += ind(indent) + '}' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_block(cls, it: PuppetBlock, indent: int) -> str: - return '\n'.join(cls.serialize(x, indent) for x in it.entries) + def _puppet_block(self, it: PuppetBlock) -> str: + return '\n'.join(self.serialize(x) for x in it.entries) @override - @classmethod - def _puppet_node(cls, it: PuppetNode, indent: int) -> str: + def _puppet_node(self, it: PuppetNode) -> str: out: str = 'node ' - out += ', '.join(cls.serialize(x, indent) for x in it.matches) + out += ', '.join(self.serialize(x) for x in it.matches) out += ' {\n' for item in it.body: - out += ind(indent + 1) + cls.serialize(item, indent + 1) + '\n' - out += ind(indent) + '}' + out += self.ind(1) + self.indent(1).serialize(item) + '\n' + out += self.ind() + '}' return out @override - @classmethod - def _puppet_parenthesis(cls, it: PuppetParenthesis, indent: int) -> str: - return f'({cls.serialize(it.form, indent)})' + def _puppet_parenthesis(self, it: PuppetParenthesis) -> str: + return f'({self.serialize(it.form)})' @override - @classmethod - def _puppet_nop(cls, it: PuppetNop, indent: int) -> str: + def _puppet_nop(self, it: PuppetNop) -> str: return '' diff --git a/muppet/puppet/strings/__init__.py b/muppet/puppet/strings/__init__.py index fabaf14c048874d76b4c28040fb0f05f1c1cdba0..42d558d3d43fabda37404a23ca3db61c65639f81 100644 --- a/muppet/puppet/strings/__init__.py +++ b/muppet/puppet/strings/__init__.py @@ -35,6 +35,7 @@ from dataclasses import dataclass, field import logging from .internal import Deserializable import re +# from muppet.util import ILoggerAdapter logger = logging.getLogger(__name__) @@ -377,6 +378,9 @@ def puppet_strings(path: str) -> bytes: import tempfile tmpfile = tempfile.NamedTemporaryFile() + logger = logging.getLogger('muppet.puppet-strings') + # logger = ILoggerAdapter(logging.getLogger('muppet.puppet-strings'), + # extra={'file': path}) cmd = subprocess.Popen( ['puppet', 'strings', 'generate', @@ -397,6 +401,7 @@ def puppet_strings(path: str) -> bytes: # enum found here: # https://github.com/puppetlabs/puppet-strings/blob/afe75151f8b47ce33433c488e22ca508aa48ac7c/spec/unit/puppet-strings/yard/handlers/ruby/rsapi_handler_spec.rb#L105 # Expected formatting from observing the output. + # TODO document this special logger if m := re.match(r'^\[(\w+)]: (.*)', line): match m[1]: case "debug": logger.debug(m[2]) diff --git a/muppet/util.py b/muppet/util.py index c12d15d48ad5d5696f78a481b3ef153384be5b1d..874ce8de0f8ba1b4ed5a92c0076d65458c906d9f 100644 --- a/muppet/util.py +++ b/muppet/util.py @@ -3,12 +3,16 @@ from typing import ( TypeVar, Callable, + Iterable, ) from collections.abc import ( Sequence, ) +# from logging import LoggerAdapter +# import copy + T = TypeVar('T') U = TypeVar('U') @@ -26,3 +30,29 @@ def group_by(proc: Callable[[T], U], seq: Sequence[T]) -> dict[U, list[T]]: key = proc(item) d[key] = (d.get(key) or []) + [item] return d + + +def concatenate(lstlst: Iterable[Iterable[T]]) -> list[T]: + """Concatenate a list of lists into a flat(er) list.""" + out: list[T] = [] + for lst in lstlst: + out += lst + return out + + +# class ILoggerAdapter(LoggerAdapter): +# """Wrapper around a logger, allowing extra keys to added once.""" +# +# def __init__(self, logger, extra): +# super().__init__(logger, extra) +# self.env = extra +# +# def process(self, msg: str, kwargs): +# """Overridden method from upstream.""" +# msg, kwargs = super().process(msg, kwargs) +# result = copy.deepcopy(kwargs) +# default_kwargs_key = ['exc_info', 'stack_info', 'extra'] +# custom_key = [k for k in result.keys() if k not in default_kwargs_key] +# result['extra'].update({k: result.pop(k) for k in custom_key}) +# +# return msg, result