Skip to content

Commit 11d8b94

Browse files
Merge branch 'main' into 1576-create-a-prek-hook-to-automatically-export-the-module-structure-with-docstrings-to-a-tree-diagram-file
2 parents b89a71a + 13f5208 commit 11d8b94

5 files changed

Lines changed: 138 additions & 10 deletions

File tree

docs/cli/reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ Additional configuration in `pyproject.toml`:
475475
- `tool.usethis.sonarqube.exclusions` (list of strings, default `[]`) — sets `sonar.exclusions`.
476476
- `tool.coverage.xml.output` (string, required) — sets `sonar.python.coverage.reportPaths`.
477477

478+
Supported options:
479+
480+
- `--output-file` to write the output to a file instead of stdout.
481+
478482
## `usethis browse pypi <package>`
479483

480484
Display or open the PyPI landing page associated with another project.

src/usethis/_core/show.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,37 @@
22

33
from __future__ import annotations
44

5+
from typing import TYPE_CHECKING
6+
57
from usethis._backend.dispatch import get_backend
68
from usethis._console import plain_print
79
from usethis._integrations.project.name import get_project_name
810
from usethis._integrations.sonarqube.config import get_sonar_project_properties
911

12+
if TYPE_CHECKING:
13+
from pathlib import Path
14+
15+
16+
def show_backend(*, output_file: Path | None = None) -> None:
17+
_output(get_backend().value, output_file=output_file)
18+
1019

11-
def show_backend() -> None:
12-
plain_print(get_backend().value)
20+
def show_name(*, output_file: Path | None = None) -> None:
21+
_output(get_project_name(), output_file=output_file)
1322

1423

15-
def show_name() -> None:
16-
plain_print(get_project_name())
24+
def show_sonarqube_config(
25+
*, project_key: str | None = None, output_file: Path | None = None
26+
) -> None:
27+
_output(
28+
get_sonar_project_properties(project_key=project_key), output_file=output_file
29+
)
1730

1831

19-
def show_sonarqube_config(*, project_key: str | None = None) -> None:
20-
plain_print(get_sonar_project_properties(project_key=project_key))
32+
def _output(content: str, *, output_file: Path | None = None) -> None:
33+
if output_file is not None:
34+
if not content.endswith("\n"):
35+
content += "\n"
36+
output_file.write_text(content, encoding="utf-8")
37+
else:
38+
plain_print(content)

