Skip to content

Commit 8d4d22c

Browse files
committed
fixup! feat: add docstring prompt
1 parent 375ca0d commit 8d4d22c

File tree

11 files changed

+112
-71
lines changed

11 files changed

+112
-71
lines changed

src/git_draft/__main__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from .common import (
1010
Config,
1111
PROGRAM,
12-
PromptRenderer,
1312
Store,
1413
UnreachableError,
1514
ensure_state_home,
1615
open_editor,
1716
)
1817
from .manager import Manager
18+
from .prompt import TemplatedPrompt
1919

2020

2121
def new_parser() -> optparse.OptionParser:
@@ -134,9 +134,7 @@ def main() -> None:
134134
prompt = opts.prompt
135135
if not prompt:
136136
if opts.template:
137-
renderer = PromptRenderer.default()
138-
kwargs = dict(e.split("=", 1) for e in args)
139-
prompt = renderer.render(opts.template, **kwargs)
137+
prompt = TemplatedPrompt.parse(opts.template, *args)
140138
elif sys.stdin.isatty():
141139
prompt = open_editor("Enter your prompt here...")
142140
else:

src/git_draft/common.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import dataclasses
55
from datetime import datetime
66
import functools
7-
import jinja2
87
import logging
98
import os
109
from pathlib import Path
@@ -27,7 +26,7 @@
2726
type JSONObject = Mapping[str, JSONValue]
2827

2928

30-
_package_root = Path(__file__).parent
29+
package_root = Path(__file__).parent
3130

3231

3332
def ensure_state_home() -> Path:
@@ -72,31 +71,6 @@ class BotConfig:
7271
pythonpath: str | None = None
7372

7473

75-
_prompt_root = _package_root / "prompts"
76-
77-
78-
class PromptRenderer:
79-
def __init__(self, env: jinja2.Environment) -> None:
80-
self._environment = env
81-
82-
@classmethod
83-
def default(cls):
84-
env = jinja2.Environment(
85-
auto_reload=False,
86-
autoescape=False,
87-
keep_trailing_newline=True,
88-
loader=jinja2.FileSystemLoader(
89-
[Config.folder_path() / "prompts", str(_prompt_root)]
90-
),
91-
undefined=jinja2.StrictUndefined,
92-
)
93-
return cls(env)
94-
95-
def render(self, template_name: str, **kwargs) -> str:
96-
template = self._environment.get_template(f"{template_name}.jinja")
97-
return template.render(kwargs)
98-
99-
10074
_default_editors = ["vim", "emacs", "nano"]
10175

10276

@@ -169,7 +143,7 @@ def cursor(self) -> Iterator[sqlite3.Cursor]:
169143
self._connection.commit()
170144

171145

172-
_query_root = _package_root / "queries"
146+
_query_root = package_root / "queries"
173147

174148

175149
@functools.cache

src/git_draft/manager.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .bots import Bot, OperationHook, Toolbox
1515
from .common import Store, random_id, sql
16+
from .prompt import PromptRenderer, TemplatedPrompt
1617

1718

1819
_logger = logging.getLogger(__name__)
@@ -135,13 +136,13 @@ def _create_branch(self, sync: bool) -> _Branch:
135136

