add feature for automatic init of a TZK repository
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | ||||
| __pycache__/ | ||||
| .vscode/ | ||||
|   | ||||
							
								
								
									
										41
									
								
								config.py
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								config.py
									
									
									
									
									
								
							| @@ -1,27 +1,56 @@ | ||||
| import datetime | ||||
| import importlib | ||||
| import os | ||||
| from pathlib import Path | ||||
| import sys | ||||
| from typing import Any | ||||
|  | ||||
| from util import fail | ||||
|  | ||||
|  | ||||
| class ConfigurationManager: | ||||
|     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'): | ||||
|                 mod_name = child.name.rsplit('.', 1)[0] | ||||
|                 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) | ||||
|                     del sys.path[0] | ||||
|                     break | ||||
|         else: | ||||
|             print( | ||||
|             fail( | ||||
|                 f"Your TZK config file could not be found. " | ||||
|                 f"Please ensure there is a file called tzk_config.py " | ||||
|                 f"in the current directory.", file=sys.stderr) | ||||
|             sys.exit(1) | ||||
|                 f"in the current directory.") | ||||
|  | ||||
|     def __getattr__(self, attr): | ||||
|         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 | ||||
| from typing import Sequence | ||||
|  | ||||
| def exec(*args: Sequence[str]): | ||||
|     return subprocess.call(["git", *args]) | ||||
| def exec(*args: str): | ||||
|     return subprocess.check_call(["git", *args]) | ||||
|  | ||||
| def read(*args: Sequence[str]): | ||||
|     return subprocess.check_output(["git", *args], text=True).strip() | ||||
| def read(*args: str): | ||||
|     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 | ||||
| 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: | ||||
|     bin_dir = subprocess.check_output(("npm", "bin"), text=True).strip() | ||||
|     call_args = [bin_dir + "/tiddlywiki"] | ||||
| @functools.lru_cache(1) | ||||
| def _npm_bin() -> str: | ||||
|     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: | ||||
|         call_args.append(f"--{tw_arg[0]}") | ||||
|         for inner_arg in tw_arg[1:]: | ||||
|             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 | ||||
| import argparse | ||||
| import os | ||||
| import shutil | ||||
| import sys | ||||
| from typing import Optional | ||||
|  | ||||
| import config | ||||
| from config import cm | ||||
| import git | ||||
| import tw | ||||
| from util import fail | ||||
|  | ||||
|  | ||||
| class CliCommand(ABC): | ||||
|     cmd = None   # type: str | ||||
|     help = None  # type: str | ||||
|  | ||||
|     @abstractclassmethod | ||||
|     def setup_arguments(self, parser: argparse.ArgumentParser) -> None: | ||||
|     def setup_arguments(cls, parser: argparse.ArgumentParser) -> None: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @abstractmethod | ||||
|     def execute(self, parser: argparse.ArgumentParser) -> None: | ||||
|     def execute(self, parser: argparse.Namespace) -> None: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| @@ -23,7 +29,7 @@ class CommitCommand(CliCommand): | ||||
|     help = "Commit all changes to the wiki repository." | ||||
|  | ||||
|     @classmethod | ||||
|     def setup_arguments(self, parser: argparse.ArgumentParser) -> None: | ||||
|     def setup_arguments(cls, parser: argparse.ArgumentParser) -> None: | ||||
|         parser.add_argument( | ||||
|             "-m", "--message", | ||||
|             metavar="MSG", | ||||
| @@ -46,11 +52,9 @@ class CommitCommand(CliCommand): | ||||
|         if cm.commit_require_branch: | ||||
|             current_branch = git.read("rev-parse", "--abbrev-ref", "HEAD") | ||||
|             if current_branch != cm.commit_require_branch: | ||||
|                 print(f"You are on the '{current_branch}' branch, " | ||||
|                       f"but your TZK configuration requires you to be on the " | ||||
|                       f"'{cm.commit_require_branch}' branch to commit.", | ||||
|                       file=sys.stderr) | ||||
|                 sys.exit(1) | ||||
|                 fail(f"You are on the '{current_branch}' branch, " | ||||
|                      f"but your TZK configuration requires you to be on the " | ||||
|                      f"'{cm.commit_require_branch}' branch to commit.") | ||||
|  | ||||
|         git.exec("add", "-A") | ||||
|         git.exec("commit", "-m", args.message) | ||||
| @@ -63,7 +67,7 @@ class ListenCommand(CliCommand): | ||||
|     help = "Start a TiddlyWiki server in the current directory." | ||||
|  | ||||
|     @classmethod | ||||
|     def setup_arguments(self, parser: argparse.ArgumentParser) -> None: | ||||
|     def setup_arguments(cls, parser: argparse.ArgumentParser) -> None: | ||||
|         parser.add_argument( | ||||
|             "-p", "--port", | ||||
|             metavar="PORT", | ||||
| @@ -84,27 +88,85 @@ class ListenCommand(CliCommand): | ||||
|         ) | ||||
|      | ||||
|     def execute(self, args: argparse.Namespace) -> None: | ||||
|         tw.exec( | ||||
|             [ | ||||
|                 ("listen", | ||||
|                  f"port={args.port}", | ||||
|                  f"username={args.username}", | ||||
|                  f"password={args.password}") | ||||
|             ] | ||||
|         try: | ||||
|             tw.exec( | ||||
|                 [ | ||||
|                     ("listen", | ||||
|                     f"port={args.port}", | ||||
|                     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() | ||||
| subparsers = parser.add_subparsers() | ||||
| for command in CliCommand.__subclasses__(): | ||||
|     subparser = subparsers.add_parser(command.cmd, help=command.help) | ||||
|     subparser.set_defaults(_cls=command) | ||||
|     command.setup_arguments(subparser) | ||||
|     command.setup_arguments(subparser)  # type: ignore | ||||
|  | ||||
| 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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Soren I. Bjornstad
					Soren I. Bjornstad