Repository navigation
Use hyphens, not underscores, in generated GitHub repository URLs #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mariushelf
merged 4 commits into
main
from
feat/16-loop-use-hyphens-not-underscores-in-generated
Oct 8, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
4811a78
Materialize signed-off spec for #16
mariushelf 17a41a4
test: assert generated GitHub URLs use hyphenated repo name
mariushelf 2be5cb3
fix: use hyphens in generated GitHub repository URLs
mariushelf c32a506
refactor: derive GitHub repo name once in a computed repo_name variable
mariushelf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Plan: #16, use hyphens in generated GitHub repository URLs | ||
|
|
||
| ## What I found in the tree | ||
|
|
||
| - Only three template lines build `github.com/{{ github_username }}/{{ project_slug }}`: `template/pyproject.toml.jinja:28-29` and `template/README.md.jinja:3` and `:9`. A grep of `template/` for `github.com` and `github_username` finds nothing else. `.pre-commit-config.yaml.jinja` and `README.md.jinja:74` point at third-party repositories. `.claude/settings.json` only lists the bare host. These stay as they are. | ||
| - The template already builds the hyphenated name inline as `{{ project_slug | replace('_', '-') }}`. It does this 4 times: `pyproject.toml.jinja:6`, `README.md.jinja:4` (twice) and `:25`, and `docs/source/conf.py.jinja:25`. | ||
| - The `project_slug` uses that must keep underscores are: `pyproject.toml.jinja:32` (wheel `packages`), `:114`, `:129`, `:135` and `:149` (import-linter), all of `AGENTS.md.jinja`, `python-api.md.jinja`, and the `src/` and `tests/` file and directory names. | ||
| - The tests render with `copier.run_copy(..., vcs_ref="HEAD")`. In copier 9.11.3, `_vcs.py:191-211` copies uncommitted changes from a local HEAD and raises a `DirtyLocalWarning`. So a template edit is visible to the tests before it is committed. You do not need to commit before you go from red to green. | ||
| - There is no `graphify-out/` in this worktree, so skip the `graphify update .` step from CLAUDE.md. | ||
|
|
||
| ## Decisions the spec left open | ||
|
|
||
| 1. **Inline filter, no shared variable.** Write `{{ project_slug | replace('_', '-') }}` directly at each of the 6 places. Do not add a `{% set %}`, and do not add a hidden copier variable with `when: false`. This matches the 4 existing places that do the same. A hidden copier variable would also be saved in `.copier-answers.yml`, and that comes too close to the "no new question" assumption. | ||
| 2. **The new test does its own light render, not the session `project` fixture.** The fixture runs `uv sync`, so the rendered tree has `.venv/` and `uv.lock`. The editable install's `*.dist-info/METADATA` repeats the `Project-URL` values. A grep over the whole tree (criterion 3) would then scan thousands of files and depend on what `uv` writes. Follow the pattern of `test_default_slug_is_valid_package_name`: call `copier.run_copy` into `tmp_path` with `COPIER_DATA` and no `uv sync`. Parametrize over `include_hexagonal` (`[True, False]`, ids `hexagonal` and `flat`) so both layouts are grepped. Each case takes a few seconds and needs no network. | ||
| 3. **One test function covers criteria 1–3.** They are all facts about the same render. If one fails, the assertion message shows which. | ||
|
|
||
| ## Steps | ||
|
|
||
| ### Step 1: Write a failing test (red) | ||
|
|
||
| **File:** `tests/test_template.py`. Add the test after `test_docs_scaffold_renders`, or next to `test_default_slug_is_valid_package_name`. A suggested name is `test_github_urls_use_hyphenated_repo_name`. Add `import tomllib` to the imports. | ||
|
|
||
| The test should: | ||
| - Render as described in decision 2. | ||
| - Parse `pyproject.toml` with `tomllib`. Assert that `project.urls` equals `{"Homepage": "https://github.com/testuser/test-project", "Repository": "https://github.com/testuser/test-project"}`. | ||
| - Collect every `https://github.com/testuser/<repo>` in `README.md` with a regex such as `r"github\.com/testuser/([^/)\s\"]+)"`. Assert the list is not empty (4 matches are expected today). Assert every match equals `"test-project"`. | ||
| - Walk every file under the destination, skipping `.git/`. Read each one as text and skip any that fail to decode. Assert that no file contains `github.com/testuser/test_project`. The message should list the files that do. | ||
| - Have a docstring that says why: the repository name uses hyphens, the package name uses underscores, and the CI badge broke for `auto-shopper`. Match the docstring style of the tests around it. | ||
|
|
||
| **Verify:** `uv run pytest tests/test_template.py -k github_urls -v` fails on the current template, in both parametrized cases, on the pyproject assertion. Record that red run, because the spec requires the test to fail against the current template. | ||
|
|
||
| **Dependencies:** none. | ||
|
|
||
| ### Step 2: Fix the template (green) | ||
|
|
||
| **Files:** | ||
| - `template/pyproject.toml.jinja:28-29`: replace `{{ project_slug }}` with `{{ project_slug | replace('_', '-') }}` in the `Homepage` and `Repository` URLs. | ||
| - `template/README.md.jinja:3`: same change in both URLs (badge image and link). | ||
| - `template/README.md.jinja:9`: same change in both URLs (link text and target). | ||
|
|
||
| Do not touch any other `project_slug` use (see the list above). | ||
|
|
||
| **Verify:** | ||
| - `uv run pytest tests/test_template.py -k github_urls -v` passes in both cases. | ||
| - `grep -rn 'github.com/{{ github_username }}/{{ project_slug }}}' template/` returns nothing. | ||
|
|
||
| **Dependencies:** Step 1, so that the test is red before the fix. | ||
|
|
||
| ### Checkpoint: full suite | ||
|
|
||
| - Run `uv run pytest`. It is slow: 2 renders, `uv sync`, pre-commit and a Sphinx build. Pass `timeout` at about 600000 ms and run it in the foreground. All existing tests must stay green. This covers criterion 5: `test_template_renders` checks `src/test_project/`, `test_main_executes` imports `test_project.main`, and `test_make_lint` runs the import-linter contracts. | ||
| - `git diff --stat` shows only `tests/test_template.py`, `template/pyproject.toml.jinja` and `template/README.md.jinja`. | ||
|
|
||
| ### Step 3: Commit | ||
|
|
||
| Make one commit for the test and one for the fix, or a single `fix:` commit. Both fit the history, which uses separate conventional commits per concern (see `bd38693` and `9515c31`). Suggested messages are `test: assert generated GitHub URLs use hyphenated repo name` and `fix: use hyphens in generated GitHub repository URLs`. | ||
|
|
||
| ## Mapping to the acceptance criteria | ||
|
|
||
| | Criterion | Proved by | | ||
| |---|---| | ||
| | 1 (pyproject URLs) | Step 1, `tomllib` assertion | | ||
| | 2 (README URLs) | Step 1, regex over `README.md` | | ||
| | 3 (no underscore URL anywhere) | Step 1, file walk over the rendered tree | | ||
| | 4 (test exists and fails before the fix) | Step 1 red run, then Step 2 green run | | ||
| | 5 (underscores kept for imports) | Existing suite at the checkpoint, with no edits to those lines | | ||
|
|
||
| ## Risks | ||
|
|
||
| | Risk | Impact | Mitigation | | ||
| |---|---|---| | ||
| | The file walk hits binary or odd files | Low | Skip `.git/` and skip files that fail to decode. The render without `uv sync` contains only template output and `.copier-answers.yml`. | | ||
| | The fix is applied too broadly and touches `packages` or import-linter lines | High: breaks imports and lint | Edit only the 3 listed lines. `test_make_lint`, `test_main_executes` and `test_generated_tests_pass` would catch a mistake. | | ||
| | The full suite needs network access for `uv sync` and pre-commit | Medium | Run Step 1 and Step 2 by themselves first, then the full suite. If the full suite fails only because of the network, report that as such. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| ## Spec | ||
|
|
||
| **What:** Derive the GitHub repository name in generated URLs from `project_slug` with underscores replaced by hyphens (`{{ project_slug | replace('_', '-') }}`), the same derivation the template already uses for the distribution name. Affected lines on `main`: | ||
|
|
||
| - `template/pyproject.toml.jinja:28-29` — `Homepage` and `Repository` URLs | ||
| - `template/README.md.jinja:3` — CI badge image and link | ||
| - `template/README.md.jinja:9` — "Original repository" link | ||
|
|
||
| **Why:** GitHub repositories are conventionally named with hyphens, while the Python package name needs underscores. Today a project with `project_slug = auto_shopper` gets URLs pointing at `github.com/<user>/auto_shopper`, while the repository is `auto-shopper`, so the CI badge and the project URLs are broken in every generated project with an underscore in its slug. Found while generating `mariushelf/auto-shopper`. | ||
|
|
||
| ## Acceptance criteria | ||
| - [ ] In a project generated with `project_slug = "test_project"` and `github_username = "testuser"`, `pyproject.toml` has `Homepage` and `Repository` equal to `https://github.com/testuser/test-project`. | ||
| - [ ] In the same project, every `github.com/testuser/...` URL in `README.md` uses `test-project`, and none uses `test_project`. | ||
| - [ ] No other generated file contains `github.com/<github_username>/<project_slug>` with an underscore (checked by grep over the rendered project). | ||
| - [ ] A test in `tests/test_template.py` renders the template and asserts the two criteria above, and fails against the current template. | ||
| - [ ] Python import paths, the package directory under `src/`, and import-linter contracts still use `project_slug` with underscores (existing tests stay green). | ||
|
|
||
| ## Assumptions | ||
| - No new copier question (such as `repo_name`) is added; the repository name is derived, as asked. A separate question can be added later if a repository name ever differs from the hyphenated slug. | ||
| - Already generated projects are not migrated; `copier update` picks the change up. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[minor] The refactor adds a copier variable
repo_name, the name the spec assumption excludes. The plan also rejected a hiddenwhen: falsevariable. The variable is not asked, but--data repo_name=custom-repooverrides it. A render showedcustom-repoin the URLs. The override is undocumented, and copier does not save it.Suggested fix: A human decides. To keep the variable, document the
--dataoverride inhelpor the README. Otherwise revertc32a506to the inline{{ project_slug | replace('_', '-') }}filter. That filter matches the four existing uses.~Written by Claude, run via the agentic engineering loop