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 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,79 @@ 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),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
+9
-5
@@ -27,6 +27,7 @@ from pathlib import Path
|
|||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
builtin_templates = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
class Site:
|
class Site:
|
||||||
@@ -43,15 +44,18 @@ class Site:
|
|||||||
self.tags_by_count: list[tuple[str, int]] = []
|
self.tags_by_count: list[tuple[str, int]] = []
|
||||||
|
|
||||||
self.config = SiteConfig.from_file(site / "config.yml")
|
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.output_path: Path = self.site_path / "output"
|
||||||
self.jinja_env = Environment(loader=FileSystemLoader(self.template_path))
|
self.jinja_env = Environment(loader=FileSystemLoader(self.template_path))
|
||||||
|
|
||||||
@staticmethod
|
def resolve_template_path(self, template: str) -> Path:
|
||||||
def resolve_template_path(template: str) -> Path:
|
path = self.site_path / "templates" / template
|
||||||
path = Path(__file__).parent / "templates" / template
|
if path.is_dir():
|
||||||
|
return path
|
||||||
|
|
||||||
|
path = builtin_templates / template
|
||||||
if not path.is_dir():
|
if not path.is_dir():
|
||||||
raise ConfigError(f"template '{template}' not found at {path}")
|
raise ConfigError(f"template '{template}' not found.")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def add_by_section(self, content: Content) -> None:
|
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")
|
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"
|
||||||
|
|
||||||
|
|||||||
+22
-2
@@ -78,9 +78,29 @@ def test_clean_removes_output_directory(make_site):
|
|||||||
assert not site.output_path.exists()
|
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"):
|
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):
|
def test_home_url(site_dir):
|
||||||
|
|||||||
Reference in new issue
Block a user