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
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):
"""
@ -27,9 +38,93 @@ def _lazy_evaluable(func):
# if the user wants to write her own builder.
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
def printer(username: str):
def printer(username: str) -> None:
"Display the user's name"
if username == 'Maud':
raise Exception("No Mauds allowed!")
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 functools
import importlib
import os
from pathlib import Path
@ -54,4 +55,5 @@ class ConfigurationManager:
f.write(f'{attr} = "{value}"\n')
return True
cm = ConfigurationManager()

3
git.py
View File

@ -4,5 +4,8 @@ from typing import Sequence
def exec(*args: str):
return subprocess.check_call(["git", *args])
def rc(*args: str):
return subprocess.call(["git", *args])
def read(*args: str):
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 typing import Optional, Sequence
from config import cm
import config
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.
"""
print("tzk: Writing new wiki folder to config file...")
if not cm.write_attr("wiki_folder", wiki_name):
if cm.wiki_folder == wiki_name:
if not config.cm.write_attr("wiki_folder", wiki_name):
if config.cm.wiki_folder == wiki_name:
print("tzk: (Looks like it was already there.)")
else:
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 "
"and update this option if necessary.")
return False

21
tzk.py
View File

@ -9,7 +9,7 @@ from typing import Optional
from config import cm
import git
import tw
from util import fail
from util import BuildError, fail
class CliCommand(ABC):
@ -179,18 +179,29 @@ class BuildCommand(CliCommand):
print(f"tzk: Found {len(steps)} build steps.")
for idx, step in enumerate(steps, 1):
if hasattr(step, 'name'):
print(f"\ntzk: Step {idx}/{len(steps)}: {step.name}")
if hasattr(step, '__doc__'):
print(f"\ntzk: Step {idx}/{len(steps)}: {step.__doc__}")
else:
print(f"\ntzk: Step {idx}/{len(steps)}")
try:
step()
except Exception as e:
print(f"\ntzk: Build of product '{args.product}' failed on step {failed}. "
except BuildError as e:
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:")
traceback.print_exc()
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.")

View File

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