readwright 0.4.0__tar.gz → 0.5.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.
Files changed (93) hide show
  1. {readwright-0.4.0 → readwright-0.5.0}/.agents/skills/readwright/SKILL.md +1 -0
  2. {readwright-0.4.0 → readwright-0.5.0}/AGENTS.md +1 -1
  3. {readwright-0.4.0 → readwright-0.5.0}/PKG-INFO +15 -3
  4. {readwright-0.4.0 → readwright-0.5.0}/README.md +14 -2
  5. {readwright-0.4.0 → readwright-0.5.0}/README.md.j2 +11 -0
  6. {readwright-0.4.0 → readwright-0.5.0}/pyproject.toml +1 -1
  7. readwright-0.5.0/src/readwright/__init__.py +12 -0
  8. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/cli.py +100 -42
  9. {readwright-0.4.0 → readwright-0.5.0}/tests/conftest.py +8 -0
  10. {readwright-0.4.0 → readwright-0.5.0}/tests/test_cli.py +34 -0
  11. {readwright-0.4.0 → readwright-0.5.0}/uv.lock +1 -1
  12. readwright-0.4.0/src/readwright/__init__.py +0 -8
  13. {readwright-0.4.0 → readwright-0.5.0}/.agents/skills/readwright/helpers.md +0 -0
  14. {readwright-0.4.0 → readwright-0.5.0}/.github/workflows/ci.yml +0 -0
  15. {readwright-0.4.0 → readwright-0.5.0}/.github/workflows/release.yml +0 -0
  16. {readwright-0.4.0 → readwright-0.5.0}/.gitignore +0 -0
  17. {readwright-0.4.0 → readwright-0.5.0}/.pre-commit-config.yaml +0 -0
  18. {readwright-0.4.0 → readwright-0.5.0}/.pre-commit-hooks.yaml +0 -0
  19. {readwright-0.4.0 → readwright-0.5.0}/.python-version +0 -0
  20. {readwright-0.4.0 → readwright-0.5.0}/CLAUDE.md +0 -0
  21. {readwright-0.4.0 → readwright-0.5.0}/LICENSE +0 -0
  22. {readwright-0.4.0 → readwright-0.5.0}/action.yml +0 -0
  23. {readwright-0.4.0 → readwright-0.5.0}/examples/README.md +0 -0
  24. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/README.md +0 -0
  25. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/docs/screenshots/CREDITS.md +0 -0
  26. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/docs/screenshots/captions.yaml +0 -0
  27. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/docs/screenshots/dashboard.jpg +0 -0
  28. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/docs/screenshots/editor-dark.jpg +0 -0
  29. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/docs/screenshots/editor-light.jpg +0 -0
  30. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/pyproject.toml +0 -0
  31. {readwright-0.4.0 → readwright-0.5.0}/examples/config-only/readme.yaml +0 -0
  32. {readwright-0.4.0 → readwright-0.5.0}/examples/dotnet-tool/DotTool.csproj +0 -0
  33. {readwright-0.4.0 → readwright-0.5.0}/examples/dotnet-tool/README.md +0 -0
  34. {readwright-0.4.0 → readwright-0.5.0}/examples/dotnet-tool/readme.yaml +0 -0
  35. {readwright-0.4.0 → readwright-0.5.0}/examples/go-cli/README.md +0 -0
  36. {readwright-0.4.0 → readwright-0.5.0}/examples/go-cli/go.mod +0 -0
  37. {readwright-0.4.0 → readwright-0.5.0}/examples/go-cli/readme.yaml +0 -0
  38. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/.all-contributorsrc +0 -0
  39. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/.env.example +0 -0
  40. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/CHANGELOG.md +0 -0
  41. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/LICENSE +0 -0
  42. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/README.md +0 -0
  43. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/README.md.j2 +0 -0
  44. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/demo_tool/__init__.py +0 -0
  45. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/demo_tool/cli.py +0 -0
  46. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/logo.svg +0 -0
  47. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/CREDITS.md +0 -0
  48. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/captions.yaml +0 -0
  49. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/main.jpg +0 -0
  50. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/mobile/captions.yaml +0 -0
  51. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/mobile/phone.jpg +0 -0
  52. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/settings-dark.jpg +0 -0
  53. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/screenshots/settings-light.jpg +0 -0
  54. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/docs/usage.md +0 -0
  55. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/pyproject.toml +0 -0
  56. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/readme.yaml +0 -0
  57. {readwright-0.4.0 → readwright-0.5.0}/examples/kitchen-sink/templates/partials/contributing.md.j2 +0 -0
  58. {readwright-0.4.0 → readwright-0.5.0}/examples/node-cli/README.md +0 -0
  59. {readwright-0.4.0 → readwright-0.5.0}/examples/node-cli/package.json +0 -0
  60. {readwright-0.4.0 → readwright-0.5.0}/examples/node-cli/readme.yaml +0 -0
  61. {readwright-0.4.0 → readwright-0.5.0}/examples/rust-cli/Cargo.toml +0 -0
  62. {readwright-0.4.0 → readwright-0.5.0}/examples/rust-cli/README.md +0 -0
  63. {readwright-0.4.0 → readwright-0.5.0}/examples/rust-cli/readme.yaml +0 -0
  64. {readwright-0.4.0 → readwright-0.5.0}/readme.yaml +0 -0
  65. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/badges.py +0 -0
  66. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/changelog.py +0 -0
  67. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/config.py +0 -0
  68. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/helpers.py +0 -0
  69. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/images.py +0 -0
  70. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/metadata.py +0 -0
  71. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/py.typed +0 -0
  72. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/renderer.py +0 -0
  73. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/base.md.j2 +0 -0
  74. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/badges.md.j2 +0 -0
  75. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/contributing.md.j2 +0 -0
  76. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/donate.md.j2 +0 -0
  77. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/header.md.j2 +0 -0
  78. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/install.md.j2 +0 -0
  79. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/license.md.j2 +0 -0
  80. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/screenshots.md.j2 +0 -0
  81. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/templates/partials/usage.md.j2 +0 -0
  82. {readwright-0.4.0 → readwright-0.5.0}/src/readwright/toc.py +0 -0
  83. {readwright-0.4.0 → readwright-0.5.0}/tests/golden/base.md +0 -0
  84. {readwright-0.4.0 → readwright-0.5.0}/tests/test_badges.py +0 -0
  85. {readwright-0.4.0 → readwright-0.5.0}/tests/test_changelog.py +0 -0
  86. {readwright-0.4.0 → readwright-0.5.0}/tests/test_config.py +0 -0
  87. {readwright-0.4.0 → readwright-0.5.0}/tests/test_examples.py +0 -0
  88. {readwright-0.4.0 → readwright-0.5.0}/tests/test_helpers.py +0 -0
  89. {readwright-0.4.0 → readwright-0.5.0}/tests/test_images.py +0 -0
  90. {readwright-0.4.0 → readwright-0.5.0}/tests/test_metadata.py +0 -0
  91. {readwright-0.4.0 → readwright-0.5.0}/tests/test_renderer.py +0 -0
  92. {readwright-0.4.0 → readwright-0.5.0}/tests/test_toc.py +0 -0
  93. {readwright-0.4.0 → readwright-0.5.0}/tox.ini +0 -0
