Skip to content

Commit 13f5208

Browse files
Add --output-file option to usethis show commands (#1565)
* Initial plan * Initial plan for --output-file option Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/1258b6ab-bd9c-4a25-a42a-1f452a5349f3 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Add --output-file option to usethis show commands Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/1258b6ab-bd9c-4a25-a42a-1f452a5349f3 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Improve test for sonarqube --output-file redirect scenario Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/1258b6ab-bd9c-4a25-a42a-1f452a5349f3 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Move output_file_opt to centralized options module Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/5efe9934-159d-487c-8b3f-444c3d252dfc Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Remove excessive detail from --output-file docs Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/cd21b7cc-e068-4f4d-b672-45567726b7e2 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Co-authored-by: Nathan McDougall <nathan.j.mcdougall@gmail.com>
1 parent 0080bcf commit 13f5208

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
@@ -1,18 +1,36 @@
11
from __future__ import annotations
22

3+
from typing import TYPE_CHECKING
4+
35
from usethis._backend.dispatch import get_backend
46
from usethis._console import plain_print
57
from usethis._integrations.project.name import get_project_name
68
from usethis._integrations.sonarqube.config import get_sonar_project_properties
79

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

9-
def show_backend() -> None:
10-
plain_print(get_backend().value)
18+
def show_name(*, output_file: Path | None = None) -> None:
19+
_output(get_project_name(), output_file=output_file)
1120

1221

13-
def show_name() -> None:
14-
plain_print(get_project_name())
22+
def show_sonarqube_config(
23+
*, project_key: str | None = None, output_file: Path | None = None
24+
) -> None:
25+
_output(
26+
get_sonar_project_properties(project_key=project_key), output_file=output_file
27+
)
1528

1629

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

src/usethis/_ui/interface/show.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from pathlib import Path
2+
13
import typer
24

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

68
app = typer.Typer(
79
help="Show information about the current project.", add_completion=False
@@ -19,6 +21,7 @@
1921
def backend(
2022
offline: bool = offline_opt,
2123
quiet: bool = quiet_opt,
24+
output_file: Path | None = output_file_opt,
2225
) -> None:
2326
from usethis._config_file import files_manager
2427
from usethis._console import err_print
@@ -27,7 +30,7 @@ def backend(
2730

2831
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
2932
try:
30-
show_backend()
33+
show_backend(output_file=output_file)
3134
except UsethisError as err:
3235
err_print(err)
3336
raise typer.Exit(code=1) from None
@@ -37,6 +40,7 @@ def backend(
3740
def name(
3841
offline: bool = offline_opt,
3942
quiet: bool = quiet_opt,
43+
output_file: Path | None = output_file_opt,
4044
) -> None:
4145
from usethis._config_file import files_manager
4246
from usethis._console import err_print
@@ -45,7 +49,7 @@ def name(
4549

4650
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
4751
try:
48-
show_name()
52+
show_name(output_file=output_file)
4953
except UsethisError as err:
5054
err_print(err)
5155
raise typer.Exit(code=1) from None
@@ -59,6 +63,7 @@ def sonarqube(
5963
offline: bool = offline_opt,
6064
quiet: bool = quiet_opt,
6165
project_key: str | None = project_key_opt,
66+
output_file: Path | None = output_file_opt,
6267
) -> None:
6368
from usethis._config_file import files_manager
6469
from usethis._console import err_print
@@ -67,7 +72,7 @@ def sonarqube(
6772

6873
with usethis_config.set(offline=offline, quiet=quiet), files_manager():
6974
try:
70-
show_sonarqube_config(project_key=project_key)
75+
show_sonarqube_config(project_key=project_key, output_file=output_file)
7176
except UsethisError as err:
7277
err_print(err)
7378
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
@@ -104,6 +104,13 @@
104104
# status command options
105105
status_arg = typer.Argument(default=..., help="Docstring style to enforce.")
106106

107+
# show command options
108+
output_file_opt = typer.Option(
109+
None,
110+
"--output-file",
111+
help="Write output to this file instead of stdout.",
112+
)
113+
107114
# ruff command options
108115
linter_opt = typer.Option(
109116
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)