From 8636ee18e122683a1ca7b30ec97eb6c61c4652fb Mon Sep 17 00:00:00 2001 From: Dan Shernicoff Date: Fri, 5 Sep 2025 15:47:01 -0400 Subject: [PATCH 1/3] Add the ability to set the editor for `new-entry`. Formatting fixes. --- src/render_engine_cli/cli.py | 42 +++++++++++++++++++++++++++------- src/render_engine_cli/utils.py | 41 ++++++++++++++++++++++++++------- tests/test_cli.py | 10 ++++++++ 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/render_engine_cli/cli.py b/src/render_engine_cli/cli.py index ce97d9c..2bb5c69 100644 --- a/src/render_engine_cli/cli.py +++ b/src/render_engine_cli/cli.py @@ -1,6 +1,5 @@ import datetime import json -import os import re import subprocess from pathlib import Path @@ -8,6 +7,7 @@ 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 @@ -15,6 +15,7 @@ create_collection_entry, display_filtered_templates, get_available_themes, + get_editor, get_site, get_site_content_paths, remove_output_folder, @@ -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`. @@ -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", @@ -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", @@ -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""" @@ -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: @@ -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]) diff --git a/src/render_engine_cli/utils.py b/src/render_engine_cli/utils.py index a9fcdf3..2c3e79d 100644 --- a/src/render_engine_cli/utils.py +++ b/src/render_engine_cli/utils.py @@ -2,6 +2,8 @@ import re import shutil import sys +from dataclasses import dataclass +from os import getenv from pathlib import Path import click @@ -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: @@ -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""" @@ -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")): @@ -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, ".") @@ -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}.") @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index 089347e..b741189 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,6 +7,7 @@ create_collection_entry, display_filtered_templates, get_available_themes, + get_editor, get_site_content_paths, split_args, split_module_site, @@ -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 From bd3051afcd70323d6bcb5aa731822977fe0b1f9a Mon Sep 17 00:00:00 2001 From: Dan Shernicoff Date: Fri, 5 Sep 2025 15:53:02 -0400 Subject: [PATCH 2/3] Fix broken tests. --- tests/test_cli_commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index 3f30fc9..f40190b 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -127,7 +127,7 @@ 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 *_: {}) + monkeypatch.setattr("render_engine_cli.utils.getenv", lambda *_: {}) # Create content directory content_dir = tmp_path / "content" @@ -172,7 +172,7 @@ 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 *_: {}) + monkeypatch.setattr("render_engine_cli.utils.getenv", lambda *_: {}) content_dir = tmp_path / "content" content_dir.mkdir() @@ -268,7 +268,7 @@ 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.utils.getenv", lambda *_: {}) monkeypatch.setattr("render_engine_cli.cli.create_collection_entry", mock_create_collection_entry) content_dir = tmp_path / "content" content_dir.mkdir() From 430434e99a222e89e6d42f8e589f30ffb28256ca Mon Sep 17 00:00:00 2001 From: Dan Shernicoff Date: Fri, 5 Sep 2025 16:13:17 -0400 Subject: [PATCH 3/3] Remove unnecessary monkeypatches. --- tests/test_cli_commands.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index f40190b..cdebd9b 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -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.utils.getenv", lambda *_: {}) # Create content directory content_dir = tmp_path / "content" @@ -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.utils.getenv", lambda *_: {}) content_dir = tmp_path / "content" content_dir.mkdir() @@ -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.utils.getenv", lambda *_: {}) monkeypatch.setattr("render_engine_cli.cli.create_collection_entry", mock_create_collection_entry) content_dir = tmp_path / "content" content_dir.mkdir()