@@ -37,6 +37,7 @@ template/config, then re-render.
37
37
  | `readwright blocks` | List overridable template blocks/partials |
38
38
  | `readwright skill [--install] [--dest DIR] [--force]` | Print the bundled copy of this skill, or install it into a project's `.agents/skills/` (or `--dest`) |
39
39
  | `readwright show <template>` | Print a packaged template (`base.md.j2` for the block order, or e.g. `partials/install.md.j2`) to copy and customize |
40
+ | `readwright completion <shell>` | Print a bash/zsh/fish/PowerShell completion script |
40
41
 
41
42
  All accept `-C/--root` (repo root) and `-c/--config` (explicit config path) as
42
43
  options *after* the subcommand, e.g. `readwright render -C /path/to/repo` —
@@ -51,7 +51,7 @@ regenerating rather than hand-editing expected output.
51
51
  ## Layout
52
52
 
53
53
  - `src/readwright/cli.py` — Typer commands: `init`, `render`, `check`, `badges`,
54
- `blocks`, `show`, `skill`.
54
+ `blocks`, `show`, `skill`, `completion`.
55
55
  - `src/readwright/renderer.py` — Jinja2 environment setup and rendering pipeline.
56
56
  - `src/readwright/helpers.py`, `badges.py`, `images.py`, `toc.py`, `changelog.py`,
