prompt-comments 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: pip install -e ".[dev]"
21
+ - run: pytest -v
@@ -0,0 +1,20 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # required for PyPI trusted publishing
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ venv/
12
+ .env
13
+ .DS_Store
14
+ .vscode/
15
+ .idea/
16
+ coverage.xml
17
+ .coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 viswanathms
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.5
2
+ Name: prompt-comments
3
+ Version: 0.1.0
4
+ Summary: Write comments in your AI prompts, strip them before they reach the model.
5
+ Project-URL: Homepage, https://github.com/viswanathms/prompt-comments
6
+ Project-URL: Repository, https://github.com/viswanathms/prompt-comments
7
+ Project-URL: Issues, https://github.com/viswanathms/prompt-comments/issues
8
+ Author: viswanathms
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,comments,jinja,llm,markdown,prompt,prompt-engineering,template
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Text Processing :: Filters
26
+ Requires-Python: >=3.8
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # prompt-comments
32
+
33
+ [![CI](https://github.com/viswanathms/prompt-comments/actions/workflows/ci.yml/badge.svg)](https://github.com/viswanathms/prompt-comments/actions/workflows/ci.yml)
34
+ [![PyPI](https://img.shields.io/pypi/v/prompt-comments.svg)](https://pypi.org/project/prompt-comments/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
36
+
37
+ Write comments in your AI prompts. Strip them before they reach the model.
38
+
39
+ Prompts live in Markdown files, Jinja templates, YAML configs, plain
40
+ `.txt` files — but none of those give you a way to leave a note for
41
+ your future self ("why is this instruction here?", "TODO: tighten
42
+ this up") without the note leaking into the actual text sent to the
43
+ model. `prompt-comments` strips comments in whatever format your
44
+ prompt already uses, right before you hand the text to your LLM SDK
45
+ of choice.
46
+
47
+ ```python
48
+ from prompt_comments import strip_comments
49
+
50
+ prompt = """
51
+ You are a helpful support assistant. {# keep tone friendly, not robotic #}
52
+ Always answer in the user's language. <!-- TODO: revisit for RTL languages -->
53
+ """
54
+
55
+ strip_comments(prompt)
56
+ # "\nYou are a helpful support assistant.\nAlways answer in the user's language.\n"
57
+ ```
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install prompt-comments
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ### Strip comments from a string
68
+
69
+ ```python
70
+ from prompt_comments import strip_comments
71
+
72
+ strip_comments("Summarize this. {# only for the 'concise' variant #}")
73
+ # "Summarize this."
74
+ ```
75
+
76
+ By default, `strip_comments` removes a small set of comment styles
77
+ that are unlikely to collide with real prompt text: Jinja-style block
78
+ comments (`{# ... #}`), HTML/Markdown block comments (`<!-- ... -->`),
79
+ and `//` line comments. Pick a different (or additional) dialect for
80
+ formats with their own conventions:
81
+
82
+ ```python
83
+ strip_comments(open("system_prompt.yaml").read(), dialect="yaml")
84
+
85
+ # Combine multiple dialects
86
+ strip_comments(text, dialect=["jinja", "yaml"])
87
+ ```
88
+
89
+ ### Load a prompt straight from a file
90
+
91
+ `load_prompt` reads a file and infers the right dialect from its
92
+ extension automatically (`.j2`/`.jinja2` → Jinja, `.md` → Markdown,
93
+ `.yaml` → YAML, ...):
94
+
95
+ ```python
96
+ from prompt_comments import load_prompt
97
+
98
+ system_prompt = load_prompt("prompts/system.md")
99
+ ```
100
+
101
+ Override the inferred dialect if needed:
102
+
103
+ ```python
104
+ load_prompt("prompts/system.txt", dialect="yaml")
105
+ ```
106
+
107
+ ### Command line
108
+
109
+ ```bash
110
+ prompt-comments prompts/system.md # auto-detects dialect from extension
111
+ prompt-comments prompts/system.txt -d yaml # force a dialect
112
+ cat prompt.txt | prompt-comments -d jinja # read from stdin
113
+ prompt-comments --list-dialects
114
+ ```
115
+
116
+ ### Escaping a literal marker
117
+
118
+ Prefix a line-comment marker with `\` to keep it literally instead of
119
+ treating it as a comment:
120
+
121
+ ```python
122
+ strip_comments(r"The meeting room is \#3 today", dialect="yaml")
123
+ # "The meeting room is #3 today"
124
+ ```
125
+
126
+ ### Dropping now-empty lines
127
+
128
+ Comments that occupied a whole line leave a blank line behind by
129
+ default. Pass `strip_blank_lines=True` to drop them instead:
130
+
131
+ ```python
132
+ strip_comments("keep\n// only a comment\nkeep too", dialect="c", strip_blank_lines=True)
133
+ # "keep\nkeep too"
134
+ ```
135
+
136
+ ## Supported dialects
137
+
138
+ | Dialect | Line comments | Block comments |
139
+ |---|---|---|
140
+ | `default` | `//` | `{# #}`, `<!-- -->` |
141
+ | `jinja` / `jinja2` | — | `{# #}` |
142
+ | `django` | — | `{# #}`, `{% comment %}...{% endcomment %}` |
143
+ | `liquid` | — | `{% comment %}...{% endcomment %}` |
144
+ | `handlebars` / `mustache` | — | `{{! }}`, `{{!-- --}}` |
145
+ | `markdown` / `html` / `xml` | — | `<!-- -->` |
146
+ | `yaml` / `toml` / `env` | `#` | — |
147
+ | `ini` | `#`, `;` | — |
148
+ | `json5` / `jsonc` | `//` | `/* */` |
149
+ | `python` | `#` | — |
150
+ | `shell` / `bash` / `sh` / `zsh` | `#` | — |
151
+ | `sql` | `--` | `/* */` |
152
+ | `latex` | `%` | — |
153
+ | `c` / `js` / `ts` / `go` / `rust` / `java` / `csharp` / ... | `//` | `/* */` |
154
+ | `all` | every marker above | every block style above |
155
+
156
+ Run `prompt-comments --list-dialects` (or `prompt_comments.list_dialects()`) for the exact, current list.
157
+
158
+ Dialects can be combined by passing a list, e.g. `dialect=["yaml", "jinja"]`.
159
+
160
+ ## A note on false positives
161
+
162
+ Comment stripping here is regex-based, not a full parser for every
163
+ format. A line-comment marker only starts a comment when it's at the
164
+ start of a line or preceded by whitespace (this is why
165
+ `https://example.com` survives the `//` marker: the `//` is preceded
166
+ by `:`, not whitespace). It is still a heuristic — e.g. `# Heading`
167
+ in a Markdown file *would* be stripped if you opted into the `yaml`
168
+ or `python` dialect (which treat `#` as a comment marker) on that
169
+ text. Stick to `default`/`markdown`/`jinja`-style dialects for prose
170
+ prompts with literal `#`, `--`, or `%` characters, or escape the
171
+ character with a backslash where it must be kept literally.
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ pip install -e ".[dev]"
177
+ pytest
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,152 @@
1
+ # prompt-comments
2
+
3
+ [![CI](https://github.com/viswanathms/prompt-comments/actions/workflows/ci.yml/badge.svg)](https://github.com/viswanathms/prompt-comments/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/prompt-comments.svg)](https://pypi.org/project/prompt-comments/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ Write comments in your AI prompts. Strip them before they reach the model.
8
+
9
+ Prompts live in Markdown files, Jinja templates, YAML configs, plain
10
+ `.txt` files — but none of those give you a way to leave a note for
11
+ your future self ("why is this instruction here?", "TODO: tighten
12
+ this up") without the note leaking into the actual text sent to the
13
+ model. `prompt-comments` strips comments in whatever format your
14
+ prompt already uses, right before you hand the text to your LLM SDK
15
+ of choice.
16
+
17
+ ```python
18
+ from prompt_comments import strip_comments
19
+
20
+ prompt = """
21
+ You are a helpful support assistant. {# keep tone friendly, not robotic #}
22
+ Always answer in the user's language. <!-- TODO: revisit for RTL languages -->
23
+ """
24
+
25
+ strip_comments(prompt)
26
+ # "\nYou are a helpful support assistant.\nAlways answer in the user's language.\n"
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install prompt-comments
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Strip comments from a string
38
+
39
+ ```python
40
+ from prompt_comments import strip_comments
41
+
42
+ strip_comments("Summarize this. {# only for the 'concise' variant #}")
43
+ # "Summarize this."
44
+ ```
45
+
46
+ By default, `strip_comments` removes a small set of comment styles
47
+ that are unlikely to collide with real prompt text: Jinja-style block
48
+ comments (`{# ... #}`), HTML/Markdown block comments (`<!-- ... -->`),
49
+ and `//` line comments. Pick a different (or additional) dialect for
50
+ formats with their own conventions:
51
+
52
+ ```python
53
+ strip_comments(open("system_prompt.yaml").read(), dialect="yaml")
54
+
55
+ # Combine multiple dialects
56
+ strip_comments(text, dialect=["jinja", "yaml"])
57
+ ```
58
+
59
+ ### Load a prompt straight from a file
60
+
61
+ `load_prompt` reads a file and infers the right dialect from its
62
+ extension automatically (`.j2`/`.jinja2` → Jinja, `.md` → Markdown,
63
+ `.yaml` → YAML, ...):
64
+
65
+ ```python
66
+ from prompt_comments import load_prompt
67
+
68
+ system_prompt = load_prompt("prompts/system.md")
69
+ ```
70
+
71
+ Override the inferred dialect if needed:
72
+
73
+ ```python
74
+ load_prompt("prompts/system.txt", dialect="yaml")
75
+ ```
76
+
77
+ ### Command line
78
+
79
+ ```bash
80
+ prompt-comments prompts/system.md # auto-detects dialect from extension
81
+ prompt-comments prompts/system.txt -d yaml # force a dialect
82
+ cat prompt.txt | prompt-comments -d jinja # read from stdin
83
+ prompt-comments --list-dialects
84
+ ```
85
+
86
+ ### Escaping a literal marker
87
+
88
+ Prefix a line-comment marker with `\` to keep it literally instead of
89
+ treating it as a comment:
90
+
91
+ ```python
92
+ strip_comments(r"The meeting room is \#3 today", dialect="yaml")
93
+ # "The meeting room is #3 today"
94
+ ```
95
+
96
+ ### Dropping now-empty lines
97
+
98
+ Comments that occupied a whole line leave a blank line behind by
99
+ default. Pass `strip_blank_lines=True` to drop them instead:
100
+
101
+ ```python
102
+ strip_comments("keep\n// only a comment\nkeep too", dialect="c", strip_blank_lines=True)
103
+ # "keep\nkeep too"
104
+ ```
105
+
106
+ ## Supported dialects
107
+
108
+ | Dialect | Line comments | Block comments |
109
+ |---|---|---|
110
+ | `default` | `//` | `{# #}`, `<!-- -->` |
111
+ | `jinja` / `jinja2` | — | `{# #}` |
112
+ | `django` | — | `{# #}`, `{% comment %}...{% endcomment %}` |
113
+ | `liquid` | — | `{% comment %}...{% endcomment %}` |
114
+ | `handlebars` / `mustache` | — | `{{! }}`, `{{!-- --}}` |
115
+ | `markdown` / `html` / `xml` | — | `<!-- -->` |
116
+ | `yaml` / `toml` / `env` | `#` | — |
117
+ | `ini` | `#`, `;` | — |
118
+ | `json5` / `jsonc` | `//` | `/* */` |
119
+ | `python` | `#` | — |
120
+ | `shell` / `bash` / `sh` / `zsh` | `#` | — |
121
+ | `sql` | `--` | `/* */` |
122
+ | `latex` | `%` | — |
123
+ | `c` / `js` / `ts` / `go` / `rust` / `java` / `csharp` / ... | `//` | `/* */` |
124
+ | `all` | every marker above | every block style above |
125
+
126
+ Run `prompt-comments --list-dialects` (or `prompt_comments.list_dialects()`) for the exact, current list.
127
+
128
+ Dialects can be combined by passing a list, e.g. `dialect=["yaml", "jinja"]`.
129
+
130
+ ## A note on false positives
131
+
132
+ Comment stripping here is regex-based, not a full parser for every
133
+ format. A line-comment marker only starts a comment when it's at the
134
+ start of a line or preceded by whitespace (this is why
135
+ `https://example.com` survives the `//` marker: the `//` is preceded
136
+ by `:`, not whitespace). It is still a heuristic — e.g. `# Heading`
137
+ in a Markdown file *would* be stripped if you opted into the `yaml`
138
+ or `python` dialect (which treat `#` as a comment marker) on that
139
+ text. Stick to `default`/`markdown`/`jinja`-style dialects for prose
140
+ prompts with literal `#`, `--`, or `%` characters, or escape the
141
+ character with a backslash where it must be kept literally.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ pip install -e ".[dev]"
147
+ pytest
148
+ ```
149
+
150
+ ## License
151
+
152
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "prompt-comments"
7
+ version = "0.1.0"
8
+ description = "Write comments in your AI prompts, strip them before they reach the model."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "viswanathms" },
15
+ ]
16
+ keywords = ["prompt", "prompt-engineering", "llm", "ai", "comments", "jinja", "template", "markdown"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ "Topic :: Text Processing :: Filters",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/viswanathms/prompt-comments"
36
+ Repository = "https://github.com/viswanathms/prompt-comments"
37
+ Issues = "https://github.com/viswanathms/prompt-comments/issues"
38
+
39
+ [project.scripts]
40
+ prompt-comments = "prompt_comments.cli:main"
41
+
42
+ [project.optional-dependencies]
43
+ dev = ["pytest>=7.0"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/prompt_comments"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
@@ -0,0 +1,21 @@
1
+ """prompt_comments: write comments in your AI prompts, strip them before
2
+ they reach the model.
3
+
4
+ >>> from prompt_comments import strip_comments
5
+ >>> strip_comments("Summarize this. {# only for the 'concise' variant #}")
6
+ 'Summarize this.'
7
+ """
8
+
9
+ from .core import strip_comments
10
+ from .dialects import Dialect, list_dialects
11
+ from .loader import detect_dialect, load_prompt
12
+
13
+ __all__ = [
14
+ "strip_comments",
15
+ "load_prompt",
16
+ "detect_dialect",
17
+ "list_dialects",
18
+ "Dialect",
19
+ ]
20
+
21
+ __version__ = "0.1.0"
@@ -0,0 +1,62 @@
1
+ """``prompt-comments`` command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from typing import Optional, Sequence
8
+
9
+ from .core import strip_comments
10
+ from .dialects import list_dialects
11
+ from .loader import detect_dialect
12
+
13
+
14
+ def build_parser() -> argparse.ArgumentParser:
15
+ parser = argparse.ArgumentParser(
16
+ prog="prompt-comments",
17
+ description="Strip comments from a prompt file (or stdin) and print the result.",
18
+ )
19
+ parser.add_argument("path", nargs="?", help="Prompt file to clean. Reads stdin if omitted.")
20
+ parser.add_argument(
21
+ "-d",
22
+ "--dialect",
23
+ action="append",
24
+ help="Comment dialect to strip (repeatable). Default: inferred from the file "
25
+ "extension, or 'default' when reading stdin.",
26
+ )
27
+ parser.add_argument(
28
+ "--strip-blank-lines",
29
+ action="store_true",
30
+ help="Drop lines that become empty after their comment is removed.",
31
+ )
32
+ parser.add_argument(
33
+ "--list-dialects",
34
+ action="store_true",
35
+ help="List available dialect names and exit.",
36
+ )
37
+ return parser
38
+
39
+
40
+ def main(argv: Optional[Sequence[str]] = None) -> int:
41
+ parser = build_parser()
42
+ args = parser.parse_args(argv)
43
+
44
+ if args.list_dialects:
45
+ for name in list_dialects():
46
+ print(name)
47
+ return 0
48
+
49
+ if args.path:
50
+ with open(args.path, "r", encoding="utf-8") as f:
51
+ text = f.read()
52
+ dialect = args.dialect or detect_dialect(args.path)
53
+ else:
54
+ text = sys.stdin.read()
55
+ dialect = args.dialect or "default"
56
+
57
+ sys.stdout.write(strip_comments(text, dialect=dialect, strip_blank_lines=args.strip_blank_lines))
58
+ return 0
59
+
60
+
61
+ if __name__ == "__main__":
62
+ raise SystemExit(main())
@@ -0,0 +1,85 @@
1
+ """Core comment-stripping logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Iterable, Union
7
+
8
+ from .dialects import Dialect, resolve_dialects
9
+
10
+ DialectLike = Union[str, Dialect, Iterable[Union[str, Dialect]]]
11
+
12
+ _ESCAPE_PLACEHOLDER = "\0PROMPT_COMMENTS_ESCAPED\0"
13
+
14
+
15
+ def strip_comments(text: str, dialect: DialectLike = "default", *, strip_blank_lines: bool = False) -> str:
16
+ """Remove comments from ``text``.
17
+
18
+ Args:
19
+ text: The prompt text to clean.
20
+ dialect: A dialect name (e.g. ``"jinja"``), a :class:`~prompt_comments.dialects.Dialect`
21
+ instance, or a list mixing either. Defaults to ``"default"``, a small
22
+ set of comment styles ( ``{# #}``, ``<!-- -->``, ``//`` ) chosen to be
23
+ unlikely to collide with real prompt text. See
24
+ :func:`prompt_comments.dialects.list_dialects` for all available names.
25
+ strip_blank_lines: If ``True``, lines that become empty after a comment
26
+ is removed are dropped entirely instead of left as blank lines.
27
+
28
+ Returns:
29
+ The text with all matching comments removed.
30
+
31
+ A line-comment marker can be kept literally by escaping it with a
32
+ backslash, e.g. ``\\# not a comment``.
33
+ """
34
+ dialects = resolve_dialects(dialect)
35
+ text = _strip_block_comments(text, dialects)
36
+ text = _strip_line_comments(text, dialects)
37
+ if strip_blank_lines:
38
+ text = "\n".join(line for line in text.split("\n") if line.strip())
39
+ return text
40
+
41
+
42
+ def _strip_block_comments(text: str, dialects) -> str:
43
+ blocks = []
44
+ seen = set()
45
+ for d in dialects:
46
+ for block in d.block_comments:
47
+ key = (block.start, block.end)
48
+ if key not in seen:
49
+ seen.add(key)
50
+ blocks.append(block)
51
+ if not blocks:
52
+ return text
53
+
54
+ # Longer start delimiters first, so e.g. `{{!--` is tried before `{{!`.
55
+ blocks.sort(key=lambda b: len(b.start), reverse=True)
56
+ pattern = "|".join(re.escape(b.start) + r".*?" + re.escape(b.end) for b in blocks)
57
+ return re.sub(pattern, "", text, flags=re.DOTALL)
58
+
59
+
60
+ def _strip_line_comments(text: str, dialects) -> str:
61
+ markers = set()
62
+ for d in dialects:
63
+ for lc in d.line_comments:
64
+ markers.add(lc.marker)
65
+ if not markers:
66
+ return text
67
+
68
+ # Longest markers first so e.g. `--` isn't a partial match ahead of a
69
+ # hypothetical `---` marker.
70
+ ordered = sorted(markers, key=len, reverse=True)
71
+ marker_pattern = "|".join(re.escape(m) for m in ordered)
72
+
73
+ escaped_re = re.compile(r"\\(" + marker_pattern + r")")
74
+ text = escaped_re.sub(lambda m: _ESCAPE_PLACEHOLDER + m.group(1), text)
75
+
76
+ # A marker only starts a comment at the beginning of a line or after
77
+ # whitespace -- this keeps things like `https://example.com` and
78
+ # `color: #fff` mid-token intact in common cases (URLs specifically:
79
+ # the char before `//` there is `:`, not whitespace, so it's skipped).
80
+ comment_re = re.compile(r"(?:^|(?<=\s))(?:" + marker_pattern + r").*$", re.MULTILINE)
81
+ text = comment_re.sub("", text)
82
+
83
+ text = text.replace(_ESCAPE_PLACEHOLDER, "")
84
+ text = re.sub(r"[ \t]+$", "", text, flags=re.MULTILINE)
85
+ return text
@@ -0,0 +1,189 @@
1
+ """Comment-syntax definitions for the file formats and template engines
2
+ that AI prompts commonly live in.
3
+
4
+ A :class:`Dialect` is just a named bundle of line-comment markers and
5
+ block-comment delimiters. Dialects can be combined freely -- pass a list
6
+ of names to :func:`prompt_comments.strip_comments` to strip several
7
+ comment styles from the same text at once (handy for e.g. a Jinja
8
+ template that is also a Markdown document).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Dict, Tuple
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class LineComment:
19
+ """A marker that comments out everything from itself to end of line."""
20
+
21
+ marker: str
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class BlockComment:
26
+ """A start/end delimiter pair that comments out everything between."""
27
+
28
+ start: str
29
+ end: str
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Dialect:
34
+ """A named collection of comment styles."""
35
+
36
+ name: str
37
+ line_comments: Tuple[LineComment, ...] = field(default_factory=tuple)
38
+ block_comments: Tuple[BlockComment, ...] = field(default_factory=tuple)
39
+
40
+
41
+ # --- Reusable comment-style building blocks --------------------------------
42
+
43
+ _HASH_LINE = LineComment("#")
44
+ _SLASH_LINE = LineComment("//")
45
+ _DASH_LINE = LineComment("--")
46
+ _SEMI_LINE = LineComment(";")
47
+ _PERCENT_LINE = LineComment("%")
48
+
49
+ _JINJA_BLOCK = BlockComment("{#", "#}")
50
+ _HTML_BLOCK = BlockComment("<!--", "-->")
51
+ _C_BLOCK = BlockComment("/*", "*/")
52
+ _LIQUID_BLOCK = BlockComment("{% comment %}", "{% endcomment %}")
53
+ _MUSTACHE_LONG_BLOCK = BlockComment("{{!--", "--}}")
54
+ _MUSTACHE_SHORT_BLOCK = BlockComment("{{!", "}}")
55
+
56
+
57
+ def _dialect(name: str, *, line=(), block=()) -> Dialect:
58
+ return Dialect(name=name, line_comments=tuple(line), block_comments=tuple(block))
59
+
60
+
61
+ # --- Named dialects ----------------------------------------------------------
62
+ #
63
+ # "default" is deliberately small: only comment styles that are very
64
+ # unlikely to collide with real prompt text. Format-specific dialects
65
+ # (yaml, python, sql, ...) add markers like `#`, `--`, `%` that *do*
66
+ # collide with plausible prompt content (Markdown headings, "item #3",
67
+ # LaTeX-ish text) -- opt into those explicitly via the `dialect` argument.
68
+
69
+ _BASE: Dict[str, Dialect] = {
70
+ "default": _dialect("default", line=[_SLASH_LINE], block=[_JINJA_BLOCK, _HTML_BLOCK]),
71
+ "jinja": _dialect("jinja", block=[_JINJA_BLOCK]),
72
+ "django": _dialect("django", block=[_JINJA_BLOCK, _LIQUID_BLOCK]),
73
+ "liquid": _dialect("liquid", block=[_LIQUID_BLOCK]),
74
+ "handlebars": _dialect("handlebars", block=[_MUSTACHE_LONG_BLOCK, _MUSTACHE_SHORT_BLOCK]),
75
+ "markdown": _dialect("markdown", block=[_HTML_BLOCK]),
76
+ "html": _dialect("html", block=[_HTML_BLOCK]),
77
+ "xml": _dialect("xml", block=[_HTML_BLOCK]),
78
+ "yaml": _dialect("yaml", line=[_HASH_LINE]),
79
+ "toml": _dialect("toml", line=[_HASH_LINE]),
80
+ "ini": _dialect("ini", line=[_HASH_LINE, _SEMI_LINE]),
81
+ "env": _dialect("env", line=[_HASH_LINE]),
82
+ "json5": _dialect("json5", line=[_SLASH_LINE], block=[_C_BLOCK]),
83
+ "python": _dialect("python", line=[_HASH_LINE]),
84
+ "shell": _dialect("shell", line=[_HASH_LINE]),
85
+ "sql": _dialect("sql", line=[_DASH_LINE], block=[_C_BLOCK]),
86
+ "latex": _dialect("latex", line=[_PERCENT_LINE]),
87
+ "c": _dialect("c", line=[_SLASH_LINE], block=[_C_BLOCK]),
88
+ }
89
+
90
+ # Aliases: alternate names that map to an existing dialect above.
91
+ _ALIASES: Dict[str, str] = {
92
+ "jinja2": "jinja",
93
+ "mustache": "handlebars",
94
+ "jsonc": "json5",
95
+ "bash": "shell",
96
+ "sh": "shell",
97
+ "zsh": "shell",
98
+ "js": "c",
99
+ "javascript": "c",
100
+ "ts": "c",
101
+ "typescript": "c",
102
+ "go": "c",
103
+ "golang": "c",
104
+ "rust": "c",
105
+ "java": "c",
106
+ "csharp": "c",
107
+ "cs": "c",
108
+ "cpp": "c",
109
+ "c++": "c",
110
+ }
111
+
112
+ DIALECTS: Dict[str, Dialect] = dict(_BASE)
113
+ for _alias, _target in _ALIASES.items():
114
+ DIALECTS[_alias] = DIALECTS[_target]
115
+
116
+ # "all": every comment style from every named dialect, deduplicated. Handy
117
+ # as a maximal default, though it maximizes the odds of a false-positive
118
+ # match against literal prompt text -- prefer naming specific dialects.
119
+ _all_line = {lc for d in _BASE.values() for lc in d.line_comments}
120
+ _all_block = {bc for d in _BASE.values() for bc in d.block_comments}
121
+ DIALECTS["all"] = _dialect("all", line=sorted(_all_line, key=lambda l: l.marker), block=sorted(_all_block, key=lambda b: b.start))
122
+
123
+
124
+ def resolve_dialects(dialect):
125
+ """Normalize a dialect argument into a list of :class:`Dialect` objects.
126
+
127
+ `dialect` may be a dialect name, a :class:`Dialect` instance, or an
128
+ iterable mixing either.
129
+ """
130
+ if isinstance(dialect, (str, Dialect)):
131
+ items = [dialect]
132
+ else:
133
+ items = list(dialect)
134
+
135
+ resolved = []
136
+ for item in items:
137
+ if isinstance(item, Dialect):
138
+ resolved.append(item)
139
+ continue
140
+ name = str(item).lower()
141
+ if name not in DIALECTS:
142
+ raise ValueError(
143
+ f"Unknown dialect {item!r}. Available dialects: {', '.join(sorted(DIALECTS))}"
144
+ )
145
+ resolved.append(DIALECTS[name])
146
+ return resolved
147
+
148
+
149
+ def list_dialects():
150
+ """Return the sorted list of known dialect names (including aliases)."""
151
+ return sorted(DIALECTS)
152
+
153
+
154
+ # --- File-extension -> dialect mapping, used by prompt_comments.loader -----
155
+
156
+ EXTENSION_DIALECTS: Dict[str, str] = {
157
+ ".md": "markdown",
158
+ ".markdown": "markdown",
159
+ ".j2": "jinja",
160
+ ".jinja": "jinja",
161
+ ".jinja2": "jinja",
162
+ ".html": "html",
163
+ ".htm": "html",
164
+ ".xml": "xml",
165
+ ".yaml": "yaml",
166
+ ".yml": "yaml",
167
+ ".toml": "toml",
168
+ ".ini": "ini",
169
+ ".cfg": "ini",
170
+ ".env": "env",
171
+ ".json5": "json5",
172
+ ".jsonc": "jsonc",
173
+ ".py": "python",
174
+ ".sh": "shell",
175
+ ".bash": "shell",
176
+ ".sql": "sql",
177
+ ".tex": "latex",
178
+ ".liquid": "liquid",
179
+ ".hbs": "handlebars",
180
+ ".handlebars": "handlebars",
181
+ ".mustache": "mustache",
182
+ ".js": "javascript",
183
+ ".ts": "typescript",
184
+ ".go": "go",
185
+ ".rs": "rust",
186
+ ".java": "java",
187
+ ".cs": "csharp",
188
+ ".txt": "default",
189
+ }
@@ -0,0 +1,34 @@
1
+ """File-aware convenience wrapper around :func:`prompt_comments.core.strip_comments`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional, Union
7
+
8
+ from .core import DialectLike, strip_comments
9
+ from .dialects import EXTENSION_DIALECTS
10
+
11
+
12
+ def detect_dialect(path: Union[str, Path]) -> str:
13
+ """Guess a dialect name from a file's extension, defaulting to ``"default"``."""
14
+ return EXTENSION_DIALECTS.get(Path(path).suffix.lower(), "default")
15
+
16
+
17
+ def load_prompt(
18
+ path: Union[str, Path],
19
+ dialect: Optional[DialectLike] = None,
20
+ *,
21
+ encoding: str = "utf-8",
22
+ strip_blank_lines: bool = False,
23
+ ) -> str:
24
+ """Read a prompt file and return it with comments stripped.
25
+
26
+ If ``dialect`` is omitted, it is inferred from the file extension
27
+ (e.g. ``.j2`` -> ``jinja``, ``.md`` -> ``markdown``), falling back to
28
+ ``"default"`` for unrecognized extensions.
29
+ """
30
+ path = Path(path)
31
+ text = path.read_text(encoding=encoding)
32
+ if dialect is None:
33
+ dialect = detect_dialect(path)
34
+ return strip_comments(text, dialect=dialect, strip_blank_lines=strip_blank_lines)
File without changes
@@ -0,0 +1,30 @@
1
+ from prompt_comments.cli import main
2
+
3
+
4
+ def test_cli_reads_file_and_strips_comments(tmp_path, capsys):
5
+ p = tmp_path / "prompt.j2"
6
+ p.write_text("Hello {# note #}world")
7
+ assert main([str(p)]) == 0
8
+ assert capsys.readouterr().out == "Hello world"
9
+
10
+
11
+ def test_cli_explicit_dialect_flag(tmp_path, capsys):
12
+ p = tmp_path / "prompt.txt"
13
+ p.write_text("role: system # comment")
14
+ assert main([str(p), "-d", "yaml"]) == 0
15
+ assert capsys.readouterr().out == "role: system"
16
+
17
+
18
+ def test_cli_list_dialects(capsys):
19
+ assert main(["--list-dialects"]) == 0
20
+ out = capsys.readouterr().out.splitlines()
21
+ assert "jinja" in out
22
+ assert "default" in out
23
+
24
+
25
+ def test_cli_reads_stdin(monkeypatch, capsys):
26
+ import io
27
+
28
+ monkeypatch.setattr("sys.stdin", io.StringIO("Hi {# drop #}there"))
29
+ assert main([]) == 0
30
+ assert capsys.readouterr().out == "Hi there"
@@ -0,0 +1,94 @@
1
+ import pytest
2
+
3
+ from prompt_comments import strip_comments
4
+ from prompt_comments.dialects import list_dialects
5
+
6
+
7
+ def test_default_strips_jinja_block():
8
+ assert strip_comments("Hello {# note to self #}world") == "Hello world"
9
+
10
+
11
+ def test_default_strips_html_block():
12
+ text = "Line one\n<!-- drop this\nand this -->\nLine two"
13
+ assert strip_comments(text) == "Line one\n\nLine two"
14
+
15
+
16
+ def test_default_strips_slash_line_comment():
17
+ text = "Be concise. // internal note\nBe kind."
18
+ assert strip_comments(text) == "Be concise.\nBe kind."
19
+
20
+
21
+ def test_default_preserves_url_double_slash():
22
+ text = "See https://example.com/path for details."
23
+ assert strip_comments(text) == text
24
+
25
+
26
+ def test_default_does_not_strip_hash_by_default():
27
+ text = "# Heading\nBody # not stripped in default dialect"
28
+ assert strip_comments(text) == text
29
+
30
+
31
+ def test_yaml_dialect_strips_hash_line_comment():
32
+ text = "role: system # who is speaking"
33
+ assert strip_comments(text, dialect="yaml") == "role: system"
34
+
35
+
36
+ def test_multiple_dialects_combined():
37
+ text = "role: system # yaml comment\nGreet the user. // js comment"
38
+ result = strip_comments(text, dialect=["yaml", "c"])
39
+ assert result == "role: system\nGreet the user."
40
+
41
+
42
+ def test_escaped_marker_is_preserved_literally():
43
+ text = r"The price is \# 3 today"
44
+ assert strip_comments(text, dialect="yaml") == "The price is # 3 today"
45
+
46
+
47
+ def test_liquid_block_comment():
48
+ text = "before {% comment %} secret {% endcomment %} after"
49
+ assert strip_comments(text, dialect="liquid") == "before after"
50
+
51
+
52
+ def test_handlebars_short_and_long_forms():
53
+ text = "a {{! short }} b {{!-- long --}} c"
54
+ assert strip_comments(text, dialect="handlebars") == "a b c"
55
+
56
+
57
+ def test_sql_dialect_line_and_block():
58
+ text = "SELECT * FROM t -- pick everything\n/* multi\nline */ WHERE 1=1"
59
+ assert strip_comments(text, dialect="sql") == "SELECT * FROM t\n WHERE 1=1"
60
+
61
+
62
+ def test_latex_dialect():
63
+ # The marker only starts a comment when preceded by whitespace or line
64
+ # start, so the literal "100%" survives while " % this is..." is dropped.
65
+ text = r"100% done % this is a latex comment"
66
+ assert strip_comments(text, dialect="latex") == "100% done"
67
+
68
+
69
+ def test_strip_blank_lines_option():
70
+ text = "keep\n// only a comment\nkeep too"
71
+ result = strip_comments(text, dialect="c", strip_blank_lines=True)
72
+ assert result == "keep\nkeep too"
73
+
74
+
75
+ def test_unknown_dialect_raises():
76
+ with pytest.raises(ValueError):
77
+ strip_comments("text", dialect="not-a-real-dialect")
78
+
79
+
80
+ def test_all_dialect_is_superset_and_listed():
81
+ assert "all" in list_dialects()
82
+ assert "jinja" in list_dialects()
83
+ assert "jinja2" in list_dialects()
84
+
85
+
86
+ def test_dialect_alias_jinja2_matches_jinja():
87
+ text = "keep {# drop #} keep"
88
+ assert strip_comments(text, dialect="jinja2") == strip_comments(text, dialect="jinja")
89
+
90
+
91
+ def test_no_dialects_selected_is_noop_for_line_only_text():
92
+ # "markdown" dialect only defines block comments; a lone // survives.
93
+ text = "visit https://example.com // not stripped"
94
+ assert strip_comments(text, dialect="markdown") == text
@@ -0,0 +1,27 @@
1
+ from prompt_comments import detect_dialect, load_prompt
2
+
3
+
4
+ def test_detect_dialect_from_extension():
5
+ assert detect_dialect("prompt.j2") == "jinja"
6
+ assert detect_dialect("prompt.md") == "markdown"
7
+ assert detect_dialect("prompt.yaml") == "yaml"
8
+ assert detect_dialect("prompt.unknownext") == "default"
9
+
10
+
11
+ def test_load_prompt_auto_detects_dialect(tmp_path):
12
+ p = tmp_path / "system.md"
13
+ p.write_text("You are a helpful assistant. <!-- internal note --> Be concise.")
14
+ assert load_prompt(p) == "You are a helpful assistant. Be concise."
15
+
16
+
17
+ def test_load_prompt_explicit_dialect_overrides_extension(tmp_path):
18
+ p = tmp_path / "system.txt"
19
+ p.write_text("role: system # not stripped by default dialect")
20
+ assert load_prompt(p, dialect="yaml") == "role: system"
21
+
22
+
23
+ def test_load_prompt_strip_blank_lines(tmp_path):
24
+ p = tmp_path / "prompt.j2"
25
+ p.write_text("keep\n{# drop entire line #}\nkeep too")
26
+ result = load_prompt(p, strip_blank_lines=True)
27
+ assert result == "keep\nkeep too"