136137
def generate_draft(
137138
self,
138-
prompt: str,
139+
prompt: str | TemplatedPrompt,
139140
bot: Bot,
140141
checkout=False,
141142
reset=False,
142143
sync=False,
143144
) -> None:
144-
if not prompt.strip():
145+
if isinstance(prompt, str) and not prompt.strip():
145146
raise ValueError("Empty prompt")
146147
if self._repo.is_dirty(working_tree=False):
147148
if not reset:
@@ -157,24 +158,31 @@ def generate_draft(
157158
branch = self._create_branch(sync)
158159
_logger.debug("Created branch %s.", branch)
159160

161+
if isinstance(prompt, TemplatedPrompt):
162+
renderer = PromptRenderer.for_repo(self._repo)
163+
prompt_contents = renderer.render(prompt)
164+
else:
165+
prompt_contents = prompt
160166
with self._store.cursor() as cursor:
161167
[(prompt_id,)] = cursor.execute(
162168
sql("add-prompt"),
163169
{
164170
"branch_suffix": branch.suffix,
165-
"contents": prompt,
171+
"contents": prompt_contents,
166172
},
167173
)
168174

169175
start_time = time.perf_counter()
170176
toolbox = _Toolbox(self._repo, self._operation_hook)
171-
action = bot.act(prompt, toolbox)
177+
action = bot.act(prompt_contents, toolbox)
172178
end_time = time.perf_counter()
173179

174180
title = action.title
175181
if not title:
176-
title = textwrap.shorten(prompt, break_on_hyphens=False, width=72)
177-
commit = self._repo.index.commit(f"draft! {title}\n\n{prompt}")
182+
title = _default_title(prompt_contents)
183+
commit = self._repo.index.commit(
184+
f"draft! {title}\n\n{prompt_contents}"
185+
)
178186

179187
with self._store.cursor() as cursor:
180188
cursor.execute(
@@ -247,3 +255,7 @@ def _exit_draft(self, apply: bool, delete=False) -> None:
247255
self._repo.git.checkout(sync_sha, "--", ".")
248256
if delete:
249257
self._repo.git.branch("-D", branch.name)
258+
259+
260+
def _default_title(prompt: str) -> str:
261+
return textwrap.shorten(prompt, break_on_hyphens=False, width=72)

src/git_draft/prompt.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import dataclasses
2+
import git
3+
import jinja2
4+
from typing import Mapping, Self
5+
6+
from .common import Config, package_root
7+
8+
9+
_prompt_root = package_root / "prompts"
10+
11+
12+
@dataclasses.dataclass(frozen=True)
13+
class TemplatedPrompt:
14+
template: str
15+
context: Mapping[str, str]
16+
17+
@classmethod
18+
def parse(cls, name: str, *args: str) -> Self:
19+
return cls(name, dict(e.split("=", 1) for e in args))
20+
21+
22+
class PromptRenderer:
23+
def __init__(self, env: jinja2.Environment) -> None:
24+
self._environment = env
25+
26+
@classmethod
27+
def for_repo(cls, repo: git.Repo):
28+
env = jinja2.Environment(
29+
auto_reload=False,
30+
autoescape=False,
31+
keep_trailing_newline=True,
32+
loader=jinja2.FileSystemLoader(
33+
[Config.folder_path() / "prompts", str(_prompt_root)]
34+
),
35+
undefined=jinja2.StrictUndefined,
36+
)
37+
env.globals["repo"] = {
38+
"file_paths": repo.git.ls_files(),
39+
}
40+
return cls(env)
41+
42+
def render(self, prompt: TemplatedPrompt) -> str:
43+
template = self._environment.get_template(f"{prompt.template}.jinja")
44+
return template.render(prompt.context)
Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
Add docstrings to all public functions and classes in {{ src_path }}. Be
2-
concise and do not repeat yourself.
1+
{% if src_path is defined %}
2+
Add docstrings to all public functions and classes in {{ src_path }}.
3+
{% else %}
4+
Add docstrings to all public functions and classes in this repository.
5+
{% endif %}
6+
7+
Be concise and do not repeat yourself.
38

49
Focus on highlighting aspects which are not obvious from the name of the
510
symbols. Take time to look at the implementation to discover any behaviors
@@ -11,16 +16,18 @@ examples:
1116

1217
```python
1318
def write_file(path: Path, contents: str) -> None:
14-
"""Update a file's contents
19+
"""Update a file's contents
1520

16-
Args:
17-
path: Path to the file to update.
18-
contents: New file contents.
19-
"""
21+
Args:
22+
path: Path to the file to update.
23+
contents: New file contents.
24+
"""
2025
...
2126

2227
class Renderer:
2328
"""A smart renderer"""
2429

2530
...
2631
```
32+
33+
{% include "includes/file-list.jinja" %}

src/git_draft/prompts/add-test.jinja

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,4 @@ tests. For example, if the surrounding code uses fixtures, do so as well.
88
Do not stop until you have added at least one test. You should add separate
99
tests to cover the normal execution path, and to cover any exceptional cases.
1010

11-
For reference, here is the list of all currently available files in the
12-
repository:
13-
14-
{% for path in repo.file_paths %}
15-
* {{ path }}
16-
{% endfor %}
11+
{% include "includes/file-list.jinja" %}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
For reference, here is the list of all currently available files in the
2+
repository:
3+
4+
{% for path in repo.file_paths %}
5+
* {{ path }}
6+
{% endfor %}

tests/git_draft/common_test.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,3 @@ def test_load_ok(self) -> None:
8585
def test_load_default(self) -> None:
8686
config = sut.Config.load()
8787
assert config.log_level == logging.INFO
88-
89-
90-
class TestPromptRenderer:
91-
@pytest.fixture(autouse=True)
92-
def setup(self) -> None:
93-
self._renderer = sut.PromptRenderer.default()
94-
95-
def test_ok(self) -> None:
96-
prompt = self._renderer.render("add-test", symbol="foo")
97-
assert "foo" in prompt

tests/git_draft/conftest.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pathlib import Path
2+
from typing import Iterator
3+
import git
4+
import pytest
5+
6+
7+
@pytest.fixture
8+
def repo(tmp_path: Path) -> Iterator[git.Repo]:
9+
repo = git.Repo.init(str(tmp_path / "repo"), initial_branch="main")
10+
repo.index.commit("init")
11+
yield repo

tests/git_draft/manager_test.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,12 @@
22
import os.path as osp
33
from pathlib import PurePosixPath
44
import pytest
5-
import tempfile
6-
from typing import Iterator
75

86
from git_draft.bots import Action, Bot, Toolbox
97
from git_draft.common import Store
108
import git_draft.manager as sut
119

1210

13-
@pytest.fixture
14-
def repo() -> Iterator[git.Repo]:
15-
with tempfile.TemporaryDirectory() as name:
16-
repo = git.Repo.init(name, initial_branch="main")
17-
repo.index.commit("init")
18-
yield repo
19-
20-
2111
class _FakeBot(Bot):
2212
def act(self, prompt: str, toolbox: Toolbox) -> Action:
2313
toolbox.write_file(PurePosixPath("PROMPT"), prompt)

0 commit comments

Comments
 (0)