add feature for automatic init of a TZK repository
This commit is contained in:
parent
d23070435a
commit
a48f527ec8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
.vscode/
|
||||||
|
41
config.py
41
config.py
@ -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
8
git.py
@ -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
160
tw.py
@ -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
104
tzk.py
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user