Source code for pkglts.templating

"""
Set of functions to extend jinja2.
"""
import re

from .small_tools import ensure_path

OPENING_MARKER = "{" + "#"
CLOSING_MARKER = "#" + "}"

BLOCK_RE = re.compile(
    r"\{#[ ]pkglts,[ ](?P<key>[a-zA-Z0-9._]*)(?P<aft_head>.*?)\n(?P<cnt>.*?)#\}(?P<aft_foot>.*?(?=\n))",
    re.DOTALL | re.MULTILINE)

NON_BIN_EXT = ("", ".bat", ".cfg", ".json", ".in", ".ini", ".md", ".no", ".ps1", ".py", ".rst", ".sh",
               ".svg",  ".toml", ".tpl", ".txt", ".yml", ".yaml")


[docs] class TplBlock: """Simple container for template blocks.""" def __init__(self): self.before_header = "" self.bid = None self.loc = None self.after_header = "" self.content = "" self.before_footer = "" self.after_footer = "" def __str__(self): return (f"Block(\n" f"|{self.before_header}|{self.bid}|{self.after_header}|\n" f"|{repr(self.content)}|\n|{self.before_footer}|{self.after_footer}|\n)")
[docs] def pkglts_defined(self, bid, cnt): """Create a block regenerated by pkglts. Args: bid (str): id for this block (unique per file only) cnt (str): content Returns: None """ self.bid = bid self.content = cnt
[docs] def user_defined(self, cnt): """Make this block a user defined one. Args: cnt (str): content Returns: None """ self.bid = None self.content = cnt
[docs] def parse_source(txt): """Parse text to find preserved blocks Args: txt (str): full text to parse Returns: (list of [str, str, str, str]): ordered list of blocks: - block_id (or None if content is not preserved - line start before start for preserved content - content - line start before end for preserved content """ blocks = [] last_end = -1 for res in BLOCK_RE.finditer(txt): i = res.start() while i > last_end and txt[i] != "\n": i -= 1 if i > last_end: block = TplBlock() block.user_defined(txt[(last_end + 1): (i + 1)]) blocks.append(block) bef = txt[(i + 1): res.start()] bid = res.group('key') aft_head = res.group('aft_head') cnt = res.group('cnt') aft_foot = res.group('aft_foot') i = len(cnt) - 1 while i > 0 and cnt[i] != "\n": i -= 1 aft = cnt[(i + 1): len(cnt)] cnt = cnt[:(i + 1)] last_end = res.end() block = TplBlock() block.pkglts_defined(bid, cnt) block.before_header = bef block.after_header = aft_head block.before_footer = aft block.after_footer = aft_foot if aft_head.startswith(", after"): block.loc = ('after', aft_head[7:].split(" ")[1]) elif aft_head.startswith(", before"): block.loc = ('before', aft_head[8:].split(" ")[1]) elif aft_head.startswith(", replace"): block.loc = ('replace', aft_head[8:].split(" ")[1]) blocks.append(block) if last_end < (len(txt) - 1): block = TplBlock() block.user_defined(txt[(last_end + 1): len(txt)]) blocks.append(block) return blocks
[docs] class Template(object): """Simple container that holds a list of blocks The may objective is to represent a full templated file """ def __init__(self): self.blocks = [] self.bin_cnt = None # binary content for binary files
[docs] def add(self, block): """Insert a new block in the overall template Notes: will use block loc to position it. Args: block (TplBlock): block with all fields filled Returns: None """ if block.loc is None: self.blocks.append(block) else: ind = [b.bid for b in self.blocks].index(block.loc[1]) if block.loc[0] == 'after': self.blocks.insert(ind + 1, block) elif block.loc[0] == 'before': self.blocks.insert(ind, block) elif block.loc[0] == 'replace': self.blocks[ind] = block else: raise NotImplementedError
[docs] def parse(self, pth): """Parse file and append all blocks into this template Args: pth (Path): path to file to read Returns: None """ if pth.suffix in NON_BIN_EXT: for block in parse_source(pth.read_text()): self.add(block) else: self.bin_cnt = pth.read_bytes()
[docs] def render(self, cfg, tgt_pth): """Render template into tgt_pth Notes: keeps 'preserved' block structure Args: cfg (Config): current package configuration tgt_pth (Path): path to potentially non existent yet target file Returns: (list of [str, str]): key, cnt for preserved blocks """ if self.bin_cnt is not None: ensure_path(tgt_pth) tgt_pth.write_bytes(self.bin_cnt) else: return self._render_non_bin(cfg, tgt_pth)
def _render_non_bin(self, cfg, tgt_pth): blocks = [] if tgt_pth.exists(): # retrieves preserved blocks from source # parse tgt file to find 'preserved' blocks tgt_blocks = parse_source(tgt_pth.read_text()) tpl_bids = set(b.bid for b in self.blocks if b.bid is not None) tgt_bids = set(b.bid for b in tgt_blocks if b.bid is not None) if tpl_bids != tgt_bids: raise UserWarning(f"File '{tgt_pth}' not compatible with current template") src_blocks = dict((b.bid, b) for b in self.blocks if b.bid is not None) for tgt_block in tgt_blocks: # reset content of preserved blocks only if tgt_block.bid is not None: tgt_block.content = src_blocks[tgt_block.bid].content blocks.append(tgt_block) else: # format non preserved blocks for the first and only time for block in self.blocks: if block.bid is None: block.content = cfg.render(block.content) blocks.append(block) # regenerate preserved block content preserved = [] tgt = "" for block in blocks: if block.bid is None: tgt += block.content else: # format cnt cnt = cfg.render(block.content) if len(cnt) == 0 or cnt[-1] != "\n": # case where templating has eaten the remaining spaces and '\n' cnt += "\n" preserved.append((block.bid, cnt)) # rewrite preserved tag tgt += block.before_header + "{" + f"# pkglts, {block.bid}{block.after_header}\n" tgt += cnt tgt += block.before_footer + "#" + "}" + f"{block.after_footer}\n" ensure_path(tgt_pth) tgt_pth.write_text(tgt) return preserved