add kill-phrase builder

This commit is contained in:
Soren I. Bjornstad 2021-08-26 14:04:09 -05:00
parent f63f1a61ea
commit 279342bb4f
6 changed files with 127 additions and 12 deletions

View File

@ -1,4 +1,15 @@
from contextlib import contextmanager
import functools import functools
from pathlib import Path
import re
import shutil
import tempfile
from typing import Set
import git
import tw
from util import BuildError
def _lazy_evaluable(func): def _lazy_evaluable(func):
""" """
@ -27,9 +38,93 @@ def _lazy_evaluable(func):
# if the user wants to write her own builder. # if the user wants to write her own builder.
tzk_builder = _lazy_evaluable tzk_builder = _lazy_evaluable
def stop(message: str) -> None:
"Stop the build due to an error condition."
raise BuildError(message)
# Global state available to all builders.
build_state = {}
@tzk_builder @tzk_builder
def printer(username: str): def printer(username: str) -> None:
"Display the user's name"
if username == 'Maud': if username == 'Maud':
raise Exception("No Mauds allowed!") raise Exception("No Mauds allowed!")
print(f"Hallelujah, {username} built a wiki!") print(f"Hallelujah, {username} built a wiki!")
printer.name = "Display the user's name"
@tzk_builder
def require_branch(branchname: str) -> None:
"Require a specific Git branch to be checked out"
if git.read("branch", "--show-current") != branchname:
stop(f"You may only run this build from the {branchname} branch.")
@tzk_builder
def require_clean_working_tree() -> None:
"Require the working tree of the Git repository to be clean"
pleasecommit = "Please commit or stash them before publishing."
if git.rc("diff-index", "--quiet", "--cached", "HEAD", "--") != 0:
stop(f"There are staged changes. {pleasecommit}")
if git.rc("diff-files", "--quiet") != 0:
stop(f"There are uncommitted changes. {pleasecommit}")
@tzk_builder
def new_output_folder():
"Create a new temporary folder to hold output being built"
assert 'public_wiki_folder' not in build_state
build_state['public_wiki_folder'] = tempfile.mkdtemp()
new_output_folder.cleaner = lambda: shutil.rmtree(build_state['public_wiki_folder'])
@tzk_builder
def export_public_tiddlers(export_filter: str) -> None:
"Export public tiddlers to public wiki folder"
assert 'public_wiki_folder' in build_state
tw.exec((
("savewikifolder", build_state['public_wiki_folder'], export_filter),
))
def _find_kill_phrases(phrases: Set[str]):
regexes = [re.compile(phrase) for phrase in phrases]
failures = []
tid_files = (Path(build_state['public_wiki_folder']) / "tiddlers").glob("**/*.tid")
for tid_file in tid_files:
with tid_file.open() as f:
for line in f:
for regex in regexes:
if re.search(regex, line):
failures.append((regex, str(tid_file), line))
return failures
@tzk_builder
def check_for_kill_phrases(kill_phrase_file: str = None):
"Fail build if any of a series of regexes appears in a tiddler's source"
if kill_phrase_file is None:
kill_phrase_file = "tiddlers/_system/config/zettelkasten/Build/KillPhrases.tid"
kill_phrases = set()
with open(kill_phrase_file) as f:
reading = False
for line in f:
if not line.strip():
reading = True
if line.strip() and reading:
kill_phrases.add(line)
failures = _find_kill_phrases(kill_phrases)
if failures:
result = ["Kill phrases were found in your public wiki:"]
for failed_regex, failed_file, failed_line in failures:
trimmed_file = failed_file.replace(build_state['public_wiki_folder'], '')
result.append(f"'{failed_regex.pattern}' matched file {trimmed_file}:\n"
f" {failed_line.strip()}")
stop('\n'.join(result))

View File

@ -1,4 +1,5 @@
import datetime import datetime
import functools
import importlib import importlib
import os import os
from pathlib import Path from pathlib import Path
@ -54,4 +55,5 @@ class ConfigurationManager:
f.write(f'{attr} = "{value}"\n') f.write(f'{attr} = "{value}"\n')
return True return True
cm = ConfigurationManager() cm = ConfigurationManager()

