textprompts 0.0.1__tar.gz → 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: textprompts
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Minimal text-based prompt-loader with TOML front-matter
5
5
  Keywords: prompts,toml,frontmatter,template
6
6
  Author: Jan Siml
@@ -76,7 +76,7 @@ import textprompts
76
76
  prompt = textprompts.load_prompt("greeting.txt")
77
77
 
78
78
  # Use it safely - all placeholders must be provided
79
- message = prompt.body.format(
79
+ message = prompt.prompt.format(
80
80
  customer_name="Alice",
81
81
  company_name="ACME Corp",
82
82
  issue_type="billing question",
@@ -86,19 +86,22 @@ message = prompt.body.format(
86
86
  print(message)
87
87
 
88
88
  # Or use partial formatting when needed
89
- partial = prompt.body.format(
89
+ partial = prompt.prompt.format(
90
90
  customer_name="Alice",
91
91
  company_name="ACME Corp",
92
92
  skip_validation=True
93
93
  )
94
94
  # Result: "Hello Alice!\n\nWelcome to ACME Corp. We're here to help you with {issue_type}.\n\nBest regards,\n{agent_name}"
95
+
96
+ # Prompt objects expose `.meta` and `.prompt`.
97
+ # Use `prompt.prompt.format()` for safe formatting or `str(prompt)` for raw text.
95
98
  ```
96
99
 
97
100
  **Even simpler** - no metadata required:
98
101
  ```python
99
102
  # simple_prompt.txt contains just: "Analyze this data: {data}"
100
103
  prompt = textprompts.load_prompt("simple_prompt.txt") # Just works!
101
- result = prompt.body.format(data="sales figures")
104
+ result = prompt.prompt.format(data="sales figures")
102
105
  ```
103
106
 
104
107
  ## Core Features
@@ -108,9 +111,9 @@ result = prompt.body.format(data="sales figures")
108
111
  Never ship a prompt with missing variables again:
109
112
 
110
113
  ```python
111
- from textprompts import SafeString
114
+ from textprompts import PromptString
112
115
 
113
- template = SafeString("Hello {name}, your order {order_id} is {status}")
116
+ template = PromptString("Hello {name}, your order {order_id} is {status}")
114
117
 
115
118
  # ✅ Strict formatting - all placeholders must be provided
116
119
  result = template.format(name="Alice", order_id="12345", status="shipped")
@@ -190,14 +193,14 @@ response = openai.chat.completions.create(
190
193
  messages=[
191
194
  {
192
195
  "role": "system",
193
- "content": system_prompt.body.format(
196
+ "content": system_prompt.prompt.format(
194
197
  company_name="ACME Corp",
195
198
  support_level="premium"
196
199
  )
197
200
  },
198
201
  {
199
202
  "role": "user",
200
- "content": user_prompt.body.format(
203
+ "content": user_prompt.prompt.format(
201
204
  query="How do I return an item?",
202
205
  customer_tier="premium"
203
206
  )
@@ -208,7 +211,7 @@ response = openai.chat.completions.create(
208
211
 
209
212
  ### Function Calling (Tool Definitions)
210
213
 
211
- Yes, you can version control your function schemas too:
214
+ Yes, you can version control your whole tool schemas too:
212
215
 
213
216
  ```python
214
217
  # tools/search_products.txt
@@ -253,7 +256,7 @@ from textprompts import load_prompt
253
256
 
254
257
  # Load and parse the tool definition
255
258
  tool_prompt = load_prompt("tools/search_products.txt")
256
- tool_schema = json.loads(tool_prompt.body)
259
+ tool_schema = json.loads(tool_prompt.prompt)
257
260
 
258
261
  # Use with OpenAI
259
262
  response = openai.chat.completions.create(
@@ -338,7 +341,7 @@ Load a single prompt file.
338
341
 
339
342
  Returns a `Prompt` object with:
340
343
  - `prompt.meta`: Metadata from TOML front-matter (always present)
341
- - `prompt.body`: The prompt content as a `SafeString`
344
+ - `prompt.prompt`: The prompt content as a `PromptString`
342
345
  - `prompt.path`: Path to the original file
343
346
 
344
347
  ### `load_prompts(*paths, recursive=False, glob="*.txt", meta=None, max_files=1000)`
@@ -385,14 +388,14 @@ save_prompt("my_prompt.txt", "You are a helpful assistant.")
385
388
  save_prompt("my_prompt.txt", prompt_object)
386
389
  ```
387
390
 
388
- ### `SafeString`
391
+ ### `PromptString`
389
392
 
390
393
  A string subclass that validates `format()` calls:
391
394
 
392
395
  ```python
393
- from textprompts import SafeString
396
+ from textprompts import PromptString
394
397
 
395
- template = SafeString("Hello {name}, you are {role}")
398
+ template = PromptString("Hello {name}, you are {role}")
396
399
 
397
400
  # Strict formatting (default) - all placeholders required
398
401
  result = template.format(name="Alice", role="admin") # ✅ Works
@@ -460,8 +463,8 @@ textprompts validate prompts/
460
463
  4. **Test your prompts**: Write unit tests for critical prompts
461
464
  ```python
462
465
  def test_greeting_prompt():
463
- prompt = load_prompt("greeting.txt")
464
- result = prompt.body.format(customer_name="Test")
466
+ prompt = load_prompt("greeting.txt")
467
+ result = prompt.prompt.format(customer_name="Test")
465
468
  assert "Test" in result
466
469
  ```
467
470
 
@@ -52,7 +52,7 @@ import textprompts
52
52
  prompt = textprompts.load_prompt("greeting.txt")
53
53
 
54
54
  # Use it safely - all placeholders must be provided
55
- message = prompt.body.format(
55
+ message = prompt.prompt.format(
56
56
  customer_name="Alice",
57
57
  company_name="ACME Corp",
58
58
  issue_type="billing question",
@@ -62,19 +62,22 @@ message = prompt.body.format(
62
62
  print(message)
63
63
 
64
64
  # Or use partial formatting when needed
65
- partial = prompt.body.format(
65
+ partial = prompt.prompt.format(
66
66
  customer_name="Alice",
67
67
  company_name="ACME Corp",
68
68
  skip_validation=True
69
69
  )
70
70
  # Result: "Hello Alice!\n\nWelcome to ACME Corp. We're here to help you with {issue_type}.\n\nBest regards,\n{agent_name}"
71
+
72
+ # Prompt objects expose `.meta` and `.prompt`.
73
+ # Use `prompt.prompt.format()` for safe formatting or `str(prompt)` for raw text.
71
74
  ```
72
75
 
73
76
  **Even simpler** - no metadata required:
74
77
  ```python
75
78
  # simple_prompt.txt contains just: "Analyze this data: {data}"
76
79
  prompt = textprompts.load_prompt("simple_prompt.txt") # Just works!
77
- result = prompt.body.format(data="sales figures")
80
+ result = prompt.prompt.format(data="sales figures")
78
81
  ```
79
82
 
80
83
  ## Core Features
@@ -84,9 +87,9 @@ result = prompt.body.format(data="sales figures")
84
87
  Never ship a prompt with missing variables again:
85
88
 
86
89
  ```python
87
- from textprompts import SafeString
90
+ from textprompts import PromptString
88
91
 
89
- template = SafeString("Hello {name}, your order {order_id} is {status}")
92
+ template = PromptString("Hello {name}, your order {order_id} is {status}")
90
93
 
91
94
  # ✅ Strict formatting - all placeholders must be provided
92
95
  result = template.format(name="Alice", order_id="12345", status="shipped")
@@ -166,14 +169,14 @@ response = openai.chat.completions.create(
166
169
  messages=[
167
170
  {
168
171
  "role": "system",
169
- "content": system_prompt.body.format(
172
+ "content": system_prompt.prompt.format(
170
173
  company_name="ACME Corp",
171
174
  support_level="premium"
172
175
  )
173
176
  },
174
177
  {
175
178
  "role": "user",
176
- "content": user_prompt.body.format(
179
+ "content": user_prompt.prompt.format(
177
180
  query="How do I return an item?",
178
181
  customer_tier="premium"
179
182
  )
@@ -184,7 +187,7 @@ response = openai.chat.completions.create(
184
187
 
185
188
  ### Function Calling (Tool Definitions)
186
189
 
187
- Yes, you can version control your function schemas too:
190
+ Yes, you can version control your whole tool schemas too:
188
191
 
189
192
  ```python
190
193
  # tools/search_products.txt
@@ -229,7 +232,7 @@ from textprompts import load_prompt
229
232
 
230
233
  # Load and parse the tool definition
231
234
  tool_prompt = load_prompt("tools/search_products.txt")
232
- tool_schema = json.loads(tool_prompt.body)
235
+ tool_schema = json.loads(tool_prompt.prompt)
233
236
 
234
237
  # Use with OpenAI
235
238
  response = openai.chat.completions.create(
@@ -314,7 +317,7 @@ Load a single prompt file.
314
317
 
315
318
  Returns a `Prompt` object with:
316
319
  - `prompt.meta`: Metadata from TOML front-matter (always present)
317
- - `prompt.body`: The prompt content as a `SafeString`
320
+ - `prompt.prompt`: The prompt content as a `PromptString`
318
321
  - `prompt.path`: Path to the original file
319
322
 
320
323
  ### `load_prompts(*paths, recursive=False, glob="*.txt", meta=None, max_files=1000)`
@@ -361,14 +364,14 @@ save_prompt("my_prompt.txt", "You are a helpful assistant.")
361
364
  save_prompt("my_prompt.txt", prompt_object)
362
365
  ```
363
366
 
364
- ### `SafeString`
367
+ ### `PromptString`
365
368
 
366
369
  A string subclass that validates `format()` calls:
367
370
 
368
371
  ```python
369
- from textprompts import SafeString
372
+ from textprompts import PromptString
370
373
 
371
- template = SafeString("Hello {name}, you are {role}")
374
+ template = PromptString("Hello {name}, you are {role}")
372
375
 
373
376
  # Strict formatting (default) - all placeholders required
374
377
  result = template.format(name="Alice", role="admin") # ✅ Works
@@ -436,8 +439,8 @@ textprompts validate prompts/
436
439
  4. **Test your prompts**: Write unit tests for critical prompts
437
440
  ```python
438
441
  def test_greeting_prompt():
439
- prompt = load_prompt("greeting.txt")
440
- result = prompt.body.format(customer_name="Test")
442
+ prompt = load_prompt("greeting.txt")
443
+ result = prompt.prompt.format(customer_name="Test")
441
444
  assert "Test" in result
442
445
  ```
443
446
 
@@ -1,9 +1,10 @@
1
1
  [project]
2
2
  name = "textprompts"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Minimal text-based prompt-loader with TOML front-matter"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
+ license-file = "LICENSE"
7
8
  authors = [
8
9
  {name = "Jan Siml", email = "49557684+svilupp@users.noreply.github.com"},
9
10
  ]
@@ -1,4 +1,10 @@
1
- from .config import MetadataMode, get_metadata, set_metadata
1
+ from .config import (
2
+ MetadataMode,
3
+ get_metadata,
4
+ set_metadata,
5
+ skip_metadata,
6
+ warn_on_ignored_metadata,
7
+ )
2
8
  from .errors import (
3
9
  FileMissingError,
4
10
  InvalidMetadataError,
@@ -8,7 +14,7 @@ from .errors import (
8
14
  )
9
15
  from .loaders import load_prompt, load_prompts
10
16
  from .models import Prompt, PromptMeta
11
- from .safe_string import SafeString
17
+ from .prompt_string import PromptString, SafeString
12
18
  from .savers import save_prompt
13
19
 
14
20
  __all__ = [
@@ -17,10 +23,13 @@ __all__ = [
17
23
  "save_prompt",
18
24
  "Prompt",
19
25
  "PromptMeta",
26
+ "PromptString",
20
27
  "SafeString",
21
28
  "MetadataMode",
22
29
  "set_metadata",
23
30
  "get_metadata",
31
+ "skip_metadata",
32
+ "warn_on_ignored_metadata",
24
33
  "TextPromptsError",
25
34
  "FileMissingError",
26
35
  "MissingMetadataError",
@@ -0,0 +1,8 @@
1
+ """Command-line entry point for ``python -m textprompts``.""" # pragma: no cover
2
+
3
+ from textprompts.cli import main # pragma: no cover
4
+
5
+ __all__ = ["main"] # pragma: no cover
6
+
7
+ if __name__ == "__main__": # pragma: no cover - small entrypoint wrapper
8
+ main() # pragma: no cover
@@ -4,13 +4,13 @@ from typing import Optional
4
4
 
5
5
  try:
6
6
  import tomllib
7
- except ImportError:
7
+ except ImportError: # pragma: no cover - Python <3.11 fallback
8
8
  import tomli as tomllib # type: ignore[import-not-found, no-redef]
9
9
 
10
- from .config import MetadataMode
10
+ from .config import MetadataMode, warn_on_ignored_metadata
11
11
  from .errors import InvalidMetadataError, MalformedHeaderError, MissingMetadataError
12
12
  from .models import Prompt, PromptMeta
13
- from .safe_string import SafeString
13
+ from .prompt_string import PromptString
14
14
 
15
15
  DELIM = "---"
16
16
 
@@ -59,8 +59,21 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
59
59
 
60
60
  # Handle IGNORE mode - treat entire file as body
61
61
  if metadata_mode == MetadataMode.IGNORE:
62
+ if (
63
+ warn_on_ignored_metadata()
64
+ and raw.startswith(DELIM)
65
+ and raw.find(DELIM, len(DELIM)) != -1
66
+ ):
67
+ import warnings
68
+
69
+ warnings.warn(
70
+ "Metadata detected but ignored; use set_metadata('allow') or skip_metadata(skip_warning=True) to silence",
71
+ stacklevel=2,
72
+ )
62
73
  ignore_meta = PromptMeta(title=path.stem)
63
- return Prompt(path=path, meta=ignore_meta, body=SafeString(textwrap.dedent(raw)))
74
+ return Prompt(
75
+ path=path, meta=ignore_meta, prompt=PromptString(textwrap.dedent(raw))
76
+ )
64
77
 
65
78
  # For STRICT and ALLOW modes, try to parse front matter
66
79
  try:
@@ -72,7 +85,7 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
72
85
  f"{e}. If this file has no metadata and starts with '---', "
73
86
  f"use meta=MetadataMode.IGNORE to skip metadata parsing."
74
87
  ) from e
75
- raise
88
+ raise # pragma: no cover - reraised to preserve stack
76
89
 
77
90
  meta: Optional[PromptMeta] = None
78
91
  if header_txt is not None:
@@ -114,7 +127,7 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
114
127
  ) from e
115
128
  except InvalidMetadataError:
116
129
  raise
117
- except Exception as e:
130
+ except Exception as e: # pragma: no cover - unlikely generic error
118
131
  raise InvalidMetadataError(f"Invalid metadata: {e}") from e
119
132
 
120
133
  else:
@@ -129,11 +142,11 @@ def parse_file(path: Path, *, metadata_mode: MetadataMode) -> Prompt:
129
142
  meta = PromptMeta()
130
143
 
131
144
  # Always ensure we have metadata with a title
132
- if not meta:
145
+ if not meta: # pragma: no cover - meta is never falsy but kept for safety
133
146
  meta = PromptMeta()
134
147
 
135
148
  # Use filename as title if not provided
136
149
  if meta.title is None:
137
150
  meta.title = path.stem
138
151
 
139
- return Prompt(path=path, meta=meta, body=SafeString(textwrap.dedent(body)))
152
+ return Prompt(path=path, meta=meta, prompt=PromptString(textwrap.dedent(body)))
@@ -28,4 +28,4 @@ def main() -> None:
28
28
 
29
29
 
30
30
  if __name__ == "__main__":
31
- main()
31
+ main() # pragma: no cover - simple CLI entry point
@@ -30,6 +30,7 @@ class MetadataMode(Enum):
30
30
 
31
31
  # Global configuration variable
32
32
  _METADATA_MODE: MetadataMode = MetadataMode.IGNORE
33
+ _WARN_ON_IGNORED_META: bool = True
33
34
 
34
35
 
35
36
  def set_metadata(mode: Union[MetadataMode, str]) -> None:
@@ -82,6 +83,20 @@ def get_metadata() -> MetadataMode:
82
83
  return _METADATA_MODE
83
84
 
84
85
 
86
+ def skip_metadata(*, skip_warning: bool = False) -> None:
87
+ """Convenience setter for ignoring metadata with optional warnings."""
88
+
89
+ global _WARN_ON_IGNORED_META
90
+ _WARN_ON_IGNORED_META = not skip_warning # pragma: no cover
91
+ set_metadata(MetadataMode.IGNORE) # pragma: no cover - simple wrapper
92
+
93
+
94
+ def warn_on_ignored_metadata() -> bool:
95
+ """Return whether warnings for ignored metadata are enabled."""
96
+
97
+ return _WARN_ON_IGNORED_META
98
+
99
+
85
100
  def _resolve_metadata_mode(meta: Union[MetadataMode, str, None]) -> MetadataMode:
86
101
  """
87
102
  Resolve the metadata mode from parameters and global config.
@@ -96,17 +96,25 @@ def load_prompts(
96
96
  if pth.is_dir():
97
97
  itr: Iterable[Path] = pth.rglob(glob) if recursive else pth.glob(glob)
98
98
  for f in itr:
99
- if max_files and file_count >= max_files:
99
+ if (
100
+ max_files and file_count >= max_files
101
+ ): # pragma: no cover - boundary check
100
102
  from .errors import TextPromptsError
101
103
 
102
- raise TextPromptsError(f"Exceeded max_files limit of {max_files}")
104
+ raise TextPromptsError(
105
+ f"Exceeded max_files limit of {max_files}"
106
+ ) # pragma: no cover - boundary check
103
107
  collected.append(load_prompt(f, meta=meta))
104
108
  file_count += 1
105
109
  else:
106
- if max_files and file_count >= max_files:
110
+ if (
111
+ max_files and file_count >= max_files
112
+ ): # pragma: no cover - boundary check
107
113
  from .errors import TextPromptsError
108
114
 
109
- raise TextPromptsError(f"Exceeded max_files limit of {max_files}")
115
+ raise TextPromptsError(
116
+ f"Exceeded max_files limit of {max_files}"
117
+ ) # pragma: no cover - boundary check
110
118
  collected.append(load_prompt(pth, meta=meta))
111
119
  file_count += 1
112
120
 
@@ -0,0 +1,67 @@
1
+ from datetime import date
2
+ from pathlib import Path
3
+ from typing import Any, Union
4
+
5
+ from pydantic import BaseModel, Field, field_validator
6
+
7
+ from .prompt_string import PromptString
8
+
9
+
10
+ class PromptMeta(BaseModel):
11
+ title: Union[str, None] = Field(default=None, description="Human-readable name")
12
+ version: Union[str, None] = Field(default=None)
13
+ author: Union[str, None] = Field(default=None)
14
+ created: Union[date, None] = Field(default=None)
15
+ description: Union[str, None] = Field(default=None)
16
+
17
+
18
+ class Prompt(BaseModel):
19
+ path: Path
20
+ meta: Union[PromptMeta, None]
21
+ prompt: PromptString
22
+
23
+ @field_validator("prompt")
24
+ @classmethod
25
+ def prompt_not_empty(cls, v: str) -> PromptString:
26
+ if not v.strip():
27
+ raise ValueError("Prompt body is empty")
28
+ return PromptString(v)
29
+
30
+ def __repr__(self) -> str:
31
+ if self.meta and self.meta.title:
32
+ if self.meta.version:
33
+ return (
34
+ f"Prompt(title='{self.meta.title}', version='{self.meta.version}')"
35
+ " # use .format() or str()"
36
+ )
37
+ return f"Prompt(title='{self.meta.title}') # use .format() or str()"
38
+ return f"Prompt(path='{self.path}') # use .format() or str()"
39
+
40
+ def __str__(self) -> str:
41
+ return str(self.prompt)
42
+
43
+ @property
44
+ def body(self) -> PromptString:
45
+ import warnings
46
+
47
+ warnings.warn(
48
+ "Prompt.body is deprecated; use .prompt instead",
49
+ DeprecationWarning,
50
+ stacklevel=2,
51
+ )
52
+ return self.prompt
53
+
54
+ def __len__(self) -> int:
55
+ return len(self.prompt)
56
+
57
+ def __getitem__(self, item: int | slice) -> str:
58
+ return self.prompt[item]
59
+
60
+ def __add__(self, other: str) -> str:
61
+ return str(self.prompt) + str(other)
62
+
63
+ def strip(self, *args: Any, **kwargs: Any) -> str:
64
+ return self.prompt.strip(*args, **kwargs)
65
+
66
+ def format(self, *args: Any, **kwargs: Any) -> str:
67
+ return self.prompt.format(*args, **kwargs)
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Utility functions for extracting and validating placeholders in format strings.
3
3
 
4
- This module provides robust placeholder extraction and validation for SafeString
4
+ This module provides robust placeholder extraction and validation for PromptString
5
5
  formatting operations.
6
6
  """
7
7
 
@@ -53,7 +53,10 @@ def extract_placeholders(text: str) -> Set[str]:
53
53
 
54
54
 
55
55
  def validate_format_args(
56
- placeholders: Set[str], args: Tuple[Any, ...], kwargs: Dict[str, Any], skip_validation: bool = False
56
+ placeholders: Set[str],
57
+ args: Tuple[Any, ...],
58
+ kwargs: Dict[str, Any],
59
+ skip_validation: bool = False,
57
60
  ) -> None:
58
61
  """
59
62
  Validate that format arguments match the placeholders in the template.
@@ -0,0 +1,60 @@
1
+ from typing import Any, Set
2
+
3
+ from pydantic import GetCoreSchemaHandler
4
+ from pydantic_core import core_schema
5
+
6
+ from .placeholder_utils import extract_placeholders, validate_format_args
7
+
8
+
9
+ class PromptString(str):
10
+ """String subclass that validates ``format()`` calls."""
11
+
12
+ placeholders: Set[str]
13
+
14
+ def __new__(cls, value: str) -> "PromptString":
15
+ instance = str.__new__(cls, value)
16
+ instance.placeholders = extract_placeholders(value)
17
+ return instance
18
+
19
+ def format(self, *args: Any, **kwargs: Any) -> str:
20
+ """Format with validation and optional partial formatting."""
21
+ skip_validation = kwargs.pop("skip_validation", False)
22
+ source = str(self).strip()
23
+ if skip_validation:
24
+ return self._partial_format(*args, source=source, **kwargs)
25
+ validate_format_args(self.placeholders, args, kwargs, skip_validation=False)
26
+ return str.format(source, *args, **kwargs)
27
+
28
+ def _partial_format(
29
+ self, *args: Any, source: str | None = None, **kwargs: Any
30
+ ) -> str:
31
+ """Partial formatting - replace placeholders that have values."""
32
+ all_kwargs = kwargs.copy()
33
+ for i, arg in enumerate(args):
34
+ all_kwargs[str(i)] = arg
35
+
36
+ result = source if source is not None else str(self)
37
+ for placeholder in self.placeholders:
38
+ if placeholder in all_kwargs:
39
+ pattern = f"{{{placeholder}}}"
40
+ if pattern in result:
41
+ try:
42
+ result = result.replace(pattern, str(all_kwargs[placeholder]))
43
+ except (KeyError, ValueError): # pragma: no cover - defensive
44
+ pass
45
+ return result
46
+
47
+ def __repr__(self) -> str:
48
+ return f"PromptString({str.__repr__(self)}, placeholders={self.placeholders})"
49
+
50
+ @classmethod
51
+ def __get_pydantic_core_schema__(
52
+ cls, source_type: Any, handler: GetCoreSchemaHandler
53
+ ) -> core_schema.CoreSchema:
54
+ return core_schema.no_info_after_validator_function(
55
+ cls, core_schema.str_schema()
56
+ )
57
+
58
+
59
+ # Backwards compatibility alias
60
+ SafeString = PromptString
@@ -0,0 +1,5 @@
1
+ """Backward-compatible alias module.""" # pragma: no cover
2
+
3
+ from .prompt_string import PromptString, SafeString # pragma: no cover
4
+
5
+ __all__ = ["PromptString", "SafeString"] # pragma: no cover
@@ -25,7 +25,7 @@ def save_prompt(path: Union[str, Path], content: Union[str, Prompt]) -> None:
25
25
  >>> prompt = Prompt(
26
26
  ... path=Path("my_prompt.txt"),
27
27
  ... meta=PromptMeta(title="Assistant", version="1.0.0", description="A helpful AI"),
28
- ... body="You are a helpful assistant."
28
+ ... prompt="You are a helpful assistant."
29
29
  ... )
30
30
  >>> save_prompt("my_prompt.txt", prompt)
31
31
  """
@@ -59,7 +59,7 @@ version = ""
59
59
 
60
60
  lines.append("---")
61
61
  lines.append("")
62
- lines.append(str(content.body))
62
+ lines.append(str(content.prompt))
63
63
 
64
64
  path.write_text("\n".join(lines), encoding="utf-8")
65
65
  else:
@@ -1,42 +0,0 @@
1
- from datetime import date
2
- from pathlib import Path
3
- from typing import Union
4
-
5
- from pydantic import BaseModel, Field, field_validator
6
-
7
- from .safe_string import SafeString
8
-
9
-
10
- class PromptMeta(BaseModel):
11
- title: Union[str, None] = Field(default=None, description="Human-readable name")
12
- version: Union[str, None] = Field(default=None)
13
- author: Union[str, None] = Field(default=None)
14
- created: Union[date, None] = Field(default=None)
15
- description: Union[str, None] = Field(default=None)
16
-
17
-
18
- class Prompt(BaseModel):
19
- path: Path
20
- meta: Union[PromptMeta, None]
21
- body: SafeString
22
-
23
- @field_validator("body")
24
- @classmethod
25
- def body_not_empty(cls, v: str) -> SafeString:
26
- if not v.strip():
27
- raise ValueError("Prompt body is empty")
28
- return SafeString(v)
29
-
30
- def __repr__(self) -> str:
31
- if self.meta and self.meta.title:
32
- if self.meta.version:
33
- return (
34
- f"Prompt(title='{self.meta.title}', version='{self.meta.version}')"
35
- )
36
- else:
37
- return f"Prompt(title='{self.meta.title}')"
38
- else:
39
- return f"Prompt(path='{self.path}')"
40
-
41
- def __str__(self) -> str:
42
- return str(self.body)
@@ -1,110 +0,0 @@
1
- from typing import Any, Set
2
-
3
- from pydantic import GetCoreSchemaHandler
4
- from pydantic_core import core_schema
5
-
6
- from .placeholder_utils import extract_placeholders, validate_format_args
7
-
8
-
9
- class SafeString(str):
10
- """
11
- A string subclass that validates format() calls to ensure all placeholders are provided.
12
-
13
- This prevents common errors where format variables are missing, making prompt templates
14
- more reliable and easier to debug.
15
-
16
- Attributes:
17
- placeholders: Set of placeholder names found in the string
18
- """
19
-
20
- placeholders: Set[str]
21
-
22
- def __new__(cls, value: str) -> "SafeString":
23
- """Create a new SafeString instance with extracted placeholders."""
24
- instance = str.__new__(cls, value)
25
- instance.placeholders = extract_placeholders(value)
26
- return instance
27
-
28
- def format(self, *args: Any, **kwargs: Any) -> str:
29
- """
30
- Format the string with configurable validation behavior.
31
-
32
- By default (skip_validation=False), this method validates that all placeholders
33
- have corresponding values and raises ValueError if any are missing.
34
-
35
- When skip_validation=True, it performs partial formatting, replacing only
36
- the placeholders for which values are provided.
37
-
38
- Args:
39
- *args: Positional arguments for formatting
40
- skip_validation: If True, perform partial formatting without validation
41
- **kwargs: Keyword arguments for formatting
42
-
43
- Returns:
44
- The formatted string
45
-
46
- Raises:
47
- ValueError: If skip_validation=False and any placeholder is missing
48
- """
49
- skip_validation = kwargs.pop('skip_validation', False)
50
- if skip_validation:
51
- # Partial formatting - replace only available placeholders
52
- return self._partial_format(*args, **kwargs)
53
- else:
54
- # Strict formatting - validate all placeholders are provided
55
- validate_format_args(self.placeholders, args, kwargs, skip_validation=False)
56
- return str.format(self, *args, **kwargs)
57
-
58
- def _partial_format(self, *args: Any, **kwargs: Any) -> str:
59
- """
60
- Perform partial formatting, replacing only the placeholders that have values.
61
-
62
- Args:
63
- *args: Positional arguments for formatting
64
- **kwargs: Keyword arguments for formatting
65
-
66
- Returns:
67
- The partially formatted string
68
- """
69
- # Convert positional args to keyword args
70
- all_kwargs = kwargs.copy()
71
- for i, arg in enumerate(args):
72
- all_kwargs[str(i)] = arg
73
-
74
- # Build a format string with only available placeholders
75
- result = str(self)
76
-
77
- # Replace placeholders one by one if they have values
78
- for placeholder in self.placeholders:
79
- if placeholder in all_kwargs:
80
- # Create a single-placeholder format string
81
- placeholder_pattern = f"{{{placeholder}}}"
82
- if placeholder_pattern in result:
83
- try:
84
- # Replace this specific placeholder
85
- result = result.replace(
86
- placeholder_pattern, str(all_kwargs[placeholder])
87
- )
88
- except (KeyError, ValueError):
89
- # If replacement fails, leave the placeholder as is
90
- pass
91
-
92
- return result
93
-
94
- def __str__(self) -> str:
95
- """Return the string representation."""
96
- return str.__str__(self)
97
-
98
- def __repr__(self) -> str:
99
- """Return the string representation for debugging."""
100
- return f"SafeString({str.__repr__(self)}, placeholders={self.placeholders})"
101
-
102
- @classmethod
103
- def __get_pydantic_core_schema__(
104
- cls, source_type: Any, handler: GetCoreSchemaHandler
105
- ) -> core_schema.CoreSchema:
106
- """Support for Pydantic v2 schema generation."""
107
- return core_schema.no_info_after_validator_function(
108
- cls,
109
- core_schema.str_schema(),
110
- )