Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 34 additions & 8 deletions src/render_engine_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import datetime
import json
import os
import re
import subprocess
from pathlib import Path

import click
from dateutil import parser as dateparser
from dateutil.parser import ParserError
from render_engine import Collection
from rich.console import Console

from render_engine_cli.event import ServerEventHandler
from render_engine_cli.utils import (
create_collection_entry,
display_filtered_templates,
get_available_themes,
get_editor,
get_site,
get_site_content_paths,
remove_output_folder,
Expand Down Expand Up @@ -145,7 +146,7 @@ def build(module_site: str, clean: bool):
is_flag=True,
default=False,
)
@click.option("-p", "--port", type=click.IntRange(0, 65534), help="Port to serve on", default=8000)
@click.option("-p", "--port", type=click.IntRange(0, 65534), help="Port to serve on", default=8000.0)
def serve(module_site: str, clean: bool, reload: bool, port: int):
"""
Create an HTTP server to serve the site at `localhost`.
Expand Down Expand Up @@ -217,9 +218,19 @@ def serve(module_site: str, clean: bool, reload: bool, port: int):
help="Title for the new entry.",
default=None,
)
@click.option("-s", "--slug", type=click.STRING, help="Slug for the new page.", callback=validate_file_name_or_slug)
@click.option(
"-d", "--include-date", is_flag=True, default=False, help="Include today's date in the metadata for your entry."
"-s",
"--slug",
type=click.STRING,
help="Slug for the new page.",
callback=validate_file_name_or_slug,
)
@click.option(
"-d",
"--include-date",
is_flag=True,
default=False,
help="Include today's date in the metadata for your entry.",
)
@click.option(
"-a",
Expand All @@ -228,7 +239,15 @@ def serve(module_site: str, clean: bool, reload: bool, port: int):
type=click.STRING,
help="key value attrs to include in your entry use the format `--args key=value` or `--args key:value`",
)
@click.option("--editor/--no-editor", default=True, help="Load the system editor after the file is created.")
@click.option(
"-e",
"--editor",
default="default",
type=click.STRING,
callback=get_editor,
help="Select the editor to use. If not set the default editor (as set by the EDITOR environment variable) "
"will be used. If 'none' is set no editor will be launched.",
)
@click.option(
"-f",
"--filename",
Expand All @@ -245,7 +264,7 @@ def new_entry(
slug: str,
include_date: bool,
args: list[str],
editor: bool,
editor: str,
filename: str,
):
"""Creates a new collection entry based on the parser. Entries are added to the Collections content_path"""
Expand All @@ -272,12 +291,20 @@ def new_entry(

module, site_name = split_module_site(module_site)
site = get_site(module, site_name)
_collection: Collection
if not (
_collection := next(
coll for coll in site.route_list.values() if type(coll).__name__.lower() == collection.lower()
)
):
raise click.exceptions.BadParameter(f"Unknown collection: {collection}")
filepath = Path(_collection.content_path).joinpath(filename)
if filepath.exists():
if not click.confirm(
f"File {filename} exists are {_collection.content_path} - do you wish to overwrite that file?"
):
click.secho("Aborting new entry.", fg="yellow")
return
if content and content_file:
raise TypeError("Both content and content_file provided. At most one may be provided.")
if content_file:
Expand All @@ -287,11 +314,10 @@ def new_entry(
# If we had a title earlier this is where we replace the default that is added by the template handler with
# the one supplied by the user.
entry = re.sub(r"title: Untitled Entry", f"title: {title}", entry)
filepath = Path(_collection.content_path).joinpath(filename)
filepath.write_text(entry)
Console().print(f'New {collection} entry created at "{filepath}"')

if editor and (editor := os.getenv("EDITOR", None)):
if editor:
subprocess.run([editor, filepath])


Expand Down
41 changes: 33 additions & 8 deletions src/render_engine_cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import re
import shutil
import sys
from dataclasses import dataclass
from os import getenv
from pathlib import Path

import click
Expand All @@ -16,14 +18,10 @@
CONFIG_FILE_NAME = "pyproject.toml"


@dataclass
class CliConfig:
"""Handles loading and storing the config from disk"""

def __init__(self):
self._module_site = None
self._collection = None
self._config_loaded = False

@property
def module_site(self):
if not self._config_loaded:
Expand All @@ -38,9 +36,20 @@ def collection(self):
self._config_loaded = True
return self._collection

@property
def editor(self):
if not self._config_loaded:
self.load_config()
self._config_loaded = True
return self._editor

# Initialize the arguments and default values
_module_site, _collection = None, None
default_module_site, default_collection = None, None
_module_site: str = None
_collection: str = None
default_module_site: str = None
default_collection: str = None
_editor: str = None
_config_loaded: bool = False

def load_config(self, config_file: str = CONFIG_FILE_NAME):
"""Load the config from the file"""
Expand All @@ -61,6 +70,7 @@ def load_config(self, config_file: str = CONFIG_FILE_NAME):
except FileNotFoundError:
click.echo(f"No config file found at {config_file}")

self._editor = stored_config.get("editor", getenv("EDITOR"))
if stored_config:
# Populate the argument variables and default values from the config
if (module := stored_config.get("module")) and (site := stored_config.get("site")):
Expand All @@ -69,6 +79,9 @@ def load_config(self, config_file: str = CONFIG_FILE_NAME):
self._collection = default_collection


config = CliConfig()


def get_site(import_path: str, site: str, reload: bool = False) -> Site:
"""Split the site module into a module and a class name"""
sys.path.insert(0, ".")
Expand Down Expand Up @@ -165,15 +178,16 @@ def validate_module_site(ctx: dict, param: str, value: str) -> str:


def validate_collection(ctx: dict, param: click.Option, value: str) -> str:
"""Validate the collection option"""
if value:
return value
config = CliConfig()
if config.collection:
return config.collection
raise click.exceptions.BadParameter("collection must be specified.")


def validate_file_name_or_slug(ctx: click.Context, param: click.Option, value: str) -> str | None:
"""Validate the filename and slug options"""
if value:
if " " in value:
raise click.exceptions.BadParameter(f"Spaces are not allowed in {param.name}.")
Expand All @@ -187,3 +201,14 @@ def validate_file_name_or_slug(ctx: click.Context, param: click.Option, value: s
if param.name == "filename":
raise click.exceptions.BadParameter("One of filename, title, or slug must be provided.")
return None


def get_editor(ctx: click.Context, param: click.Option, value: str) -> str | None:
"""Get the appropriate editor"""
match value.casefold():
case "default":
return config.editor
case "none":
return None
case _:
return value
10 changes: 10 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
create_collection_entry,
display_filtered_templates,
get_available_themes,
get_editor,
get_site_content_paths,
split_args,
split_module_site,
Expand Down Expand Up @@ -232,3 +233,12 @@ def test_display_filtered_templates():
# Check that the table was created with filtered results
call_args = mock_rprint.call_args[0][0]
assert call_args.title == "Test Templates"


@pytest.mark.parametrize(
"selection, expected", [("none", None), ("DEFAULT", "vim"), ("default", "vim"), ("nano", "nano")]
)
def test_get_editor(selection, expected, monkeypatch):
"""Test the get_editor callback"""
monkeypatch.setattr("render_engine_cli.utils.getenv", lambda *_: "vim")
assert get_editor(None, None, value=selection) == expected
3 changes: 0 additions & 3 deletions tests/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ def test_new_entry_command_success(runner, test_site_module, monkeypatch):
"""Tests new_entry command with valid parameters"""
tmp_path, module_site = test_site_module
monkeypatch.chdir(tmp_path)
monkeypatch.setattr("render_engine_cli.cli.os.getenv", lambda *_: {})

# Create content directory
content_dir = tmp_path / "content"
Expand Down Expand Up @@ -172,7 +171,6 @@ def test_new_entry_command_with_args(runner, test_site_module, monkeypatch):
"""Tests new_entry command with --args parameter"""
tmp_path, module_site = test_site_module
monkeypatch.chdir(tmp_path)
monkeypatch.setattr("render_engine_cli.cli.os.getenv", lambda *_: {})

content_dir = tmp_path / "content"
content_dir.mkdir()
Expand Down Expand Up @@ -268,7 +266,6 @@ def mock_create_collection_entry(**kwargs):

tmp_path, module_site = test_site_module
monkeypatch.chdir(tmp_path)
monkeypatch.setattr("render_engine_cli.cli.os.getenv", lambda *_: {})
monkeypatch.setattr("render_engine_cli.cli.create_collection_entry", mock_create_collection_entry)
content_dir = tmp_path / "content"
content_dir.mkdir()
Expand Down
Loading