Compare commits

...
2 Commits
Author SHA1 Message Date
omicron 7bf04ff064 Implement cli init command
closes #1
2026-10-20 18:05:15 +02:00
omicron 278d3e937e Add to_dict and to_file API to SiteConfig 2026-10-19 01:23:21 +02:00
4 changed files with 183 additions and 2 deletions

No files matched your search

+77 -2
View File
@@ -1,11 +1,14 @@
import argparse import argparse
import logging import logging
import sys import sys
from collections.abc import Callable
from omicron.ssg.output import OutputError from omicron.ssg.output import OutputError
from omicron.ssg.site import Site from omicron.ssg.site import Site
from omicron.ssg.config import ConfigError from omicron.ssg.config import ConfigError, SiteConfig, DEFAULT_ITEMS_PER_PAGE
from pathlib import Path from pathlib import Path
log = logging.getLogger(__name__)
class CliError(RuntimeError): class CliError(RuntimeError):
pass pass
@@ -46,8 +49,15 @@ def setup_argparse() -> argparse.ArgumentParser:
default=False, default=False,
help="include draft content", help="include draft content",
) )
# clean command
subparsers.add_parser("clean", help="remove the output directory") subparsers.add_parser("clean", help="remove the output directory")
# init command
subparsers.add_parser(
"init", help="create a new site directory", formatter_class=help_formatter
)
return parser return parser
@@ -84,15 +94,80 @@ def clean(args: argparse.Namespace) -> None:
site.clean() site.clean()
def prompt(
label: str,
explanation: str | None = None,
default: str | None = None,
required: bool = True,
validate: Callable[[str], bool] | None = None,
error: str = "Invalid value.",
) -> str:
if explanation:
print(explanation)
hint = f" [{default}]" if default is not None else ""
while True:
value = input(f"{label}{hint}: ").strip()
if not value and default is not None:
value = default
if not value and not required:
return ""
if not value:
print("This field is required.")
continue
if validate and not validate(value):
print(error)
continue
return value
def init(args: argparse.Namespace) -> None:
try:
args.site.mkdir(parents=True, exist_ok=True)
except (FileExistsError, NotADirectoryError) as e:
raise CliError(f"{args.site} is not a directory") from e
if any(args.site.iterdir()):
raise CliError(f"{args.site} is not empty")
config_path = args.site / "config.yml"
name = prompt("Site name")
base_url = prompt(
"Base URL", "Optional, e.g. https://example.com/blog", required=False
)
template = prompt("Template", default="plain")
items_per_page = prompt(
"Items per page",
default=str(DEFAULT_ITEMS_PER_PAGE),
validate=lambda v: v.isdigit() and int(v) > 0,
error="Items per page must be a positive integer.",
)
config = SiteConfig.from_dict(
{
"name": name,
"base_url": base_url,
"template": template,
"items_per_page": int(items_per_page),
}
)
Site.resolve_template_path(config.template)
config.to_file(config_path)
(args.site / "content").mkdir(exist_ok=True)
log.info("Created new site at %s", args.site)
def cli() -> None: def cli() -> None:
parser = setup_argparse() parser = setup_argparse()
args = parser.parse_args() args = parser.parse_args()
log_file = getattr(args, "log_file", args.site / "build.log") default_log_file = None if args.command == "init" else args.site / "build.log"
log_file = getattr(args, "log_file", default_log_file)
setup_logging(verbose=bool(args.verbose), log_file=log_file) setup_logging(verbose=bool(args.verbose), log_file=log_file)
if args.command == "build": if args.command == "build":
build(args) build(args)
elif args.command == "clean": elif args.command == "clean":
clean(args) clean(args)
elif args.command == "init":
init(args)
else: else:
raise NotImplementedError(f"'{args.command}' command not implemented yet") raise NotImplementedError(f"'{args.command}' command not implemented yet")
+13
View File
@@ -99,3 +99,16 @@ class SiteConfig:
items_per_page=items_per_page, items_per_page=items_per_page,
template=template, template=template,
) )
def to_dict(self) -> dict[str, str | int]:
return {
"name": self.name,
"template": self.template,
"items_per_page": self.items_per_page,
"base_url": f"{self.origin}{self.base_dir}",
}
def to_file(self, path: Path) -> None:
log.debug("Writing config file %s", path)
with open(path, "w") as f:
yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
+36
View File
@@ -0,0 +1,36 @@
import subprocess
import yaml
def test_init_creates_site(tmp_path):
site_dir = tmp_path / "newsite"
result = subprocess.run(
["ossg", "--site", str(site_dir), "init"],
input="My Site\nhttps://example.com\n\n\n",
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
config = yaml.safe_load((site_dir / "config.yml").read_text())
assert config == {
"name": "My Site",
"base_url": "https://example.com/",
"template": "plain",
"items_per_page": 20,
}
assert (site_dir / "content").is_dir()
def test_init_refuses_non_empty_dir(tmp_path):
(tmp_path / "some_file").touch()
result = subprocess.run(
["ossg", "--site", str(tmp_path), "init"],
input="My Site\n\n\n\n",
capture_output=True,
text=True,
)
assert result.returncode == 1
assert "is not empty" in result.stdout
+57
View File
@@ -58,6 +58,48 @@ def test_from_dict_full():
assert config.base_dir == Path("/blog") assert config.base_dir == Path("/blog")
def test_to_dict():
config = SiteConfig(
name="My Site",
origin="http://example.com",
base_dir=Path("/blog"),
items_per_page=42,
template="plain",
)
assert config.to_dict() == {
"name": "My Site",
"template": "plain",
"items_per_page": 42,
"base_url": "http://example.com/blog",
}
def test_to_dict_default_base_url():
config = SiteConfig(
name="My Site",
origin="",
base_dir=Path("/"),
items_per_page=DEFAULT_ITEMS_PER_PAGE,
template="plain",
)
assert config.to_dict()["base_url"] == "/"
def test_to_dict_round_trip():
original = SiteConfig.from_dict(
{
"name": "My Site",
"template": "plain",
"base_url": "http://example.com/blog",
"items_per_page": 42,
}
)
assert SiteConfig.from_dict(original.to_dict()) == original
MISSING = object() MISSING = object()
@@ -128,6 +170,21 @@ def test_from_file_full(tmp_path):
assert config.base_dir == Path("/blog") assert config.base_dir == Path("/blog")
def test_to_file(tmp_path):
path = tmp_path / "config.yml"
config = SiteConfig(
name="My Site",
origin="http://example.com",
base_dir=Path("/blog"),
items_per_page=42,
template="plain",
)
config.to_file(path)
assert SiteConfig.from_file(path) == config
def test_from_file_missing(tmp_path): def test_from_file_missing(tmp_path):
path = tmp_path / "config.yml" path = tmp_path / "config.yml"