Skip to content

Commit 2169415

Browse files
Add to empty README files in add_readme (#749)
* Add to empty README files in add_readme * Various refactorings and new tests * Tweak tests to reflect new add_readme behaviour for empty/all-whitespace files * Update tests to reflect new behaviour * Handle non-UTF8 encodings in add_readme existing file check
1 parent 33ae98a commit 2169415

6 files changed

Lines changed: 103 additions & 32 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ To start a new project from scratch with a complete set of recommended tooling,
105105
```console
106106
$ uvx usethis init
107107
✔ Writing 'pyproject.toml' and initializing project.
108+
✔ Writing 'README.md'.
109+
☐ Populate 'README.md' to help users understand the project.
108110
✔ Adding recommended linters.
109111
☐ Run 'uv run ruff check --fix' to run the Ruff linter with autofixes.
110112
☐ Run 'uv run deptry src' to run deptry.

src/usethis/_core/badge.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88

99
from usethis._config import usethis_config
1010
from usethis._console import plain_print, tick_print, warn_print
11-
from usethis._core.readme import add_readme, get_readme_path
11+
from usethis._core.readme import (
12+
NonMarkdownREADMEError,
13+
add_readme,
14+
get_markdown_readme_path,
15+
)
1216
from usethis._integrations.project.name import get_project_name
1317

1418
if TYPE_CHECKING:
15-
from pathlib import Path
16-
1719
from typing_extensions import Self
1820

1921

@@ -106,9 +108,11 @@ def add_badge(badge: Badge) -> None:
106108
add_readme()
107109

108110
try:
109-
path = _get_markdown_readme_path()
110-
except FileNotFoundError:
111-
warn_print("README file not found, printing badge markdown instead...")
111+
path = get_markdown_readme_path()
112+
except NonMarkdownREADMEError:
113+
warn_print(
114+
"No Markdown-based README file found, printing badge markdown instead..."
115+
)
112116
plain_print(badge.markdown)
113117
return
114118

@@ -196,20 +200,6 @@ def _get_prerequisites(badge: Badge) -> list[Badge]:
196200
return prerequisites
197201

198202

199-
def _get_markdown_readme_path() -> Path:
200-
path = get_readme_path()
201-
202-
if path.name == "README.md":
203-
pass
204-
elif path.name == "README":
205-
warn_print("Assuming 'README' file is Markdown.")
206-
else:
207-
msg = f"README file '{path.name}' is not Markdown based on its extension."
208-
raise FileNotFoundError(msg)
209-
210-
return path
211-
212-
213203
def _ensure_final_newline(content: str) -> str:
214204
if not content or content[-1] != "\n":
215205
content += "\n"
@@ -236,7 +226,7 @@ def remove_badge(badge: Badge) -> None:
236226
path = usethis_config.cpd() / "README.md"
237227

238228
try:
239-
path = _get_markdown_readme_path()
229+
path = get_markdown_readme_path()
240230
except FileNotFoundError:
241231
# If there's no README.md, there's nothing to remove
242232
return

src/usethis/_core/readme.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING
4+
35
from usethis._config import usethis_config
4-
from usethis._console import box_print, tick_print
6+
from usethis._console import box_print, tick_print, warn_print
57
from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLError
68
from usethis._integrations.file.pyproject_toml.name import get_description
79
from usethis._integrations.project.name import get_project_name
10+
from usethis.errors import UsethisError
11+
12+
if TYPE_CHECKING:
13+
from pathlib import Path
814

915

1016
def add_readme() -> None:
1117
"""Add a README.md file to the project."""
1218
# Any file extension is fine, but we'll use '.md' for consistency.
1319

1420
try:
15-
get_readme_path()
21+
path = get_readme_path()
1622
except FileNotFoundError:
1723
pass
1824
else:
19-
return
25+
# Check if the file is non-empty; if so, we will exit early
26+
try:
27+
existing_content = path.read_text(encoding="utf-8")
28+
except UnicodeDecodeError:
29+
return
30+
if existing_content.strip():
31+
return
2032

2133
project_name = get_project_name()
2234

@@ -64,6 +76,24 @@ def get_readme_path():
6476
raise FileNotFoundError(msg)
6577

6678

79+
class NonMarkdownREADMEError(UsethisError):
80+
"""Raised when the README file is not Markdown based on its extension."""
81+
82+
83+
def get_markdown_readme_path() -> Path:
84+
path = get_readme_path()
85+
86+
if path.name == "README.md":
87+
pass
88+
elif path.name == "README":
89+
warn_print("Assuming 'README' file is Markdown.")
90+
else:
91+
msg = f"README file '{path.name}' is not Markdown based on its extension."
92+
raise NonMarkdownREADMEError(msg)
93+
94+
return path
95+
96+
6797
def is_readme_used():
6898
"""Check if the README.md file is used."""
6999
try:

tests/usethis/_core/test_core_badge.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,28 @@ def test_not_badge(self):
5050

5151

5252
class TestAddBadge:
53+
def test_no_readme(self, bare_dir: Path, capfd: pytest.CaptureFixture[str]):
54+
# Act
55+
with change_cwd(bare_dir), PyprojectTOMLManager():
56+
add_badge(
57+
Badge(
58+
markdown="![Licence](https://img.shields.io/badge/licence-mit-green)",
59+
)
60+
)
61+
62+
# Assert (that the badge markdown is printed)
63+
out, err = capfd.readouterr()
64+
assert not err
65+
assert out == (
66+
"✔ Writing 'README.md'.\n"
67+
"☐ Populate 'README.md' to help users understand the project.\n"
68+
"✔ Adding Licence badge to 'README.md'.\n"
69+
)
70+
5371
def test_not_markdown(self, bare_dir: Path, capfd: pytest.CaptureFixture[str]):
5472
# Arrange
5573
path = bare_dir / "README.foo"
56-
path.touch()
74+
path.write_text("# Header\n")
5775

5876
# Act
5977
with change_cwd(bare_dir), PyprojectTOMLManager():
@@ -67,15 +85,14 @@ def test_not_markdown(self, bare_dir: Path, capfd: pytest.CaptureFixture[str]):
6785
out, err = capfd.readouterr()
6886
assert not err
6987
assert out == (
70-
"⚠ README file not found, printing badge markdown instead...\n"
88+
"⚠ No Markdown-based README file found, printing badge markdown instead...\n"
7189
"![Licence](https://img.shields.io/badge/licence-mit-green)\n"
7290
)
7391

7492
def test_empty(self, bare_dir: Path, capfd: pytest.CaptureFixture[str]):
7593
# Arrange
7694
path = bare_dir / "README.md"
77-
path.write_text("""\
78-
""")
95+
path.write_text("")
7996

8097
# Act
8198
with change_cwd(bare_dir), PyprojectTOMLManager():
@@ -89,12 +106,18 @@ def test_empty(self, bare_dir: Path, capfd: pytest.CaptureFixture[str]):
89106
assert (
90107
(bare_dir / "README.md").read_text()
91108
== """\
109+
# test_empty0
110+
92111
![Licence](https://img.shields.io/badge/licence-mit-green)
93112
"""
94113
)
95114
out, err = capfd.readouterr()
96115
assert not err
97-
assert out == "✔ Adding Licence badge to 'README.md'.\n"
116+
assert out == (
117+
"✔ Writing 'README.md'.\n"
118+
"☐ Populate 'README.md' to help users understand the project.\n"
119+
"✔ Adding Licence badge to 'README.md'.\n"
120+
)
98121

99122
def test_only_newline(self, bare_dir: Path):
100123
# Arrange
@@ -115,6 +138,8 @@ def test_only_newline(self, bare_dir: Path):
115138
assert (
116139
(bare_dir / "README.md").read_text()
117140
== """\
141+
# test_only_newline0
142+
118143
![Licence](https://img.shields.io/badge/licence-mit-green)
119144
"""
120145
)

tests/usethis/_core/test_core_readme.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_start_from_nothing(
2626

2727
def test_different_suffix(self, tmp_path: Path):
2828
# Arrange
29-
(tmp_path / "README.rst").touch()
29+
(tmp_path / "README.rst").write_text("Existing content")
3030

3131
# Act
3232
with change_cwd(tmp_path), PyprojectTOMLManager():
@@ -48,10 +48,10 @@ def test_readme_directory(self, tmp_path: Path):
4848

4949
def test_readme_no_suffix(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]):
5050
# Arrange
51-
(tmp_path / "README").touch()
51+
(tmp_path / "README").write_text("Existing content")
5252

5353
# Act
54-
with change_cwd(tmp_path):
54+
with change_cwd(tmp_path), PyprojectTOMLManager():
5555
add_readme()
5656

5757
# Assert
@@ -139,3 +139,21 @@ def test_project_name_and_description(self, tmp_path: Path):
139139
# Assert
140140
assert (tmp_path / "README.md").exists()
141141
assert (tmp_path / "README.md").read_text() == "# A name\n\nA description\n"
142+
143+
def test_start_from_readme_empty(self, tmp_path: Path):
144+
# Arrange
145+
(tmp_path / "pyproject.toml").write_text(
146+
"""\
147+
[project]
148+
name = "A name"
149+
description = "A description"
150+
"""
151+
)
152+
(tmp_path / "README.md").touch()
153+
154+
# Act
155+
with change_cwd(tmp_path), PyprojectTOMLManager():
156+
add_readme()
157+
158+
# Assert
159+
assert (tmp_path / "README.md").read_text() == "# A name\n\nA description\n"

tests/usethis/_interface/test_init.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def test_pre_commit_included(self, tmp_path: Path):
1919
assert (tmp_path / "pyproject.toml").exists()
2020
assert result.output == (
2121
"✔ Writing 'pyproject.toml' and initializing project.\n"
22+
"✔ Writing 'README.md'.\n"
23+
"☐ Populate 'README.md' to help users understand the project.\n"
2224
"✔ Adding the pre-commit framework.\n"
2325
"☐ Run 'uv run pre-commit run --all-files' to run the hooks manually.\n"
2426
"✔ Adding recommended linters.\n"
@@ -67,6 +69,8 @@ def test_readme_example(self, tmp_path: Path):
6769
# ###################################
6870
== """\
6971
✔ Writing 'pyproject.toml' and initializing project.
72+
✔ Writing 'README.md'.
73+
☐ Populate 'README.md' to help users understand the project.
7074
✔ Adding recommended linters.
7175
☐ Run 'uv run ruff check --fix' to run the Ruff linter with autofixes.
7276
☐ Run 'uv run deptry src' to run deptry.
@@ -120,6 +124,8 @@ def test_bitbucket_and_docstyle(self, tmp_path: Path):
120124
assert (tmp_path / "pyproject.toml").exists()
121125
assert result.output == (
122126
"✔ Writing 'pyproject.toml' and initializing project.\n"
127+
"✔ Writing 'README.md'.\n"
128+
"☐ Populate 'README.md' to help users understand the project.\n"
123129
"✔ Adding the pre-commit framework.\n"
124130
"☐ Run 'uv run pre-commit run --all-files' to run the hooks manually.\n"
125131
"✔ Adding recommended linters.\n"

0 commit comments

Comments
 (0)