Skip to content

Commit ffa2676

Browse files
authored
Add the ability to set the editor for new-entry. Formatting fixes. (#18)
- Adds ability to set the editor for new-entry from the command line - Adds ability to configure the default editor for new-entry - Converts CliConfig to dataclass - Formatting issues - Missing doc strings
1 parent 0e99637 commit ffa2676

File tree

4 files changed

+77
-19
lines changed

4 files changed

+77
-19
lines changed

src/render_engine_cli/cli.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import datetime
22
import json
3-
import os
43
import re
54
import subprocess
65
from pathlib import Path
76

87
import click
98
from dateutil import parser as dateparser
109
from dateutil.parser import ParserError
10+
from render_engine import Collection
1111
from rich.console import Console
1212

1313
from render_engine_cli.event import ServerEventHandler
1414
from render_engine_cli.utils import (
1515
create_collection_entry,
1616
display_filtered_templates,
1717
get_available_themes,
18+
get_editor,
1819
get_site,
1920
get_site_content_paths,
2021
remove_output_folder,
@@ -145,7 +146,7 @@ def build(module_site: str, clean: bool):
145146
is_flag=True,
146147
default=False,
147148
)
148-
@click.option("-p", "--port", type=click.IntRange(0, 65534), help="Port to serve on", default=8000)
149+
@click.option("-p", "--port", type=click.IntRange(0, 65534), help="Port to serve on", default=8000.0)
149150
def serve(module_site: str, clean: bool, reload: bool, port: int):
150151
"""
151152
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):
217218
help="Title for the new entry.",
218219
default=None,
219220
)
220-
@click.option("-s", "--slug", type=click.STRING, help="Slug for the new page.", callback=validate_file_name_or_slug)
221221
@click.option(
222-
"-d", "--include-date", is_flag=True, default=False, help="Include today's date in the metadata for your entry."
222+
"-s",
223+
"--slug",
224+
type=click.STRING,
225+
help="Slug for the new page.",
226+
callback=validate_file_name_or_slug,
227+
)
228+
@click.option(
229+
"-d",
230+
"--include-date",
231+
is_flag=True,
232+
default=False,
233+
help="Include today's date in the metadata for your entry.",
223234
)
224235
@click.option(
225236
"-a",
@@ -228,7 +239,15 @@ def serve(module_site: str, clean: bool, reload: bool, port: int):
228239
type=click.STRING,
229240
help="key value attrs to include in your entry use the format `--args key=value` or `--args key:value`",
230241
)
231-
@click.option("--editor/--no-editor", default=True, help="Load the system editor after the file is created.")
242+
@click.option(
243+
"-e",
244+
"--editor",
245+
default="default",
246+
type=click.STRING,
247+
callback=get_editor,
248+
help="Select the editor to use. If not set the default editor (as set by the EDITOR environment variable) "
249+
"will be used. If 'none' is set no editor will be launched.",
250+
)
232251
@click.option(
233252
"-f",
234253
"--filename",
@@ -245,7 +264,7 @@ def new_entry(
245264
slug: str,
246265
include_date: bool,
247266
args: list[str],
248-
editor: bool,
267+
editor: str,
249268
filename: str,
250269
):
251270
"""Creates a new collection entry based on the parser. Entries are added to the Collections content_path"""
@@ -272,12 +291,20 @@ def new_entry(
272291

273292
module, site_name = split_module_site(module_site)
274293
site = get_site(module, site_name)
294+
_collection: Collection
275295
if not (
276296
_collection := next(
277297
coll for coll in site.route_list.values() if type(coll).__name__.lower() == collection.lower()
278298
)
279299
):
280300
raise click.exceptions.BadParameter(f"Unknown collection: {collection}")
301+
filepath = Path(_collection.content_path).joinpath(filename)
302+
if filepath.exists():
303+
if not click.confirm(
304+
f"File {filename} exists are {_collection.content_path} - do you wish to overwrite that file?"
305+
):
306+
click.secho("Aborting new entry.", fg="yellow")
307+
return
281308
if content and content_file:
282309
raise TypeError("Both content and content_file provided. At most one may be provided.")
283310
if content_file:
@@ -287,11 +314,10 @@ def new_entry(
287314
# If we had a title earlier this is where we replace the default that is added by the template handler with
288315
# the one supplied by the user.
289316
entry = re.sub(r"title: Untitled Entry", f"title: {title}", entry)
290-
filepath = Path(_collection.content_path).joinpath(filename)
291317
filepath.write_text(entry)
292318
Console().print(f'New {collection} entry created at "{filepath}"')
293319

294-
if editor and (editor := os.getenv("EDITOR", None)):
320+
if editor:
295321
subprocess.run([editor, filepath])
296322

297323

src/render_engine_cli/utils.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import re
33
import shutil
44
import sys
5+
from dataclasses import dataclass
6+
from os import getenv
57
from pathlib import Path
68

79
import click
@@ -16,14 +18,10 @@
1618
CONFIG_FILE_NAME = "pyproject.toml"
1719

1820

21+
@dataclass
1922
class CliConfig:
2023
"""Handles loading and storing the config from disk"""
2124

22-
def __init__(self):
23-
self._module_site = None
24-
self._collection = None
25-
self._config_loaded = False
26-
2725
@property
2826
def module_site(self):
2927
if not self._config_loaded:
@@ -38,9 +36,20 @@ def collection(self):
3836
self._config_loaded = True
3937
return self._collection
4038

39+
@property
40+
def editor(self):
41+
if not self._config_loaded:
42+
self.load_config()
43+
self._config_loaded = True
44+
return self._editor
45+
4146
# Initialize the arguments and default values
42-
_module_site, _collection = None, None
43-
default_module_site, default_collection = None, None
47+
_module_site: str = None
48+
_collection: str = None
49+
default_module_site: str = None
50+
default_collection: str = None
51+
_editor: str = None
52+
_config_loaded: bool = False
4453

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

73+
self._editor = stored_config.get("editor", getenv("EDITOR"))
6474
if stored_config:
6575
# Populate the argument variables and default values from the config
6676
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):
6979
self._collection = default_collection
7080

7181

82+
config = CliConfig()
83+
84+
7285
def get_site(import_path: str, site: str, reload: bool = False) -> Site:
7386
"""Split the site module into a module and a class name"""
7487
sys.path.insert(0, ".")
@@ -165,15 +178,16 @@ def validate_module_site(ctx: dict, param: str, value: str) -> str:
165178

166179

167180
def validate_collection(ctx: dict, param: click.Option, value: str) -> str:
181+
"""Validate the collection option"""
168182
if value:
169183
return value
170-
config = CliConfig()
171184
if config.collection:
172185
return config.collection
173186
raise click.exceptions.BadParameter("collection must be specified.")
174187

175188

176189
def validate_file_name_or_slug(ctx: click.Context, param: click.Option, value: str) -> str | None:
190+
"""Validate the filename and slug options"""
177191
if value:
178192
if " " in value:
179193
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
187201
if param.name == "filename":
188202
raise click.exceptions.BadParameter("One of filename, title, or slug must be provided.")
189203
return None
204+
205+
206+
def get_editor(ctx: click.Context, param: click.Option, value: str) -> str | None:
207+
"""Get the appropriate editor"""
208+
match value.casefold():
209+
case "default":
210+
return config.editor
211+
case "none":
212+
return None
213+
case _:
214+
return value

tests/test_cli.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
create_collection_entry,
88
display_filtered_templates,
99
get_available_themes,
10+
get_editor,
1011
get_site_content_paths,
1112
split_args,
1213
split_module_site,
@@ -232,3 +233,12 @@ def test_display_filtered_templates():
232233
# Check that the table was created with filtered results
233234
call_args = mock_rprint.call_args[0][0]
234235
assert call_args.title == "Test Templates"
236+
237+
238+
@pytest.mark.parametrize(
239+
"selection, expected", [("none", None), ("DEFAULT", "vim"), ("default", "vim"), ("nano", "nano")]
240+
)
241+
def test_get_editor(selection, expected, monkeypatch):
242+
"""Test the get_editor callback"""
243+
monkeypatch.setattr("render_engine_cli.utils.getenv", lambda *_: "vim")
244+
assert get_editor(None, None, value=selection) == expected

tests/test_cli_commands.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ def test_new_entry_command_success(runner, test_site_module, monkeypatch):
127127
"""Tests new_entry command with valid parameters"""
128128
tmp_path, module_site = test_site_module
129129
monkeypatch.chdir(tmp_path)
130-
monkeypatch.setattr("render_engine_cli.cli.os.getenv", lambda *_: {})
131130

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

177175
content_dir = tmp_path / "content"
178176
content_dir.mkdir()
@@ -268,7 +266,6 @@ def mock_create_collection_entry(**kwargs):
268266

269267
tmp_path, module_site = test_site_module
270268
monkeypatch.chdir(tmp_path)
271-
monkeypatch.setattr("render_engine_cli.cli.os.getenv", lambda *_: {})
272269
monkeypatch.setattr("render_engine_cli.cli.create_collection_entry", mock_create_collection_entry)
273270
content_dir = tmp_path / "content"
274271
content_dir.mkdir()

0 commit comments

Comments
 (0)