57
57
  `metadata.py` — the template helper functions exposed to `README.md.j2`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: readwright
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Render GitHub READMEs from Jinja2 templates with badge and screenshot helpers
5
5
  Project-URL: Homepage, https://github.com/Garulf/readwright
6
6
  Project-URL: Issues, https://github.com/Garulf/readwright/issues
@@ -36,6 +36,7 @@ Render GitHub READMEs from Jinja2 templates with badge and screenshot helpers.
36
36
  - [Template helpers](#template-helpers)
37
37
  - [Configuration](#configuration)
38
38
  - [pre-commit and GitHub Actions](#pre-commit-and-github-actions)
39
+ - [Shell completion](#shell-completion)
39
40
  - [Agent skill](#agent-skill)
40
41
  - [Contributing](#contributing)
41
42
  - [License](#license)
@@ -168,18 +169,29 @@ stays reproducible in CI. `readwright render --user-config` merges them ad hoc.
168
169
  ```yaml
169
170
  # .pre-commit-config.yaml
170
171
  - repo: https://github.com/Garulf/readwright
171
- rev: v0.4.0
172
+ rev: v0.5.0
172
173
  hooks:
173
174
  - id: readwright-check
174
175
  ```
175
176
 
176
177
  ```yaml
177
178
  # .github/workflows/ci.yml
178
- - uses: Garulf/readwright@v0.4.0
179
+ - uses: Garulf/readwright@v0.5.0
179
180
  with:
180
181
  mode: check # or render
181
182
  ```
182
183
 
184
+ ## Shell completion
185
+
186
+ `readwright completion <shell>` prints a completion script for bash, zsh, fish or PowerShell.
187
+ It completes subcommands, options and the template names `readwright show` accepts.
188
+
189
+ ```sh
190
+ eval "$(readwright completion bash)" # add to ~/.bashrc
191
+ eval "$(readwright completion zsh)" # add to ~/.zshrc, after compinit
192
+ readwright completion fish > ~/.config/fish/completions/readwright.fish
193
+ ```
194
+
183
195
  ## Agent skill
184
196
 
185
197
  The repo ships an [agent skill](https://agentskills.io) at `.agents/skills/readwright/`
@@ -10,6 +10,7 @@ Render GitHub READMEs from Jinja2 templates with badge and screenshot helpers.
10
10
  - [Template helpers](#template-helpers)
11
11
  - [Configuration](#configuration)
12
12
  - [pre-commit and GitHub Actions](#pre-commit-and-github-actions)
13
+ - [Shell completion](#shell-completion)
13
14
  - [Agent skill](#agent-skill)
14
15
  - [Contributing](#contributing)
15
16
  - [License](#license)
@@ -142,18 +143,29 @@ stays reproducible in CI. `readwright render --user-config` merges them ad hoc.
142
143
  ```yaml
143
144
  # .pre-commit-config.yaml
144
145
  - repo: https://github.com/Garulf/readwright
145
- rev: v0.4.0
146
+ rev: v0.5.0
146
147
  hooks:
147
148
  - id: readwright-check
148
149
  ```
149
150
 
150
151
  ```yaml
151
152
  # .github/workflows/ci.yml
152
- - uses: Garulf/readwright@v0.4.0
153
+ - uses: Garulf/readwright@v0.5.0
153
154
  with:
154
155
  mode: check # or render
155
156
  ```
156
157
 
158
+ ## Shell completion
159
+
160
+ `readwright completion <shell>` prints a completion script for bash, zsh, fish or PowerShell.
161
+ It completes subcommands, options and the template names `readwright show` accepts.
162
+
163
+ ```sh
164
+ eval "$(readwright completion bash)" # add to ~/.bashrc
165
+ eval "$(readwright completion zsh)" # add to ~/.zshrc, after compinit
166
+ readwright completion fish > ~/.config/fish/completions/readwright.fish
167
+ ```
168
+
157
169
  ## Agent skill
158
170
 
159
171
  The repo ships an [agent skill](https://agentskills.io) at `.agents/skills/readwright/`
@@ -134,6 +134,17 @@ stays reproducible in CI. `readwright render --user-config` merges them ad hoc.
134
134
  mode: check # or render
135
135
  ```
136
136
 
137
+ ## Shell completion
138
+
139
+ `readwright completion <shell>` prints a completion script for bash, zsh, fish or PowerShell.
140
+ It completes subcommands, options and the template names `readwright show` accepts.
141
+
142
+ ```sh
143
+ eval "$(readwright completion bash)" # add to ~/.bashrc
144
+ eval "$(readwright completion zsh)" # add to ~/.zshrc, after compinit
145
+ readwright completion fish > ~/.config/fish/completions/readwright.fish
146
+ ```
147
+
137
148
  ## Agent skill
138
149
 
139
150
  The repo ships an [agent skill](https://agentskills.io) at `.agents/skills/readwright/`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "readwright"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Render GitHub READMEs from Jinja2 templates with badge and screenshot helpers"
5
5
  authors = [
6
6
  { name = "Garulf", email = "535299+Garulf@users.noreply.github.com" }
@@ -0,0 +1,12 @@
1
+ """Render GitHub READMEs from Jinja2 templates."""
2
+
3
+
4
+ def __getattr__(name: str) -> str:
5
+ if name != "__version__":
6
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ try:
10
+ return version("readwright")
11
+ except PackageNotFoundError:
12
+ return "0.0.0"
@@ -6,35 +6,44 @@ import difflib
6
6
  import re
7
7
  import shutil
8
8
  import sys
9
+ from functools import cache
9
10
  from pathlib import Path
10
- from typing import Annotated
11
+ from typing import TYPE_CHECKING, Annotated
11
12
 
12
13
  import typer
13
- import yaml
14
- from rich.console import Console
15
- from rich.table import Table
16
-
17
- from readwright import __version__
18
- from readwright.badges import BadgeRegistry
19
- from readwright.config import (
20
- DEFAULT_CONFIG_NAME,
21
- DEFAULT_TEMPLATE,
22
- Config,
23
- ProjectInfo,
24
- config_sources,
25
- deep_merge,
26
- load_user_config,
27
- resolve,
28
- )
29
- from readwright.renderer import BASE_TEMPLATE, MARKER_PREFIX, Renderer, RenderResult
14
+ from typer.completion import Shells, completion_init, get_completion_script
15
+
16
+ # Every shell Tab press imports this module, so heavy imports live in the functions that use them.
17
+
18
+ if TYPE_CHECKING:
19
+ from rich.console import Console
20
+
21
+ from readwright.config import Config
22
+ from readwright.renderer import RenderResult
23
+
24
+ completion_init()
30
25
 
31
26
  app = typer.Typer(
32
27
  help="Render GitHub READMEs from Jinja2 templates.",
33
28
  no_args_is_help=True,
34
29
  add_completion=False,
35
30
  )
36
- out = Console()
37
- err = Console(stderr=True)
31
+
32
+
33
+ @cache
34
+ def _console(stderr: bool) -> Console:
35
+ from rich.console import Console
36
+
37
+ return Console(stderr=stderr)
38
+
39
+
40
+ def out() -> Console:
41
+ return _console(stderr=False)
42
+
43
+
44
+ def err() -> Console:
45
+ return _console(stderr=True)
46
+
38
47
 
39
48
  RootOpt = Annotated[Path, typer.Option("--root", "-C", help="Repository root.")]
40
49
  ConfigOpt = Annotated[Path | None, typer.Option("--config", "-c", help="Config file path.")]
@@ -52,7 +61,9 @@ VerboseOpt = Annotated[
52
61
 
53
62
  def version_callback(value: bool) -> None:
54
63
  if value:
55
- out.print(f"readwright {__version__}")
64
+ from readwright import __version__
65
+
66
+ out().print(f"readwright {__version__}")
56
67
  raise typer.Exit()
57
68
 
58
69
 
@@ -66,17 +77,19 @@ def main(
66
77
 
67
78
 
68
79
  def warn(message: str) -> None:
69
- err.print("[yellow]warning:[/] ", end="")
70
- err.print(message, markup=False, highlight=False)
80
+ err().print("[yellow]warning:[/] ", end="")
81
+ err().print(message, markup=False, highlight=False)
71
82
 
72
83
 
73
84
  def fail(message: str, code: int = 1) -> None:
74
- err.print("[red]error:[/] ", end="")
75
- err.print(message, markup=False, highlight=False)
85
+ err().print("[red]error:[/] ", end="")
86
+ err().print(message, markup=False, highlight=False)
76
87
  raise typer.Exit(code)
77
88
 
78
89
 
79
90
  def _load(root: Path, config: Path | None, user_config: bool, strict: bool) -> Config:
91
+ from readwright.config import config_sources, resolve
92
+
80
93
  if config is None and len(sources := config_sources(root)) > 1:
81
94
  warn(f"both {' and '.join(sources)} exist; using {sources[0]}")
82
95
  try:
@@ -87,13 +100,15 @@ def _load(root: Path, config: Path | None, user_config: bool, strict: bool) -> C
87
100
 
88
101
 
89
102
  def _render_with(root: Path, cfg: Config, verbose: bool) -> RenderResult:
103
+ from readwright.renderer import Renderer
104
+
90
105
  try:
91
106
  result = Renderer(root, cfg, warn=warn).render()
92
107
  except Exception as exc:
93
108
  fail(f"{type(exc).__name__}: {exc}")
94
109
  if verbose:
95
110
  for name, source in result.sources.items():
96
- err.print(f"[dim]template {name} <- {source}[/]")
111
+ err().print(f"[dim]template {name} <- {source}[/]")
97
112
  return result
98
113
 
99
114
 
@@ -105,6 +120,8 @@ def _render(
105
120
 
106
121
 
107
122
  def _is_managed(path: Path) -> bool:
123
+ from readwright.renderer import MARKER_PREFIX
124
+
108
125
  return path.read_text(encoding="utf-8").startswith(MARKER_PREFIX)
109
126
 
110
127
 
@@ -112,7 +129,7 @@ def _write_output(root: Path, cfg: Config, result: RenderResult, force: bool) ->
112
129
  target = root / cfg.output
113
130
  if target.is_file():
114
131
  if target.read_text(encoding="utf-8") == result.text:
115
- out.print(f"{cfg.output} unchanged")
132
+ out().print(f"{cfg.output} unchanged")
116
133
  return
117
134
  if not force and not _is_managed(target):
118
135
  fail(
@@ -121,10 +138,12 @@ def _write_output(root: Path, cfg: Config, result: RenderResult, force: bool) ->
121
138
  )
122
139
  target.parent.mkdir(parents=True, exist_ok=True)
123
140
  target.write_text(result.text, encoding="utf-8")
124
- out.print(f"{cfg.output} updated from {result.template_name}")
141
+ out().print(f"{cfg.output} updated from {result.template_name}")
125
142
 
126
143
 
127
144
  def watch_paths(root: Path, cfg: Config) -> list[Path]:
145
+ from readwright.config import DEFAULT_CONFIG_NAME
146
+
128
147
  candidates = [
129
148
  root / cfg.template,
130
149
  root / DEFAULT_CONFIG_NAME,
@@ -180,7 +199,7 @@ def _watch_loop(
180
199
 
181
200
  cfg = _load(root, config, user_config, strict)
182
201
  paths = watch_paths(root, cfg)
183
- err.print(
202
+ err().print(
184
203
  f"[dim]watching {', '.join(str(p.relative_to(root)) for p in paths)} (ctrl-c to stop)[/]"
185
204
  )
186
205
  try:
@@ -205,6 +224,8 @@ def check(
205
224
  verbose: VerboseOpt = False,
206
225
  ) -> None:
207
226
  """Exit 1 if the output file is out of date with its template."""
227
+ from readwright.renderer import MARKER_PREFIX
228
+
208
229
  cfg, result = _render(root, config, user_config, strict, verbose)
209
230
  for name in result.user_templates:
210
231
  warn(f"user-level template '{name}' was used; CI renders will differ")
@@ -216,7 +237,7 @@ def check(
216
237
  warn(f"{cfg.output} is not managed by readwright (no marker); skipping")
217
238
  return
218
239
  if current == result.text:
219
- out.print(f"{cfg.output} is up to date")
240
+ out().print(f"{cfg.output} is up to date")
220
241
  return
221
242
  diff = difflib.unified_diff(
222
243
  current.splitlines(keepends=True),
@@ -224,7 +245,7 @@ def check(
224
245
  fromfile=cfg.output,
225
246
  tofile=f"{cfg.output} (rendered)",
226
247
  )
227
- out.print("".join(diff), end="", highlight=False, markup=False)
248
+ out().print("".join(diff), end="", highlight=False, markup=False)
228
249
  fail(f"{cfg.output} is out of date; run `readwright render`")
229
250
 
230
251
 
@@ -266,6 +287,8 @@ JINJA_SYNTAX = re.compile(r"{[{%#]")
266
287
 
267
288
 
268
289
  def _init_config_data(root: Path) -> dict:
290
+ from readwright.config import deep_merge, load_user_config, resolve
291
+
269
292
  cfg = resolve(root)
270
293
  data = cfg.model_dump(exclude_defaults=True, exclude_none=True)
271
294
  if (user := load_user_config()) is not None:
@@ -308,6 +331,10 @@ def init(
308
331
  ] = False,
309
332
  ) -> None:
310
333
  """Scaffold readme.yaml and README.md.j2 in the repository."""
334
+ import yaml
335
+
336
+ from readwright.config import DEFAULT_CONFIG_NAME, DEFAULT_TEMPLATE
337
+
311
338
  config_path = root / DEFAULT_CONFIG_NAME
312
339
  template_path = root / DEFAULT_TEMPLATE
313
340
  existing = [
@@ -339,14 +366,19 @@ def init(
339
366
  if from_readme:
340
367
  cfg = _load(root, None, False, False)
341
368
  _write_output(root, cfg, _render_with(root, cfg, False), force=True)
342
- out.print(f"created {config_name} and {DEFAULT_TEMPLATE}; README.md is now managed")
369
+ out().print(f"created {config_name} and {DEFAULT_TEMPLATE}; README.md is now managed")
343
370
  return
344
- out.print(f"created {config_name} and {DEFAULT_TEMPLATE}; run `readwright render`")
371
+ out().print(f"created {config_name} and {DEFAULT_TEMPLATE}; run `readwright render`")
345
372
 
346
373
 
347
374
  @app.command()
348
375
  def badges(root: RootOpt = Path("."), config: ConfigOpt = None) -> None:
349
376
  """List available badge presets."""
377
+ from rich.table import Table
378
+
379
+ from readwright.badges import BadgeRegistry
380
+ from readwright.config import Config, ProjectInfo, resolve
381
+
350
382
  try:
351
383
  cfg = resolve(root, config_path=config)
352
384
  except ValueError:
@@ -361,38 +393,53 @@ def badges(root: RootOpt = Path("."), config: ConfigOpt = None) -> None:
361
393
  except ValueError as exc:
362
394
  example = f"[dim]{exc}[/]"
363
395
  table.add_row(name, example)
364
- out.print(table)
396
+ out().print(table)
365
397
 
366
398
 
367
399
  @app.command()
368
400
  def blocks(root: RootOpt = Path("."), config: ConfigOpt = None) -> None:
369
401
  """List the blocks of base.md.j2 and the partials that can be shadowed."""
402
+ from readwright.renderer import BASE_TEMPLATE, Renderer
403
+
370
404
  cfg = _load(root, config, False, False)
371
405
  renderer = Renderer(root, cfg)
372
406
  template = renderer.env.get_template(BASE_TEMPLATE)
373
- out.print(f"[bold]blocks in {BASE_TEMPLATE}[/]")
407
+ out().print(f"[bold]blocks in {BASE_TEMPLATE}[/]")
374
408
  for name in template.blocks:
375
- out.print(f" {name}")
376
- out.print("[bold]partials[/] (shadow with templates/partials/<name> in the repo)")
409
+ out().print(f" {name}")
410
+ out().print("[bold]partials[/] (shadow with templates/partials/<name> in the repo)")
377
411
  for name in sorted(renderer.env.list_templates()):
378
412
  if name.startswith("partials/"):
379
- out.print(f" {name} [dim]<- {renderer.source_label(name)}[/]")
413
+ out().print(f" {name} [dim]<- {renderer.source_label(name)}[/]")
414
+
415
+
416
+ def bundled_template_names(incomplete: str) -> list[str]:
417
+ templates = Path(__file__).resolve().parent / "templates"
418
+ names = sorted(p.relative_to(templates).as_posix() for p in templates.rglob("*") if p.is_file())
419
+ return [name for name in names if name.startswith(incomplete)]
380
420
 
381
421
 
382
422
  @app.command()
383
423
  def show(
384
- name: Annotated[str, typer.Argument(help="Template name, e.g. base.md.j2")],
424
+ name: Annotated[
425
+ str,
426
+ typer.Argument(
427
+ help="Template name, e.g. base.md.j2", autocompletion=bundled_template_names
428
+ ),
429
+ ],
385
430
  root: RootOpt = Path("."),
386
431
  config: ConfigOpt = None,
387
432
  ) -> None:
388
433
  """Print a template's source (useful for copying a partial to override it)."""
434
+ from readwright.renderer import Renderer
435
+
389
436
  cfg = _load(root, config, False, False)
390
437
  renderer = Renderer(root, cfg)
391
438
  try:
392
439
  source, _, _ = renderer.env.loader.get_source(renderer.env, name)
393
440
  except Exception:
394
441
  fail(f"template '{name}' not found")
395
- err.print(f"[dim]# {name} <- {renderer.source_label(name)}[/]")
442
+ err().print(f"[dim]# {name} <- {renderer.source_label(name)}[/]")
396
443
  sys.stdout.write(source)
397
444
 
398
445
 
@@ -428,7 +475,7 @@ def skill(
428
475
  """Print the bundled agent skill's location, or --install it into a project."""
429
476
  source = bundled_skill_dir()
430
477
  if not install:
431
- out.print(str(source), highlight=False, soft_wrap=True)
478
+ out().print(str(source), highlight=False, soft_wrap=True)
432
479
  return
433
480
  skills_dir = dest if dest is not None else root / DEFAULT_SKILL_DEST
434
481
  target = skills_dir / SKILL_NAME
@@ -437,7 +484,18 @@ def skill(
437
484
  fail(f"{target} already exists (use --force to replace it)")
438
485
  shutil.rmtree(target)
439
486
  shutil.copytree(source, target)
440
- out.print(f"[green]installed[/] skill to {target}", highlight=False, soft_wrap=True)
487
+ out().print(f"[green]installed[/] skill to {target}", highlight=False, soft_wrap=True)
488
+
489
+
490
+ @app.command()
491
+ def completion(
492
+ shell: Annotated[Shells, typer.Argument(help="Shell to generate the script for.")],
493
+ ) -> None:
494
+ """Print a shell completion script, e.g. eval "$(readwright completion zsh)"."""
495
+ script = get_completion_script(
496
+ prog_name="readwright", complete_var="_READWRIGHT_COMPLETE", shell=shell.value
497
+ )
498
+ sys.stdout.write(script + "\n")
441
499
 
442
500
 
443
501
  if __name__ == "__main__":
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import subprocess
2
3
  from pathlib import Path
3
4
 
@@ -19,6 +20,13 @@ classifiers = [
19
20
  PNG_BYTES = b"\x89PNG\r\n\x1a\n"
20
21
 
21
22
 
23
+ @pytest.fixture(autouse=True)
24
+ def isolated_git_env(monkeypatch):
25
+ # git hooks export GIT_DIR, which would point test repos at the real repository
26
+ for name in [name for name in os.environ if name.startswith("GIT_")]:
27
+ monkeypatch.delenv(name)
28
+
29
+
22
30
  @pytest.fixture
23
31
  def tmp_repo(tmp_path: Path) -> Path:
24
32
  subprocess.run(["git", "init", "-q"], cwd=tmp_path, check=True)
@@ -1,3 +1,5 @@
1
+ import subprocess
2
+ import sys
1
3
  from pathlib import Path
2
4
 
3
5
  import yaml
@@ -237,3 +239,35 @@ def test_skill_install_custom_dest_and_refuses_overwrite(tmp_path):
237
239
  assert again.exit_code == 1
238
240
  assert "already exists" in again.output
239
241
  assert run("skill", "--install", "--dest", dest, "--force").exit_code == 0
242
+
243
+
244
+ def test_completion_prints_script_for_shell():
245
+ for shell in ("bash", "zsh", "fish"):
246
+ result = run("completion", shell)
247
+ assert result.exit_code == 0, result.output
248
+ assert "_READWRIGHT_COMPLETE=complete_" in result.output
249
+
250
+
251
+ def test_completion_rejects_unknown_shell():
252
+ assert run("completion", "tcsh").exit_code != 0
253
+
254
+
255
+ def test_completion_suggests_commands_and_templates():
256
+ def complete(words, cword):
257
+ env = {"_READWRIGHT_COMPLETE": "complete_bash", "COMP_WORDS": words, "COMP_CWORD": cword}
258
+ return runner.invoke(app, [], env=env, prog_name="readwright").output.split()
259
+
260
+ assert {"render", "completion"} <= set(complete("readwright ", "1"))
261
+ assert "partials/install.md.j2" in complete("readwright show partials/", "2")
262
+ assert complete("readwright show base", "2") == ["base.md.j2"]
263
+
264
+
265
+ def test_import_skips_heavy_dependencies():
266
+ code = (
267
+ "import sys, readwright.cli; "
268
+ "print(sorted(m for m in ('pydantic', 'jinja2', 'rich', 'yaml') if m in sys.modules))"
269
+ )
270
+ result = subprocess.run(
271
+ [sys.executable, "-c", code], capture_output=True, text=True, check=True
272
+ )
273
+ assert result.stdout.strip() == "[]"
@@ -577,7 +577,7 @@ wheels = [
577
577
 
578
578
  [[package]]
579
579
  name = "readwright"
580
- version = "0.4.0"
580
+ version = "0.5.0"
581
581
  source = { editable = "." }
582
582
  dependencies = [
583
583
  { name = "jinja2" },
@@ -1,8 +0,0 @@
1
- """Render GitHub READMEs from Jinja2 templates."""
2
-
3
- from importlib.metadata import PackageNotFoundError, version
4
-
5
- try:
6
- __version__ = version("readwright")
7
- except PackageNotFoundError:
8
- __version__ = "0.0.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes