add feature for automatic init of a TZK repository

This commit is contained in:
Soren I. Bjornstad 2021-08-26 09:46:24 -05:00
parent d23070435a
commit a48f527ec8
6 changed files with 285 additions and 36 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
__pycache__/ __pycache__/
.vscode/

View File

@ -1,27 +1,56 @@
import datetime
import importlib import importlib
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
from typing import Any
from util import fail
class ConfigurationManager: class ConfigurationManager:
def __init__(self): def __init__(self):
config_path = Path.cwd() self.config_path = Path.cwd()
for child in sorted(config_path.iterdir()): for child in sorted(self.config_path.iterdir()):
if child.is_file() and child.name.endswith('.py'): if child.is_file() and child.name.endswith('.py'):
mod_name = child.name.rsplit('.', 1)[0] mod_name = child.name.rsplit('.', 1)[0]
if mod_name == 'tzk_config': if mod_name == 'tzk_config':
sys.path.insert(0, str(config_path)) sys.path.insert(0, str(self.config_path))
self.conf_mod = importlib.import_module(mod_name) self.conf_mod = importlib.import_module(mod_name)
del sys.path[0] del sys.path[0]
break break
else: else:
print( fail(
f"Your TZK config file could not be found. " f"Your TZK config file could not be found. "
f"Please ensure there is a file called tzk_config.py " f"Please ensure there is a file called tzk_config.py "
f"in the current directory.", file=sys.stderr) f"in the current directory.")
sys.exit(1)
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.conf_mod, attr, None) return getattr(self.conf_mod, attr, None)
def write_attr(self, attr: str, value: str) -> bool:
"""
Try to add a simple attribute = string value config parameter to the
config file, if it doesn't already exist. More complicated data types
are not supported.
Return:
False if the attribute already has a value.
True if successful.
Raises:
File access errors if the config file is inaccessible.
"""
if hasattr(self.conf_mod, attr):
return False
else:
setattr(self.conf_mod, attr, value)
with open(self.config_path / "tzk_config.py", "a") as f:
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"\n# Added automatically by tzk at {now}\n")
f.write(f'{attr} = "{value}"\n')
return True
cm = ConfigurationManager()

8
git.py
View File

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

160
tw.py
View File

@ -1,12 +1,162 @@
from contextlib import contextmanager
import functools
import json
import os
from pathlib import Path
import subprocess import subprocess
from typing import Sequence from textwrap import dedent
from typing import Optional, Sequence
from config import cm
import git
def exec(args: Sequence[Sequence[str]]) -> None: @functools.lru_cache(1)
bin_dir = subprocess.check_output(("npm", "bin"), text=True).strip() def _npm_bin() -> str:
call_args = [bin_dir + "/tiddlywiki"] return subprocess.check_output(("npm", "bin"), text=True).strip()
@contextmanager
def _pushd(directory: str):
"""
Change directory into the directory /directory/ until the end of the with-block,
then return to previous directory.
"""
old_directory = os.getcwd()
try:
os.chdir(directory)
yield
finally:
os.chdir(old_directory)
def _tw_path() -> str:
return _npm_bin() + "/tiddlywiki"
@functools.lru_cache(1)
def _whoami() -> str:
try:
return subprocess.check_output(("whoami",), text=True).strip()
except subprocess.CalledProcessError:
return "user"
def exec(args: Sequence[Sequence[str]]) -> int:
call_args = [_tw_path()]
for tw_arg in args: for tw_arg in args:
call_args.append(f"--{tw_arg[0]}") call_args.append(f"--{tw_arg[0]}")
for inner_arg in tw_arg[1:]: for inner_arg in tw_arg[1:]:
call_args.append(inner_arg) call_args.append(inner_arg)
return subprocess.call(call_args) return subprocess.call(call_args)
def _init_npm(wiki_name: str, tw_version_spec: str, author: str) -> None:
"""
Create a package.json file for this repository, requiring TiddlyWiki
at the specified version, and install the npm dependencies.
"""
print("tzk: Creating package.json...")
PACKAGE_JSON = dedent("""
{
"name": "%(wiki_name)s",
"version": "1.0.0",
"description": "My nice notes",
"dependencies": {
"tiddlywiki": "%(tw_version_spec)s"
},
"author": "%(author)s",
"license": "See copyright notice in wiki"
}
""").strip() % ({'tw_version_spec': tw_version_spec, 'author': author,
'wiki_name': wiki_name})
with open("package.json", "w") as f:
f.write(PACKAGE_JSON)
print("tzk: Installing npm packages from package.json...")
subprocess.check_call(("npm", "install"))
def _init_tw(wiki_name: str) -> None:
"""
Create a new TiddlyWiki in the subfolder named 'wiki_name'
using 'tiddlywiki --init'.
"""
print("tzk: Creating new TiddlyWiki...")
try:
os.mkdir(wiki_name)
except FileExistsError:
pass
with _pushd(wiki_name):
subprocess.check_call((_tw_path(), "--init"))
def _save_wikifolder_to_config(wiki_name: str) -> bool:
"""
Set the wiki_folder config option to the wiki_name we initialized with,
if it's not already set in the config.
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:
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"you're initializing, {wiki_name}. Please check your config file "
"and update this option if necessary.")
return False
return True
def _add_filesystem_plugins(wiki_name: str) -> None:
print("tzk: Adding filesystem plugins to tiddlywiki.info...")
info_path = Path.cwd() / wiki_name / "tiddlywiki.info"
with info_path.open("r") as f:
info_data = json.load(f)
info_data['plugins'] = ["tiddlywiki/filesystem", "tiddlywiki/tiddlyweb"]
with info_path.open("w") as f:
json.dump(info_data, f)
def _init_gitignore() -> None:
print("tzk: Creating gitignore...")
GITIGNORE = dedent("""
__pycache__/
node_modules/
.peru/
output/
\$__StoryList.tid
""").strip()
with open(".gitignore", "w") as f:
f.write(GITIGNORE)
def _initial_commit() -> None:
print("tzk: Initializing new Git repository for wiki...")
git.exec("init")
print("tzk: Committing changes to repository...")
git.exec("add", "-A")
git.exec("commit", "-m", "Initial commit")
def install(wiki_name: str, tw_version_spec: str, author: Optional[str]):
# assert: caller has checked npm and git are installed
warnings = False
if author is None:
author = _whoami()
_init_npm(wiki_name, tw_version_spec, author)
_init_tw(wiki_name)
warnings |= not _save_wikifolder_to_config(wiki_name)
_add_filesystem_plugins(wiki_name)
_init_gitignore()
_initial_commit()
if warnings:
print("tzk: Initialization completed with warnings. Read the output and "
"make any changes required, then run 'tzk listen' to start the server.")
else:
print("tzk: Initialized successfully. Run 'tzk listen' to start the server.")

104
tzk.py
View File

@ -1,20 +1,26 @@
from abc import ABC, abstractmethod, abstractclassmethod from abc import ABC, abstractmethod, abstractclassmethod
import argparse import argparse
import os import os
import shutil
import sys import sys
from typing import Optional
import config from config import cm
import git import git
import tw import tw
from util import fail
class CliCommand(ABC): class CliCommand(ABC):
cmd = None # type: str
help = None # type: str
@abstractclassmethod @abstractclassmethod
def setup_arguments(self, parser: argparse.ArgumentParser) -> None: def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def execute(self, parser: argparse.ArgumentParser) -> None: def execute(self, parser: argparse.Namespace) -> None:
raise NotImplementedError raise NotImplementedError
@ -23,7 +29,7 @@ class CommitCommand(CliCommand):
help = "Commit all changes to the wiki repository." help = "Commit all changes to the wiki repository."
@classmethod @classmethod
def setup_arguments(self, parser: argparse.ArgumentParser) -> None: def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"-m", "--message", "-m", "--message",
metavar="MSG", metavar="MSG",
@ -46,11 +52,9 @@ class CommitCommand(CliCommand):
if cm.commit_require_branch: if cm.commit_require_branch:
current_branch = git.read("rev-parse", "--abbrev-ref", "HEAD") current_branch = git.read("rev-parse", "--abbrev-ref", "HEAD")
if current_branch != cm.commit_require_branch: if current_branch != cm.commit_require_branch:
print(f"You are on the '{current_branch}' branch, " fail(f"You are on the '{current_branch}' branch, "
f"but your TZK configuration requires you to be on the " f"but your TZK configuration requires you to be on the "
f"'{cm.commit_require_branch}' branch to commit.", f"'{cm.commit_require_branch}' branch to commit.")
file=sys.stderr)
sys.exit(1)
git.exec("add", "-A") git.exec("add", "-A")
git.exec("commit", "-m", args.message) git.exec("commit", "-m", args.message)
@ -63,7 +67,7 @@ class ListenCommand(CliCommand):
help = "Start a TiddlyWiki server in the current directory." help = "Start a TiddlyWiki server in the current directory."
@classmethod @classmethod
def setup_arguments(self, parser: argparse.ArgumentParser) -> None: def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"-p", "--port", "-p", "--port",
metavar="PORT", metavar="PORT",
@ -84,27 +88,85 @@ class ListenCommand(CliCommand):
) )
def execute(self, args: argparse.Namespace) -> None: def execute(self, args: argparse.Namespace) -> None:
tw.exec( try:
[ tw.exec(
("listen", [
f"port={args.port}", ("listen",
f"username={args.username}", f"port={args.port}",
f"password={args.password}") f"username={args.username}",
] f"password={args.password}")
]
)
except KeyboardInterrupt:
# We'll terminate anyway now that we're at the end of execute() --
# no need to display the traceback to the user.
pass
class InitCommand(CliCommand):
cmd = "init"
help = "Set up a new TZK directory."
@classmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-n", "--wiki-name",
metavar="NAME",
help="The wiki will be installed in a subfolder of the current directory, called NAME.",
default="wiki",
)
parser.add_argument(
"-v", "--tiddlywiki-version-spec",
metavar="SPEC",
help="NPM version spec for the version of TiddlyWiki to start your package.json at.",
default="^5.1.23",
)
parser.add_argument(
"-a", "--author",
metavar="AUTHOR_NAME",
help="The author to be credited in the package.json, if not your current username.",
default=None,
) )
def _precheck(self):
if shutil.which("npm") is None:
fail("TZK requires NPM. Please install NPM and make it available on your PATH.\n"
"https://docs.npmjs.com/downloading-and-installing-node-js-and-npm")
if shutil.which("git") is None:
fail("TZK requires Git. Please install Git and make it available on your PATH.\n"
"https://git-scm.com/book/en/v2/Getting-Started-Installing-Git")
if os.path.exists("package.json"):
fail("A 'package.json' file already exists in the current directory. "
"Perhaps you've already initialized a TZK repository here?")
def execute(self, args: argparse.Namespace) -> None:
self._precheck()
tw.install(args.wiki_name, args.tiddlywiki_version_spec, args.author)
cm = config.ConfigurationManager()
os.chdir("zk-wiki")
# TODO: confirm we're in the right directory
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
for command in CliCommand.__subclasses__(): for command in CliCommand.__subclasses__():
subparser = subparsers.add_parser(command.cmd, help=command.help) subparser = subparsers.add_parser(command.cmd, help=command.help)
subparser.set_defaults(_cls=command) subparser.set_defaults(_cls=command)
command.setup_arguments(subparser) command.setup_arguments(subparser) # type: ignore
args = parser.parse_args() args = parser.parse_args()
# For all operations except 'init', we start in 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 "
"of the wiki subfolder within the current directory.")
try:
os.chdir(cm.wiki_folder)
except FileNotFoundError:
fail(f"Tried to change directory into the wiki_folder '{cm.wiki_folder}' "
f"specified in your config file, but that directory does not exist.")
# TODO: confirm we're in the right directory
args._cls().execute(args) args._cls().execute(args)

7
util.py Normal file
View File

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