Skip to content

Commit dbb5824

Browse files
Don't create pyproject.toml for requriements.txt
1 parent 432c411 commit dbb5824

7 files changed

Lines changed: 292 additions & 47 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ layers = [
214214
"_core",
215215
"_tool",
216216
# Specific config file and backend implementations
217-
"_deps | _init",
217+
"_init",
218+
"_deps",
218219
"_config_file",
219220
"_integrations",
220221
"_io | _subprocess | _console",

src/usethis/_core/tool.py

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88

99
from usethis._config import usethis_config
1010
from usethis._console import info_print, instruct_print, tick_print
11-
from usethis._deps import get_project_deps
12-
from usethis._init import ensure_dep_declaration_file
11+
from usethis._init import ensure_dep_declaration_file, write_simple_requirements_txt
1312
from usethis._integrations.backend.dispatch import get_backend
1413
from usethis._integrations.backend.uv.call import call_uv_subprocess
14+
from usethis._integrations.backend.uv.lockfile import ensure_uv_lock
1515
from usethis._integrations.ci.bitbucket.used import is_bitbucket_used
1616
from usethis._integrations.file.pyproject_toml.valid import ensure_pyproject_validity
1717
from usethis._integrations.mkdocs.core import add_docs_dir
@@ -367,53 +367,46 @@ def use_requirements_txt(*, remove: bool = False, how: bool = False) -> None:
367367
path = usethis_config.cpd() / "requirements.txt"
368368

369369
if not remove:
370-
ensure_dep_declaration_file()
371-
372-
is_pre_commit = PreCommitTool().is_used()
370+
backend = get_backend()
373371

374-
if is_pre_commit:
372+
if PreCommitTool().is_used():
375373
tool.add_pre_commit_config()
376374

377-
if not path.exists():
378-
backend = get_backend()
379-
if backend is BackendEnum.uv:
380-
if (
381-
not (usethis_config.cpd() / "uv.lock").exists()
382-
and not usethis_config.frozen
383-
):
384-
tick_print("Writing 'uv.lock'.")
385-
call_uv_subprocess(["lock"], change_toml=False)
386-
387-
if not usethis_config.frozen:
388-
tick_print("Writing 'requirements.txt'.")
389-
call_uv_subprocess(
390-
[
391-
"export",
392-
"--frozen",
393-
"--output-file=requirements.txt",
394-
],
395-
change_toml=False,
396-
)
397-
elif backend is BackendEnum.none:
398-
# Simply dump the dependencies list to requirements.txt
399-
if usethis_config.backend is BackendEnum.auto:
400-
info_print(
401-
"Generating 'requirements.txt' with un-pinned, abstract dependencies."
402-
)
403-
info_print(
404-
"Consider installing 'uv' for pinned, cross-platform, full requirements files."
405-
)
375+
if path.exists():
376+
# requirements file already exists - short circuit; only need to explain how
377+
# how to re-generate it.
378+
tool.print_how_to_use()
379+
return
380+
381+
if backend is BackendEnum.uv:
382+
if not (usethis_config.cpd() / "pyproject.toml").exists():
383+
write_simple_requirements_txt()
384+
elif not usethis_config.frozen:
385+
ensure_uv_lock()
406386
tick_print("Writing 'requirements.txt'.")
407-
with open(path, "w", encoding="utf-8") as f:
408-
f.write("-e .\n")
409-
f.writelines(
410-
dep.to_requirement_string() + "\n" for dep in get_project_deps()
411-
)
412-
else:
413-
assert_never(backend)
387+
call_uv_subprocess(
388+
[
389+
"export",
390+
"--frozen",
391+
"--output-file=requirements.txt",
392+
],
393+
change_toml=False,
394+
)
395+
elif backend is BackendEnum.none:
396+
# Simply dump the dependencies list to requirements.txt
397+
if usethis_config.backend is BackendEnum.auto:
398+
info_print(
399+
"Generating 'requirements.txt' with un-pinned, abstract dependencies."
400+
)
401+
info_print(
402+
"Consider installing 'uv' for pinned, cross-platform, full requirements files."
403+
)
404+
405+
write_simple_requirements_txt()
406+
else:
407+
assert_never(backend)
414408

415409
tool.print_how_to_use()
416-
417410
else:
418411
tool.remove_pre_commit_repo_configs()
419412
tool.remove_managed_files()

src/usethis/_init.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from usethis._config import usethis_config
66
from usethis._console import tick_print
7+
from usethis._deps import get_project_deps
78
from usethis._integrations.backend.dispatch import get_backend
89
from usethis._integrations.backend.uv.init import (
910
ensure_pyproject_toml_via_uv,
@@ -64,6 +65,25 @@ def _regularize_package_name(project_name: str) -> str:
6465
return project_name.lower()
6566

6667

68+
def write_simple_requirements_txt() -> None:
69+
r"""Write a simple requirements.txt file with -e . and any project dependencies.
70+
71+
This is used when we don't have a lock file or when using backend=none.
72+
Always writes at least "-e .\n", and appends any dependencies found in
73+
pyproject.toml if they exist.
74+
"""
75+
name = "requirements.txt"
76+
tick_print(f"Writing '{name}'.")
77+
path = usethis_config.cpd() / name
78+
with open(path, "w", encoding="utf-8") as f:
79+
# Always write -e . first
80+
f.write("-e .\n")
81+
# Add any dependencies that exist
82+
project_deps = get_project_deps()
83+
if project_deps:
84+
f.writelines(dep.to_requirement_string() + "\n" for dep in project_deps)
85+
86+
6787
def ensure_dep_declaration_file() -> None:
6888
"""Ensure that the file where dependencies are declared exists, if necessary."""
6989
backend = get_backend()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from usethis._config import usethis_config
2+
from usethis._console import tick_print
3+
from usethis._integrations.backend.uv.call import call_uv_subprocess
4+
5+
6+
def ensure_uv_lock() -> None:
7+
if not (usethis_config.cpd() / "uv.lock").exists():
8+
tick_print("Writing 'uv.lock'.")
9+
call_uv_subprocess(["lock"], change_toml=False)

tests/usethis/_core/test_core_tool.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3208,14 +3208,16 @@ def test_start_from_nothing_uv_backend(
32083208

32093209
# Assert
32103210
assert (tmp_path / "requirements.txt").exists()
3211+
assert not (tmp_path / "pyproject.toml").exists()
3212+
assert not (tmp_path / "uv.lock").exists()
32113213
out, err = capfd.readouterr()
32123214
assert not err
32133215
assert out.replace("\n", "") == (
3214-
"✔ Writing 'pyproject.toml'."
3215-
"✔ Writing 'uv.lock'."
32163216
"✔ Writing 'requirements.txt'."
32173217
"☐ Run 'uv export -o=requirements.txt' to write 'requirements.txt'."
32183218
)
3219+
content = (tmp_path / "requirements.txt").read_text()
3220+
assert content == "-e .\n"
32193221

32203222
def test_start_from_nothing_none_backend(
32213223
self, tmp_path: Path, capfd: pytest.CaptureFixture[str]
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import pytest
5+
6+
import usethis._integrations.backend.uv.lockfile
7+
from usethis._integrations.backend.uv.lockfile import ensure_uv_lock
8+
from usethis._test import change_cwd
9+
10+
11+
class TestEnsureUVLock:
12+
def test_creates_lock_file_when_missing(
13+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
14+
):
15+
# Arrange
16+
(tmp_path / "pyproject.toml").write_text(
17+
"""\
18+
[project]
19+
name = "test"
20+
version = "0.1.0"
21+
dependencies = []
22+
"""
23+
)
24+
25+
called = False
26+
27+
def mock_call_uv_subprocess(args: list[str], *, change_toml: bool) -> str:
28+
nonlocal called
29+
called = True
30+
assert args == ["lock"]
31+
assert change_toml is False
32+
# Create the lock file to simulate uv lock behavior
33+
(tmp_path / "uv.lock").write_text("version = 1\n")
34+
return ""
35+
36+
monkeypatch.setattr(
37+
usethis._integrations.backend.uv.lockfile,
38+
"call_uv_subprocess",
39+
mock_call_uv_subprocess,
40+
)
41+
42+
# Act
43+
with change_cwd(tmp_path):
44+
ensure_uv_lock()
45+
46+
# Assert
47+
assert called
48+
assert (tmp_path / "uv.lock").exists()
49+
50+
def test_prints_message_when_creating(
51+
self,
52+
tmp_path: Path,
53+
monkeypatch: pytest.MonkeyPatch,
54+
capfd: pytest.CaptureFixture[str],
55+
):
56+
# Arrange
57+
(tmp_path / "pyproject.toml").write_text(
58+
"""\
59+
[project]
60+
name = "test"
61+
version = "0.1.0"
62+
dependencies = []
63+
"""
64+
)
65+
66+
def mock_call_uv_subprocess(args: list[str], *, change_toml: bool) -> str:
67+
_ = args, change_toml
68+
(tmp_path / "uv.lock").write_text("version = 1\n")
69+
return ""
70+
71+
monkeypatch.setattr(
72+
usethis._integrations.backend.uv.lockfile,
73+
"call_uv_subprocess",
74+
mock_call_uv_subprocess,
75+
)
76+
77+
# Act
78+
with change_cwd(tmp_path):
79+
ensure_uv_lock()
80+
81+
# Assert
82+
out, err = capfd.readouterr()
83+
assert not err
84+
assert out == "✔ Writing 'uv.lock'.\n"
85+
86+
def test_does_nothing_when_lock_exists(
87+
self,
88+
tmp_path: Path,
89+
monkeypatch: pytest.MonkeyPatch,
90+
capfd: pytest.CaptureFixture[str],
91+
):
92+
# Arrange
93+
(tmp_path / "pyproject.toml").write_text(
94+
"""\
95+
[project]
96+
name = "test"
97+
version = "0.1.0"
98+
dependencies = []
99+
"""
100+
)
101+
(tmp_path / "uv.lock").write_text("version = 1\n")
102+
103+
called = False
104+
105+
def mock_call_uv_subprocess(*_: Any, **__: Any) -> str:
106+
nonlocal called
107+
called = True
108+
return ""
109+
110+
monkeypatch.setattr(
111+
usethis._integrations.backend.uv.lockfile,
112+
"call_uv_subprocess",
113+
mock_call_uv_subprocess,
114+
)
115+
116+
# Act
117+
with change_cwd(tmp_path):
118+
ensure_uv_lock()
119+
120+
# Assert
121+
assert not called
122+
out, err = capfd.readouterr()
123+
assert not err
124+
assert not out

0 commit comments

Comments
 (0)