3
git.py
View File

@ -4,5 +4,8 @@ from typing import Sequence
def exec(*args: str): def exec(*args: str):
return subprocess.check_call(["git", *args]) return subprocess.check_call(["git", *args])
def rc(*args: str):
return subprocess.call(["git", *args])
def read(*args: str): def read(*args: str):
return subprocess.check_output(["git", *args], text=True).strip() return subprocess.check_output(["git", *args], text=True).strip()

8
tw.py
View File

@ -7,7 +7,7 @@ import subprocess
from textwrap import dedent from textwrap import dedent
from typing import Optional, Sequence from typing import Optional, Sequence
from config import cm import config
import git import git
@ -96,12 +96,12 @@ def _save_wikifolder_to_config(wiki_name: str) -> bool:
Return True if the option ended set to wiki_name, False otherwise. Return True if the option ended set to wiki_name, False otherwise.
""" """
print("tzk: Writing new wiki folder to config file...") print("tzk: Writing new wiki folder to config file...")
if not cm.write_attr("wiki_folder", wiki_name): if not config.cm.write_attr("wiki_folder", wiki_name):
if cm.wiki_folder == wiki_name: if config.cm.wiki_folder == wiki_name:
print("tzk: (Looks like it was already there.)") print("tzk: (Looks like it was already there.)")
else: else:
print(f"tzk: WARNING: The wiki_folder option in your config appears " print(f"tzk: WARNING: The wiki_folder option in your config appears "
f"to be set to '{cm.wiki_folder}', rather than the wiki folder " f"to be set to '{config.cm.wiki_folder}', rather than the wiki folder "
f"you're initializing, {wiki_name}. Please check your config file " f"you're initializing, {wiki_name}. Please check your config file "
"and update this option if necessary.") "and update this option if necessary.")
return False return False

21
tzk.py
View File

@ -9,7 +9,7 @@ from typing import Optional
from config import cm from config import cm
import git import git
import tw import tw
from util import fail from util import BuildError, fail
class CliCommand(ABC): class CliCommand(ABC):
@ -179,18 +179,29 @@ class BuildCommand(CliCommand):
print(f"tzk: Found {len(steps)} build steps.") print(f"tzk: Found {len(steps)} build steps.")
for idx, step in enumerate(steps, 1): for idx, step in enumerate(steps, 1):
if hasattr(step, 'name'): if hasattr(step, '__doc__'):
print(f"\ntzk: Step {idx}/{len(steps)}: {step.name}") print(f"\ntzk: Step {idx}/{len(steps)}: {step.__doc__}")
else: else:
print(f"\ntzk: Step {idx}/{len(steps)}") print(f"\ntzk: Step {idx}/{len(steps)}")
try: try:
step() step()
except Exception as e: except BuildError as e:
print(f"\ntzk: Build of product '{args.product}' failed on step {failed}. " print(f"tzk: ERROR: {str(e)}")
print(f"\ntzk: Build of product '{args.product}' failed on step {idx}, "
f"backed by builder '{step.__name__}'.")
sys.exit(1)
except Exception:
print(f"\ntzk: Build of product '{args.product}' failed on step {idx}: "
f"unhandled exception. "
f"The original error follows:") f"The original error follows:")
traceback.print_exc() traceback.print_exc()
sys.exit(1) sys.exit(1)
for idx, step in enumerate(steps, 1):
if hasattr(step, 'cleaner'):
print(f"\ntzk: Running cleanup routine for step {idx}...")
step.cleaner()
print(f"\ntzk: Build of product '{args.product}' completed successfully.") print(f"\ntzk: Build of product '{args.product}' completed successfully.")

View File

@ -1,7 +1,11 @@
import sys import sys
from typing import NoReturn from typing import NoReturn
class BuildError(Exception):
pass
def fail(msg: str, exit_code: int = 1) -> NoReturn: def fail(msg: str, exit_code: int = 1) -> NoReturn:
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.exit(exit_code) sys.exit(exit_code)