From b79f67c2af92009c30fb42165e0fd22479829994 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Sun, 21 Sep 2025 14:49:11 -0700 Subject: [PATCH 1/2] feat: support line output formatting --- src/git_draft/__main__.py | 76 +++++++++++++++++++++++---------------- src/git_draft/drafter.py | 17 +++++++-- src/git_draft/prompt.py | 17 +++++++-- 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/git_draft/__main__.py b/src/git_draft/__main__.py index f6bb495..6a3ab33 100644 --- a/src/git_draft/__main__.py +++ b/src/git_draft/__main__.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio +import dataclasses import enum import importlib.metadata import logging import optparse from pathlib import Path import sys +from typing import Any from .bots import load_bot from .common import PROGRAM, Config, UnreachableError, ensure_state_home @@ -28,7 +30,27 @@ _logger = logging.getLogger(__name__) -def new_parser() -> optparse.OptionParser: +class Accept(enum.Enum): + """Valid change accept mode""" + + MANUAL = 0 + MERGE = enum.auto() + MERGE_THEIRS = enum.auto() + MERGE_THEN_QUIT = enum.auto() + + def merge_strategy(self) -> DraftMergeStrategy | None: + match self: + case Accept.MANUAL: + return None + case Accept.MERGE: + return "ignore-all-space" + case Accept.MERGE_THEIRS | Accept.MERGE_THEN_QUIT: + return "theirs" + case _: + raise UnreachableError() + + +def _new_parser() -> optparse.OptionParser: parser = optparse.OptionParser( prog=PROGRAM, version=importlib.metadata.version("git_draft"), @@ -94,7 +116,6 @@ def callback( help="edit prompt or template", action="store_true", ) - parser.add_option( "--no-accept", help="do not merge draft", @@ -102,31 +123,17 @@ def callback( action="store_const", const=0, ) + parser.add_option( + "-f", + "--format", + dest="format", + help="formatting string", + ) return parser -class Accept(enum.Enum): - """Valid change accept mode""" - - MANUAL = 0 - MERGE = enum.auto() - MERGE_THEIRS = enum.auto() - MERGE_THEN_QUIT = enum.auto() - - def merge_strategy(self) -> DraftMergeStrategy | None: - match self: - case Accept.MANUAL: - return None - case Accept.MERGE: - return "ignore-all-space" - case Accept.MERGE_THEIRS | Accept.MERGE_THEN_QUIT: - return "theirs" - case _: - raise UnreachableError() - - -def edit(*, path: Path | None = None, text: str | None = None) -> str: +def _edit(*, path: Path | None = None, text: str | None = None) -> str: if sys.stdin.isatty(): return open_editor(text or "", path) # We exit with a custom code to allow the caller to act accordingly. @@ -145,12 +152,17 @@ def edit(*, path: Path | None = None, text: str | None = None) -> str: sys.exit(199) +def _format(props: Any, spec: str) -> str: + """Formats an instance of a dataclass using the provided pattern""" + return spec.format(**dataclasses.asdict(props)) + + _PROMPT_PLACEHOLDER = "Enter your prompt here..." async def run() -> None: # noqa: PLR0912 PLR0915 config = Config.load() - (opts, args) = new_parser().parse_args() + (opts, args) = _new_parser().parse_args() log_path = ensure_state_home() / "log" if opts.log_path: @@ -191,7 +203,7 @@ async def run() -> None: # noqa: PLR0912 PLR0915 prompt = TemplatedPrompt.public(args[0], args[1:]) editable = opts.edit else: - prompt = edit( + prompt = _edit( text=drafter.latest_draft_prompt() or _PROMPT_PLACEHOLDER ).strip() if prompt.strip() == _PROMPT_PLACEHOLDER: @@ -212,8 +224,9 @@ async def run() -> None: # noqa: PLR0912 PLR0915 drafter.quit_folio() case "list-events": draft_id = args[0] if args else None - for line in drafter.list_draft_events(draft_id): - print(line) + spec = opts.format or "{occurred_at}\t{description}" + for props in drafter.list_draft_events(draft_id): + print(_format(props, spec)) case "show-template": if len(args) != 1: raise ValueError("Expected exactly one argument") @@ -221,16 +234,17 @@ async def run() -> None: # noqa: PLR0912 PLR0915 meta = find_prompt_metadata(name) if opts.edit: if meta: - edit(path=meta.local_path(), text=meta.source()) + _edit(path=meta.local_path(), text=meta.source()) else: - edit(path=PromptMetadata.local_path_for(name)) + _edit(path=PromptMetadata.local_path_for(name)) else: if not meta: raise ValueError(f"No template named {name!r}") print(meta.source()) case "list-templates": - for line in list_templates(): - print(line) + spec = opts.format or "{name}: {description}" + for props in list_templates(): + print(_format(props, spec)) case _: raise UnreachableError() diff --git a/src/git_draft/drafter.py b/src/git_draft/drafter.py index 42256ef..1c0bfb2 100644 --- a/src/git_draft/drafter.py +++ b/src/git_draft/drafter.py @@ -452,7 +452,9 @@ def latest_draft_prompt(self) -> str | None: prompt = "\n\n".join([prompt, reindent(question, prefix="> ")]) return prompt - def list_draft_events(self, draft_ref: str | None = None) -> Iterator[str]: + def list_draft_events( + self, draft_ref: str | None = None + ) -> Iterator[DraftEventProperties]: if draft_ref: folio_id, seqno = _parse_draft_ref(draft_ref) else: @@ -469,7 +471,18 @@ def list_draft_events(self, draft_ref: str | None = None) -> Iterator[str]: occurred_at, class_name, data = row event = decoders[class_name].decode(data) description = _format_event(event) - yield "\t".join([occurred_at, class_name, description]) + yield DraftEventProperties( + occurred_at, class_name, description + ) + + +@dataclasses.dataclass(frozen=True) +class DraftEventProperties: + """Formattable properties corresponding to a draft's event""" + + occurred_at: str + class_name: str + description: str @dataclasses.dataclass(frozen=True) diff --git a/src/git_draft/prompt.py b/src/git_draft/prompt.py index 068591b..b425f28 100644 --- a/src/git_draft/prompt.py +++ b/src/git_draft/prompt.py @@ -210,7 +210,9 @@ def find_prompt_metadata(name: PromptName) -> PromptMetadata | None: return prompt.metadata -def list_templates(*, include_local: bool = True) -> Iterator[str]: +def list_templates( + *, include_local: bool = True +) -> Iterator[TemplateProperties]: env = _jinja_environment(include_local=include_local) worktree = EmptyWorktree() for rel_path in env.list_templates(extensions=[_extension]): @@ -219,5 +221,14 @@ def list_templates(*, include_local: bool = True) -> Iterator[str]: name, _ext = os.path.splitext(rel_path) prompt = _load_prompt(env, name, worktree) metadata = prompt.metadata - local = "y" if metadata.is_local() else "n" - yield "\t".join([name, local, metadata.description or ""]) + scope = "local" if metadata.is_local() else "global" + yield TemplateProperties(name, scope, metadata.description or "") + + +@dataclasses.dataclass(frozen=True) +class TemplateProperties: + """Prompt template formattable properties""" + + name: str + scope: str + description: str From ddf1432644430bcb6f9ab62635a02b99ff7baf88 Mon Sep 17 00:00:00 2001 From: Matthieu Monsch Date: Mon, 22 Sep 2025 08:58:56 -0700 Subject: [PATCH 2/2] fixup! b79f67c --- src/git_draft/__main__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/git_draft/__main__.py b/src/git_draft/__main__.py index 6a3ab33..b442017 100644 --- a/src/git_draft/__main__.py +++ b/src/git_draft/__main__.py @@ -225,8 +225,8 @@ async def run() -> None: # noqa: PLR0912 PLR0915 case "list-events": draft_id = args[0] if args else None spec = opts.format or "{occurred_at}\t{description}" - for props in drafter.list_draft_events(draft_id): - print(_format(props, spec)) + for event_props in drafter.list_draft_events(draft_id): + print(_format(event_props, spec)) case "show-template": if len(args) != 1: raise ValueError("Expected exactly one argument") @@ -243,8 +243,8 @@ async def run() -> None: # noqa: PLR0912 PLR0915 print(meta.source()) case "list-templates": spec = opts.format or "{name}: {description}" - for props in list_templates(): - print(_format(props, spec)) + for template_props in list_templates(): + print(_format(template_props, spec)) case _: raise UnreachableError()