Compare commits
3
Commits
b97487eb4b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1ecd0f480 | ||
|
|
7bf04ff064 | ||
|
|
278d3e937e |
No files matched your search
+76
-2
@@ -1,11 +1,14 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from omicron.ssg.output import OutputError
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CliError(RuntimeError):
|
||||
pass
|
||||
@@ -46,8 +49,15 @@ def setup_argparse() -> argparse.ArgumentParser:
|
||||
default=False,
|
||||
help="include draft content",
|
||||
)
|
||||
|
||||
# clean command
|
||||
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
|
||||
|
||||
|
||||
@@ -84,15 +94,79 @@ def clean(args: argparse.Namespace) -> None:
|
||||
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),
|
||||
}
|
||||
)
|
||||
|
||||
config.to_file(config_path)
|
||||
(args.site / "content").mkdir(exist_ok=True)
|
||||
log.info("Created new site at %s", args.site)
|
||||
|
||||
|
||||
def cli() -> None:
|
||||
parser = setup_argparse()
|
||||
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)
|
||||
if args.command == "build":
|
||||
build(args)
|
||||
elif args.command == "clean":
|
||||
clean(args)
|
||||
elif args.command == "init":
|
||||
init(args)
|
||||
else:
|
||||
raise NotImplementedError(f"'{args.command}' command not implemented yet")
|
||||
|
||||
|
||||
@@ -99,3 +99,16 @@ class SiteConfig:
|
||||
items_per_page=items_per_page,
|
||||
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)
|
||||
+9
-5
@@ -27,6 +27,7 @@ from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
builtin_templates = Path(__file__).parent / "templates"
|
||||
|
||||
|
||||
class Site:
|
||||
@@ -43,15 +44,18 @@ class Site:
|
||||
self.tags_by_count: list[tuple[str, int]] = []
|
||||
|
||||
self.config = SiteConfig.from_file(site / "config.yml")
|
||||
self.template_path: Path = Site.resolve_template_path(self.config.template)
|
||||
self.template_path: Path = self.resolve_template_path(self.config.template)
|
||||
self.output_path: Path = self.site_path / "output"
|
||||
self.jinja_env = Environment(loader=FileSystemLoader(self.template_path))
|
||||
|
||||
@staticmethod
|
||||
def resolve_template_path(template: str) -> Path:
|
||||
path = Path(__file__).parent / "templates" / template
|
||||
def resolve_template_path(self, template: str) -> Path:
|
||||
path = self.site_path / "templates" / template
|
||||
if path.is_dir():
|
||||
return path
|
||||
|
||||
path = builtin_templates / template
|
||||
if not path.is_dir():
|
||||
raise ConfigError(f"template '{template}' not found at {path}")
|
||||
raise ConfigError(f"template '{template}' not found.")
|
||||
return path
|
||||
|
||||
def add_by_section(self, content: Content) -> None:
|
||||
|
||||
@@ -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
|
||||
@@ -58,6 +58,48 @@ def test_from_dict_full():
|
||||
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()
|
||||
|
||||
|
||||
@@ -128,6 +170,21 @@ def test_from_file_full(tmp_path):
|
||||
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):
|
||||
path = tmp_path / "config.yml"
|
||||
|
||||
|
||||
+22
-2
@@ -78,9 +78,29 @@ def test_clean_removes_output_directory(make_site):
|
||||
assert not site.output_path.exists()
|
||||
|
||||
|
||||
def test_resolve_template_path_raises_for_missing_template():
|
||||
def test_resolve_template_path_raises_for_missing_template(site_dir):
|
||||
path = site_dir("thin_site")
|
||||
(path / "config.yml").write_text("name: test\ntemplate: does-not-exist\n")
|
||||
|
||||
with pytest.raises(ConfigError, match="not found"):
|
||||
Site.resolve_template_path("does-not-exist")
|
||||
Site(path)
|
||||
|
||||
|
||||
def test_resolve_template_path_prefers_local_override(site_dir):
|
||||
path = site_dir("thin_site")
|
||||
(path / "config.yml").write_text("name: test\ntemplate: plain\n")
|
||||
local = path / "templates" / "plain"
|
||||
local.mkdir(parents=True)
|
||||
|
||||
assert Site(path).template_path == local
|
||||
|
||||
|
||||
def test_resolve_template_path_falls_back_to_builtin(site_dir):
|
||||
path = site_dir("thin_site")
|
||||
(path / "config.yml").write_text("name: test\ntemplate: plain\n")
|
||||
|
||||
assert Site(path).template_path.name == "plain"
|
||||
assert not Site(path).template_path.is_relative_to(path)
|
||||
|
||||
|
||||
def test_home_url(site_dir):
|
||||
|
||||
Reference in new issue
Block a user