diff --git a/tzk/__main__.py b/tzk/__main__.py index d763b36..fecb303 100644 --- a/tzk/__main__.py +++ b/tzk/__main__.py @@ -13,15 +13,26 @@ from tzk.util import BuildError, fail, numerize class CliCommand(ABC): + """ + Base class for subcommands of tzk. + """ + #: The text of the subcommand to be used on the command line. cmd = None # type: str + #: Help string for argparse to display for this subcommand. help = None # type: str @abstractclassmethod def setup_arguments(cls, parser: argparse.ArgumentParser) -> None: + """ + Given the :arg:`parser`, add any arguments this subcommand wants to accept. + """ raise NotImplementedError @abstractmethod - def execute(self, parser: argparse.Namespace) -> None: + def execute(self, args: argparse.Namespace) -> None: + """ + Given the :arg:`args` passed to this subcommand, do whatever the command does. + """ raise NotImplementedError @@ -182,21 +193,27 @@ class BuildCommand(CliCommand): def execute(self, args: argparse.Namespace) -> None: self._precheck(args.product) + # Find the build steps for the product the user specified. steps = cm().products[args.product] print(f"tzk: Starting build of product '{args.product}'.") print(f"tzk: Found {len(steps)} build {numerize(len(steps), 'step')}.") + # For each build step... for idx, step in enumerate(steps, 1): + # Explain what we're doing. Use first line of the builder's docstring + # as a summary, if present. if hasattr(step, '__doc__'): short_description = step.__doc__.strip().split('\n')[0].rstrip('.') print(f"tzk: Step {idx}/{len(steps)}: {short_description}") else: print(f"tzk: Step {idx}/{len(steps)}") + # If the user asked to skip this builder on the command line, do so. if step.__name__ in args.skip_builder: print(f"tzk: Skipping step {idx} due to --skip-builder parameter.") continue + # Execute step and handle any errors. try: step() except BuildError as e: @@ -231,10 +248,12 @@ def launch(): args = parser.parse_args() if not hasattr(args, '_cls'): + # no subcommand was given parser.print_help() sys.exit(0) # For all operations except 'init', we start in the wiki folder. + # (For init, we're actually *creating* the wiki folder.) if not args._cls.cmd == "init": if not cm().wiki_folder: fail("No 'wiki_folder' option found in config. Set this option to the name " @@ -256,4 +275,4 @@ def launch(): if __name__ == '__main__': - launch() \ No newline at end of file + launch() diff --git a/tzk/config.py b/tzk/config.py index 9b78a79..eccb5ae 100644 --- a/tzk/config.py +++ b/tzk/config.py @@ -1,3 +1,6 @@ +""" +config.py - read and manage the TZK config file +""" import datetime import functools import importlib @@ -57,6 +60,14 @@ class ConfigurationManager: def cm(cache=[]): + """ + Call this function to retrieve the singleton ConfigurationManager object, + reading and initializing it if necessary. + + Since so much happens when the ConfigurationManager is initialized, + this has to go in a function so that autodoc doesn't blow up + when it tries to import the module. + """ if not cache: cache.append(ConfigurationManager()) return cache[0] diff --git a/tzk/git.py b/tzk/git.py index 6cb4f73..d9d4566 100644 --- a/tzk/git.py +++ b/tzk/git.py @@ -1,11 +1,17 @@ import subprocess from typing import Sequence -def exec(*args: str): - return subprocess.check_call(["git", *args]) -def rc(*args: str): +def exec(*args: str) -> None: + "Execute a Git command, raising CalledProcessError if the exit code is nonzero." + subprocess.check_call(["git", *args]) + + +def rc(*args: str) -> int: + "Execute a Git command, returning the exit code." return subprocess.call(["git", *args]) -def read(*args: str): + +def read(*args: str) -> str: + "Execute a Git command, returning the output as a string." return subprocess.check_output(["git", *args], text=True).strip() diff --git a/tzk/tw.py b/tzk/tw.py index 0aa989d..0c8b333 100644 --- a/tzk/tw.py +++ b/tzk/tw.py @@ -20,6 +20,7 @@ def _tw_path() -> str: @functools.lru_cache(1) def _whoami() -> str: + "Try to guess the user's name." try: return subprocess.check_output(("whoami",), text=True).strip() except subprocess.CalledProcessError: @@ -27,6 +28,19 @@ def _whoami() -> str: def exec(args: Sequence[Sequence[str]], base_wiki_folder: str = None) -> int: + """ + Execute a series of TiddlyWiki commands. + + :param args: A list of lists of CLI commands to send to TiddlyWiki. + The first element of each list is a TiddlyWiki CLI command, + without the ``--``, e.g., ``savewikifolder``. + The following elements of the list are arguments to that command. + :param base_wiki_folder: If the wiki to execute commands against is not the one + in the current directory, provide its path here. + The current directory is the source wiki's root directory + during the execution of builders, + unless explicitly changed. + """ call_args = [_tw_path()] if base_wiki_folder is not None: call_args.append(base_wiki_folder) @@ -98,6 +112,10 @@ def _save_wikifolder_to_config(wiki_name: str) -> bool: def _add_filesystem_plugins(wiki_name: str) -> None: + """ + Add the "tiddlywiki/filesystem" and "tiddlywiki/tiddlyweb" plugins + required for Node.js client-server operation to the new wiki's tiddlywiki.info. + """ print("tzk: Adding filesystem plugins to tiddlywiki.info...") info_path = Path.cwd() / wiki_name / "tiddlywiki.info" with info_path.open("r") as f: @@ -108,6 +126,9 @@ def _add_filesystem_plugins(wiki_name: str) -> None: def _init_gitignore() -> None: + """ + Create a basic gitignore for the new wiki. + """ print("tzk: Creating gitignore...") GITIGNORE = dedent(""" __pycache__/ @@ -122,6 +143,9 @@ def _init_gitignore() -> None: def _initial_commit() -> None: + """ + Create a new Git repo and commit everything we've done so far. + """ print("tzk: Initializing new Git repository for wiki...") git.exec("init") @@ -131,6 +155,9 @@ def _initial_commit() -> None: def install(wiki_name: str, tw_version_spec: str, author: Optional[str]): + """ + Install TiddlyWiki on Node.js in the current directory and set up a new wiki. + """ # assert: caller has checked npm and git are installed warnings = False diff --git a/tzk/util.py b/tzk/util.py index 3962f92..8d170ad 100644 --- a/tzk/util.py +++ b/tzk/util.py @@ -1,3 +1,6 @@ +""" +util.py - miscellaneous utility functions +""" from contextlib import contextmanager import os import sys @@ -9,11 +12,13 @@ class BuildError(Exception): def fail(msg: str, exit_code: int = 1) -> NoReturn: + "Print message to stderr and quit with exit code 1." print(msg, file=sys.stderr) sys.exit(exit_code) -def numerize(number: int, singular: str, plural: str = None): +def numerize(number: int, singular: str, plural: str = None) -> str: + "Render a string in the singular or plural as appropriate." if plural is None: plural = singular + 's'