src/usethis/_ui/interface/show.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""CLI commands for showing project information."""
22

3+
from pathlib import Path
4+
35
import typer
46

57
from usethis._config import usethis_config
6-
from usethis._ui.options import offline_opt, quiet_opt
8+
from usethis._ui.options import offline_opt, output_file_opt, quiet_opt
79

810
app = typer.Typer(
911
help="Show information about the current project.", add_completion=False
@@ -21,6 +23,7 @@
2123
def backend(
2224
offline: bool = offline_opt,
2325
quiet: bool = quiet_opt,
26+
output_file: Path | None = output_file_opt,
2427
) -> None:
2528
from usethis._config_file import files_manager
2629
from usethis._console import err_print
@@ -29,7 +32,7 @@ def backend(
2932

3033
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
3134
try:
32-
show_backend()
35+
show_backend(output_file=output_file)
3336
except UsethisError as err:
3437
err_print(err)
3538
raise typer.Exit(code=1) from None
@@ -39,6 +42,7 @@ def backend(
3942
def name(
4043
offline: bool = offline_opt,
4144
quiet: bool = quiet_opt,
45+
output_file: Path | None = output_file_opt,
4246
) -> None:
4347
from usethis._config_file import files_manager
4448
from usethis._console import err_print
@@ -47,7 +51,7 @@ def name(
4751

4852
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
4953
try:
50-
show_name()
54+
show_name(output_file=output_file)
5155
except UsethisError as err:
5256
err_print(err)
5357
raise typer.Exit(code=1) from None
@@ -61,6 +65,7 @@ def sonarqube(
6165
offline: bool = offline_opt,
6266
quiet: bool = quiet_opt,
6367
project_key: str | None = project_key_opt,
68+
output_file: Path | None = output_file_opt,
6469
) -> None:
6570
from usethis._config_file import files_manager
6671
from usethis._console import err_print
@@ -69,7 +74,7 @@ def sonarqube(
6974

7075
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
7176
try:
72-
show_sonarqube_config(project_key=project_key)
77+
show_sonarqube_config(project_key=project_key, output_file=output_file)
7378
except UsethisError as err:
7479
err_print(err)
7580
raise typer.Exit(code=1) from None

src/usethis/_ui/options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@
106106
# status command options
107107
status_arg = typer.Argument(default=..., help="Docstring style to enforce.")
108108

109+
# show command options
110+
output_file_opt = typer.Option(
111+
None,
112+
"--output-file",
113+
help="Write output to this file instead of stdout.",
114+
)
115+
109116
# ruff command options
110117
linter_opt = typer.Option(
111118
True,

tests/usethis/_ui/interface/test_show.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ def test_none_backend(self, tmp_path: Path):
3232
assert result.exit_code == 0, result.output
3333
assert result.output == "none\n"
3434

35+
def test_output_file(self, tmp_path: Path):
36+
# Arrange
37+
(tmp_path / "uv.lock").touch()
38+
output_file = tmp_path / "backend.txt"
39+
40+
# Act
41+
runner = CliRunner()
42+
with change_cwd(tmp_path):
43+
result = runner.invoke_safe(
44+
app, ["backend", "--output-file", str(output_file)]
45+
)
46+
47+
# Assert
48+
assert result.exit_code == 0, result.output
49+
assert output_file.read_text(encoding="utf-8") == "uv\n"
50+
3551

3652
class TestName:
3753
def test_output(self, tmp_path: Path):
@@ -60,6 +76,23 @@ def test_invalid_pyproject(self, tmp_path: Path):
6076
# Assert
6177
assert result.exit_code == 1, result.output
6278

79+
def test_output_file(self, tmp_path: Path):
80+
# Arrange
81+
path = tmp_path / "fun"
82+
path.mkdir()
83+
output_file = path / "name.txt"
84+
85+
# Act
86+
runner = CliRunner()
87+
with change_cwd(path):
88+
result = runner.invoke_safe(
89+
app, ["name", "--output-file", str(output_file)]
90+
)
91+
92+
# Assert
93+
assert result.exit_code == 0, result.output
94+
assert output_file.read_text(encoding="utf-8") == "fun\n"
95+
6396

6497
class TestSonarqube:
6598
def test_runs(self, tmp_path: Path):
@@ -162,3 +195,64 @@ def test_invalid_pyproject(self, tmp_path: Path):
162195

163196
# Assert
164197
assert result.exit_code == 1, result.output
198+
199+
def test_output_file(self, tmp_path: Path):
200+
# Arrange
201+
(tmp_path / "pyproject.toml").write_text(
202+
"""
203+
[tool.usethis.sonarqube]
204+
project-key = "fun"
205+
206+
[tool.coverage.xml.output]
207+
"""
208+
)
209+
output_file = tmp_path / "sonar-project.properties"
210+
211+
# Act
212+
runner = CliRunner()
213+
with change_cwd(tmp_path):
214+
result = runner.invoke_safe(
215+
app, ["sonarqube", "--output-file", str(output_file)]
216+
)
217+
218+
# Assert
219+
assert result.exit_code == 0, result.output
220+
content = output_file.read_text(encoding="utf-8")
221+
assert "sonar.projectKey=fun" in content
222+
223+
def test_output_file_not_detected_as_existing(self, tmp_path: Path):
224+
"""Using --output-file avoids the redirect problem.
225+
226+
When using shell redirect (`> file`), the file is created empty before
227+
the command runs, which causes sonarqube to read that empty file.
228+
With --output-file, the file is written after generation.
229+
"""
230+
# Arrange
231+
(tmp_path / "pyproject.toml").write_text(
232+
"""
233+
[tool.usethis.sonarqube]
234+
project-key = "fun"
235+
236+
[tool.coverage.xml.output]
237+
"""
238+
)
239+
# Simulate what happens with shell redirect: an empty file pre-exists
240+
output_file = tmp_path / "sonar-project.properties"
241+
output_file.write_text("", encoding="utf-8")
242+
243+
# Act
244+
# Despite sonar-project.properties existing (empty), --output-file
245+
# still causes the config to be read from that file (by design of
246+
# get_sonar_project_properties), then overwrites it with that content.
247+
runner = CliRunner()
248+
with change_cwd(tmp_path):
249+
result = runner.invoke_safe(
250+
app, ["sonarqube", "--output-file", str(output_file)]
251+
)
252+
253+
# Assert
254+
assert result.exit_code == 0, result.output
255+
content = output_file.read_text(encoding="utf-8")
256+
# With --output-file, the file is written after content generation,
257+
# so even if it was previously empty, it will have the content now
258+
assert content != ""

0 commit comments

Comments
 (0)