Skip to content
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

144 something to opt into requirements.txt #205

Merged
merged 4 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from pathlib import Path

from usethis._ci import (
add_bitbucket_pre_commit_step,
is_bitbucket_used,
remove_bitbucket_pre_commit_step,
remove_bitbucket_pytest_steps,
update_bitbucket_pytest_steps,
)
from usethis._console import box_print
from usethis._console import box_print, info_print, tick_print
from usethis._integrations.pre_commit.core import (
install_pre_commit_hooks,
remove_pre_commit_config,
Expand All @@ -18,6 +20,7 @@
ignore_ruff_rules,
select_ruff_rules,
)
from usethis._integrations.uv.call import call_uv_subprocess
from usethis._integrations.uv.deps import add_deps_to_group, remove_deps_from_group
from usethis._integrations.uv.init import ensure_pyproject_toml
from usethis._tool import (
Expand All @@ -26,6 +29,7 @@
PreCommitTool,
PyprojectFmtTool,
PytestTool,
RequirementsTxtTool,
RuffTool,
)

Expand Down Expand Up @@ -57,6 +61,15 @@ def use_pre_commit(*, remove: bool = False) -> None:
for _tool in ALL_TOOLS:
if _tool.is_used():
_tool.add_pre_commit_repo_configs()

if PyprojectFmtTool().is_used():
# We will use pre-commit instead of the dev-dep.
remove_deps_from_group(PyprojectFmtTool().get_unique_dev_deps(), "dev")
use_pyproject_fmt()

if RequirementsTxtTool().is_used():
use_requirements_txt()

if not get_hook_names():
add_placeholder_hook()

Expand Down Expand Up @@ -84,6 +97,9 @@ def use_pre_commit(*, remove: bool = False) -> None:
if PyprojectFmtTool().is_used():
use_pyproject_fmt()

if RequirementsTxtTool().is_used():
use_requirements_txt()


def use_pyproject_fmt(*, remove: bool = False) -> None:
tool = PyprojectFmtTool()
Expand Down Expand Up @@ -148,6 +164,50 @@ def use_pytest(*, remove: bool = False) -> None:
remove_pytest_dir() # Last, since this is a manual step


def use_requirements_txt(*, remove: bool = False) -> None:
tool = RequirementsTxtTool()

path = Path.cwd() / "requirements.txt"

if not remove:
is_pre_commit = PreCommitTool().is_used()

if is_pre_commit:
tool.add_pre_commit_repo_configs()

if not path.exists():
# N.B. this is where a task runner would come in handy, to reduce duplication.
tick_print("Writing 'requirements.txt'.")
call_uv_subprocess(
[
"export",
"--frozen",
"--no-dev",
"--output-file=requirements.txt",
"--quiet",
]
)

if not is_pre_commit:
box_print(
"Call the 'uv export --frozen --no-dev --output-file=requirements.txt --quiet' command to manually export to 'requirements.txt'."
)
info_print(
"You can automate the export of requirements.txt with pre-commit. Try `usethis tool pre-commit`."
)
else:
box_print(
"Call the 'pre-commit run uv-export' command to manually export to 'requirements.txt'."
)
else:
if PreCommitTool().is_used():
tool.remove_pre_commit_repo_configs()

if path.exists() and path.is_file():
tick_print("Removing 'requirements.txt'.")
path.unlink()


def use_ruff(*, remove: bool = False) -> None:
tool = RuffTool()

Expand Down
1 change: 1 addition & 0 deletions src/usethis/_integrations/pre_commit/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

_HOOK_ORDER = [
"validate-pyproject",
"uv-export",
"pyproject-fmt",
"ruff", # ruff followed by ruff-format seems to be the recommended way by Astral
"ruff-format",
Expand Down
12 changes: 12 additions & 0 deletions src/usethis/_interface/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use_pre_commit,
use_pyproject_fmt,
use_pytest,
use_requirements_txt,
use_ruff,
)
from usethis.errors import UsethisError
Expand Down Expand Up @@ -59,6 +60,17 @@ def pytest(
_run_tool(use_pytest, remove=remove)


@app.command(
name="requirements.txt",
help="Use a requirements.txt file exported from the uv lockfile.",
)
def requirements_txt(
remove: bool = remove_opt, offline: bool = offline_opt, quiet: bool = quiet_opt
) -> None:
with usethis_config.set(offline=offline, quiet=quiet):
_run_tool(use_requirements_txt, remove=remove)


@app.command(help="Use Ruff: an extremely fast Python linter and code formatter.")
def ruff(
remove: bool = remove_opt, offline: bool = offline_opt, quiet: bool = quiet_opt
Expand Down
32 changes: 32 additions & 0 deletions src/usethis/_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,37 @@ def get_managed_files(self):
return [Path("tests/conftest.py")]


class RequirementsTxtTool(Tool):
@property
def name(self) -> str:
return "requirements.txt"

@property
def dev_deps(self) -> list[str]:
return []

def get_pre_commit_repos(self) -> list[LocalRepo | UriRepo]:
return [
LocalRepo(
repo="local",
hooks=[
HookDefinition(
id="uv-export",
name="uv-export",
files="^uv\\.lock$",
pass_filenames=False,
entry="uv export --frozen --no-dev --output-file=requirements.txt --quiet",
language=Language("system"),
require_serial=True,
)
],
)
]

def get_managed_files(self) -> list[Path]:
return [Path("requirements.txt")]


class RuffTool(Tool):
@property
def name(self) -> str:
Expand Down Expand Up @@ -359,5 +390,6 @@ def get_managed_files(self):
PreCommitTool(),
PyprojectFmtTool(),
PytestTool(),
RequirementsTxtTool(),
RuffTool(),
]
23 changes: 22 additions & 1 deletion tests/usethis/_core/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ def test_use_after(self, uv_init_repo_dir: Path):
assert "pyproject-fmt" in hook_names

@pytest.mark.usefixtures("_vary_network_conn")
def test_remove(
def test_remove_with_precommit(
self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]
):
with change_cwd(uv_init_repo_dir):
Expand All @@ -592,6 +592,27 @@ def test_remove(
assert out == (
"✔ Removing pyproject-fmt config from 'pyproject.toml'.\n"
"✔ Removing hook 'pyproject-fmt' from '.pre-commit-config.yaml'.\n"
)
# N.B. we don't remove it as a dependency because it's not a dep when
# pre-commit is used.

@pytest.mark.usefixtures("_vary_network_conn")
def test_remove_without_precommit(
self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]
):
with change_cwd(uv_init_repo_dir):
# Arrange
with usethis_config.set(quiet=True):
use_pyproject_fmt()

# Act
use_pyproject_fmt(remove=True)

# Assert
out, err = capfd.readouterr()
assert not err
assert out == (
"✔ Removing pyproject-fmt config from 'pyproject.toml'.\n"
"✔ Removing dependency 'pyproject-fmt' from the 'dev' group in 'pyproject.toml'.\n"
)

Expand Down
Loading