diff --git a/README.md b/README.md index 5f563d2..ccf8b21 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # usethis +[![PyPI Version]()]() +![PyPI License]() +[![PyPI Supported Versions]()]() +[![uv]()]() +[![Ruff]()]() +[![GitHub Actions Status](https://github.com/nathanjmcdougall/usethis-python/workflows/CI/badge.svg)](https://github.com/nathanjmcdougall/usethis-python/actions) + Automate Python project setup and development tasks that are otherwise performed manually. Inspired by an [**R** package of the same name](https://usethis.r-lib.org/index.html), @@ -84,8 +91,6 @@ Supported arguments: ## Development -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) - This project is at the early stages of development. If you are interested in contributing, please ensure you have a corresponsing GitHub Issue open. diff --git a/doc/logo.svg b/doc/logo.svg new file mode 100644 index 0000000..43a6bda --- /dev/null +++ b/doc/logo.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/usethis/__main__.py b/src/usethis/__main__.py index fd97930..faf4296 100644 --- a/src/usethis/__main__.py +++ b/src/usethis/__main__.py @@ -1,5 +1,6 @@ import typer +import usethis._interface.badge import usethis._interface.browse import usethis._interface.ci import usethis._interface.show @@ -11,10 +12,11 @@ "performed manually." ) ) -app.add_typer(usethis._interface.tool.app, name="tool") +app.add_typer(usethis._interface.badge.app, name="badge") app.add_typer(usethis._interface.browse.app, name="browse") app.add_typer(usethis._interface.ci.app, name="ci") app.add_typer(usethis._interface.show.app, name="show") +app.add_typer(usethis._interface.tool.app, name="tool") app(prog_name="usethis") __all__ = ["app"] diff --git a/src/usethis/_core/badge.py b/src/usethis/_core/badge.py new file mode 100644 index 0000000..61e1a9e --- /dev/null +++ b/src/usethis/_core/badge.py @@ -0,0 +1,123 @@ +import re +from pathlib import Path +from typing import Self + +from pydantic import BaseModel + +from usethis._console import tick_print + + +class Badge(BaseModel): + markdown: str + + @property + def name(self) -> str | None: + match = re.match(r"^\s*\[!\[(.*)\]\(.*\)\]\(.*\)\s*$", self.markdown) + if match: + return match.group(1) + match = re.match(r"^\s*\!\[(.*)\]\(.*\)\s*$", self.markdown) + if match: + return match.group(1) + return None + + def equivalent_to(self, other: Self) -> bool: + return self.name == other.name + + +RUFF_BADGE = Badge( + markdown="[![Ruff]()]()" +) +PRE_COMMIT_BADGE = Badge( + markdown="[![pre-commit]()]()" +) + +BADGE_ORDER = [ + RUFF_BADGE, + PRE_COMMIT_BADGE, +] + + +def add_ruff_badge(): + add_badge(RUFF_BADGE) + + +def add_pre_commit_badge(): + add_badge(PRE_COMMIT_BADGE) + + +def add_badge(badge: Badge) -> None: + path = Path.cwd() / "README.md" + + if not path.exists(): + raise NotImplementedError + + prerequisites: list[Badge] = [] + for _b in BADGE_ORDER: + if badge.equivalent_to(_b): + break + prerequisites.append(_b) + + content = path.read_text() + + original_lines = content.splitlines() + + have_added = False + lines: list[str] = [] + for original_line in original_lines: + original_badge = Badge(markdown=original_line) + + if original_badge.equivalent_to(badge): + # If the badge is already there, we don't need to do anything + return + + original_line_is_prerequisite = any( + original_badge.equivalent_to(prerequisite) for prerequisite in prerequisites + ) + if not have_added and ( + not original_line_is_prerequisite + and not is_blank(original_line) + and not is_header(original_line) + ): + tick_print(f"Adding {badge.name} badge to 'README.md'.") + lines.append(badge.markdown) + have_added = True + + # Protect the badge we've just added + if not is_blank(original_line) and not is_badge(original_line): + lines.append("") + + lines.append(original_line) + + # In case the badge needs to go at the bottom of the file + if not have_added: + # Add a blank line between headers and the badge + if original_lines and is_header(original_lines[-1]): + lines.append("") + tick_print(f"Adding {badge.name} badge to 'README.md'.") + lines.append(badge.markdown) + + # If the first line is blank, we basically just want to replace it. + if is_blank(lines[0]): + del lines[0] + + # Ensure final newline + if lines[-1] != "": + lines.append("") + + path.write_text("\n".join(lines)) + + +def is_blank(line: str) -> bool: + return line.isspace() or not line + + +def is_header(line: str) -> bool: + return line.strip().startswith("#") + + +def is_badge(line: str) -> bool: + # A heuristic + return ( + re.match(r"^\[!\[.*\]\(.*\)\]\(.*\)$", line) is not None + or re.match(r"^\!\[.*\]\(.*\)$", line) is not None + ) diff --git a/src/usethis/_core/tool.py b/src/usethis/_core/tool.py index 20aaa37..4cbbefb 100644 --- a/src/usethis/_core/tool.py +++ b/src/usethis/_core/tool.py @@ -175,11 +175,11 @@ def use_ruff(*, remove: bool = False) -> None: tool.add_pre_commit_repo_configs() box_print( - "Call the 'ruff check --fix' command to run the ruff linter with autofixes." + "Call the 'ruff check --fix' command to run the Ruff linter with autofixes." ) - box_print("Call the 'ruff format' command to run the ruff formatter.") + box_print("Call the 'ruff format' command to run the Ruff formatter.") else: if PreCommitTool().is_used(): tool.remove_pre_commit_repo_configs() - tool.remove_pyproject_configs() # N.B. this will remove the selected ruff rules + tool.remove_pyproject_configs() # N.B. this will remove the selected Ruff rules remove_deps_from_group(tool.dev_deps, "dev") diff --git a/src/usethis/_integrations/bitbucket/schema_utils.py b/src/usethis/_integrations/bitbucket/schema_utils.py index 66f89f6..223ba36 100644 --- a/src/usethis/_integrations/bitbucket/schema_utils.py +++ b/src/usethis/_integrations/bitbucket/schema_utils.py @@ -1,4 +1,4 @@ -from usethis._integrations.bitbucket.schema import Step, Step1, Step2 +from usethis._integrations.bitbucket.schema import Step, Step1 def step1tostep(step1: Step1) -> Step: @@ -14,12 +14,3 @@ def step1tostep(step1: Step1) -> Step: step = Step(**step2.model_dump()) return step - - -def steptostep1(step: Step) -> Step1: - """Demoting Step to a Step1. - - See `step1tostep` for more information. - """ - step1 = Step1(step=Step2(**step.model_dump())) - return step1 diff --git a/src/usethis/_integrations/pre_commit/hooks.py b/src/usethis/_integrations/pre_commit/hooks.py index e757cde..cf64f7c 100644 --- a/src/usethis/_integrations/pre_commit/hooks.py +++ b/src/usethis/_integrations/pre_commit/hooks.py @@ -158,7 +158,7 @@ def remove_hook(name: str) -> None: for hook in repo.hooks: if hook.id == name: tick_print( - f"Removing {hook.id} config from '.pre-commit-config.yaml'." + f"Removing hook '{hook.id}' from '.pre-commit-config.yaml'." ) repo.hooks.remove(hook) diff --git a/src/usethis/_integrations/ruff/rules.py b/src/usethis/_integrations/ruff/rules.py index caed5a7..d633123 100644 --- a/src/usethis/_integrations/ruff/rules.py +++ b/src/usethis/_integrations/ruff/rules.py @@ -7,7 +7,7 @@ def select_ruff_rules(rules: list[str]) -> None: - """Add ruff rules to the project.""" + """Add Ruff rules to the project.""" rules = sorted(set(rules) - set(get_ruff_rules())) if not rules: @@ -15,13 +15,13 @@ def select_ruff_rules(rules: list[str]) -> None: rules_str = ", ".join([f"'{rule}'" for rule in rules]) s = "" if len(rules) == 1 else "s" - tick_print(f"Enabling ruff rule{s} {rules_str} in 'pyproject.toml'.") + tick_print(f"Enabling Ruff rule{s} {rules_str} in 'pyproject.toml'.") append_config_list(["tool", "ruff", "lint", "select"], rules) def ignore_ruff_rules(rules: list[str]) -> None: - """Ignore ruff rules in the project.""" + """Ignore Ruff rules in the project.""" rules = sorted(set(rules) - set(get_ignored_ruff_rules())) if not rules: @@ -29,13 +29,13 @@ def ignore_ruff_rules(rules: list[str]) -> None: rules_str = ", ".join([f"'{rule}'" for rule in rules]) s = "" if len(rules) == 1 else "s" - tick_print(f"Ignoring ruff rule{s} {rules_str} in 'pyproject.toml'.") + tick_print(f"Ignoring Ruff rule{s} {rules_str} in 'pyproject.toml'.") append_config_list(["tool", "ruff", "lint", "ignore"], rules) def deselect_ruff_rules(rules: list[str]) -> None: - """Ensure ruff rules are not selected in the project.""" + """Ensure Ruff rules are not selected in the project.""" rules = list(set(rules) & set(get_ruff_rules())) @@ -44,13 +44,13 @@ def deselect_ruff_rules(rules: list[str]) -> None: rules_str = ", ".join([f"'{rule}'" for rule in rules]) s = "" if len(rules) == 1 else "s" - tick_print(f"Disabling ruff rule{s} {rules_str} in 'pyproject.toml'.") + tick_print(f"Disabling Ruff rule{s} {rules_str} in 'pyproject.toml'.") remove_from_config_list(["tool", "ruff", "lint", "select"], rules) def get_ruff_rules() -> list[str]: - """Get the ruff rules selected in the project.""" + """Get the Ruff rules selected in the project.""" try: rules: list[str] = get_config_value(["tool", "ruff", "lint", "select"]) @@ -61,7 +61,7 @@ def get_ruff_rules() -> list[str]: def get_ignored_ruff_rules() -> list[str]: - """Get the ruff rules ignored in the project.""" + """Get the Ruff rules ignored in the project.""" try: rules: list[str] = get_config_value(["tool", "ruff", "lint", "ignore"]) diff --git a/src/usethis/_integrations/yaml/io.py b/src/usethis/_integrations/yaml/io.py index 3837de2..c36f610 100644 --- a/src/usethis/_integrations/yaml/io.py +++ b/src/usethis/_integrations/yaml/io.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from pathlib import Path from types import NoneType -from typing import TypeAlias, TypeVar +from typing import TypeAlias import ruamel.yaml from ruamel.yaml.comments import ( @@ -32,9 +32,6 @@ from usethis._integrations.yaml.errors import InvalidYAMLError -T = TypeVar("T") - - YAMLLiteral: TypeAlias = ( NoneType | bool diff --git a/src/usethis/_interface/badge.py b/src/usethis/_interface/badge.py new file mode 100644 index 0000000..f9efd35 --- /dev/null +++ b/src/usethis/_interface/badge.py @@ -0,0 +1,26 @@ +import typer + +from usethis._config import offline_opt, quiet_opt, usethis_config +from usethis._core.badge import add_pre_commit_badge, add_ruff_badge + +app = typer.Typer(help="Add badges to the top of the README.md file.") + + +@app.command(help="Add a badge for the Ruff linter.") +def ruff( + *, + offline: bool = offline_opt, + quiet: bool = quiet_opt, +) -> None: + with usethis_config.set(offline=offline, quiet=quiet): + add_ruff_badge() + + +@app.command(help="Add a badge for the pre-commit framework.") +def pre_commit( + *, + offline: bool = offline_opt, + quiet: bool = quiet_opt, +) -> None: + with usethis_config.set(offline=offline, quiet=quiet): + add_pre_commit_badge() diff --git a/src/usethis/_interface/tool.py b/src/usethis/_interface/tool.py index 8e239b9..3dba4b1 100644 --- a/src/usethis/_interface/tool.py +++ b/src/usethis/_interface/tool.py @@ -59,7 +59,7 @@ def pytest( _run_tool(use_pytest, remove=remove) -@app.command(help="Use ruff: an extremely fast Python linter and code formatter.") +@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 ) -> None: diff --git a/src/usethis/_tool.py b/src/usethis/_tool.py index 6958790..4823131 100644 --- a/src/usethis/_tool.py +++ b/src/usethis/_tool.py @@ -51,7 +51,7 @@ def get_pyproject_configs(self) -> list[PyProjectConfig]: return [] def get_associated_ruff_rules(self) -> list[str]: - """Get the ruff rule codes associated with the tool.""" + """Get the Ruff rule codes associated with the tool.""" return [] def get_unique_dev_deps(self) -> list[str]: @@ -280,7 +280,7 @@ def get_managed_files(self): class RuffTool(Tool): @property def name(self) -> str: - return "ruff" + return "Ruff" @property def dev_deps(self) -> list[str]: diff --git a/tests/conftest.py b/tests/conftest.py index 577bfba..ddc5255 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ class NetworkConn(Enum): ], scope="session", ) -def vary_network_conn(request: pytest.FixtureRequest) -> Generator[bool, None, None]: +def _vary_network_conn(request: pytest.FixtureRequest) -> Generator[None, None, None]: """Fixture to vary the network connection; returns True if offline.""" if request.param is NetworkConn.ONLINE and is_offline(): pytest.skip("Network connection is offline") @@ -59,5 +59,5 @@ def vary_network_conn(request: pytest.FixtureRequest) -> Generator[bool, None, N offline = request.param is NetworkConn.OFFLINE usethis_config.offline = offline - yield offline + yield usethis_config.offline = False diff --git a/tests/usethis/_core/test_badge.py b/tests/usethis/_core/test_badge.py new file mode 100644 index 0000000..a32fcc8 --- /dev/null +++ b/tests/usethis/_core/test_badge.py @@ -0,0 +1,342 @@ +from pathlib import Path + +import pytest + +from usethis._core.badge import Badge, add_badge +from usethis._test import change_cwd + + +class TestAddBadge: + def test_empty(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()" + ), + ) + + # Assert + assert ( + path.read_text() + == """\ +![Licence]() +""" + ) + out, err = capfd.readouterr() + assert not err + assert out == "✔ Adding Licence badge to 'README.md'.\n" + + def test_only_newline(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ + +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()" + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +![Licence]() +""" + ) + + def test_predecessor(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +[![Ruff]()]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="[![pre-commit]()]()", + ) + ) + + # Assert + content = path.read_text() + assert ( + content + == """\ +[![Ruff]()]() +[![pre-commit]()]() +""" + ) + + def test_not_predecessor(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +[![pre-commit]()]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="[![Ruff]()]()", + ) + ) + + # Assert + content = path.read_text() + assert ( + content + == """\ +[![Ruff]()]() +[![pre-commit]()]() +""" + ) + + def test_not_recognized_gets_put_after_known_order(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +[![Ruff]()]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Don't Know What This Is]()", + ) + ) + + # Assert + content = path.read_text() + assert ( + content + == """\ +[![Ruff]()]() +![Don't Know What This Is]() +""" + ) + + def test_skip_header1(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +# Header +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +# Header + +![Licence]() +""" + ) + out, err = capfd.readouterr() + assert not err + assert out == "✔ Adding Licence badge to 'README.md'.\n" + + def test_skip_header2(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +## Header +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +## Header + +![Licence]() +""" + ) + + def test_skip_header_with_extra_newline(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +# Header + +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +# Header + +![Licence]() +""" + ) + + def test_extra_unstripped_space(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ + # Header + + [![Ruff]()]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="[![pre-commit]()]()", + ) + ) + + # Assert + content = path.read_text() + assert ( + content + == """\ + # Header + + [![Ruff]()]() +[![pre-commit]()]() +""" + ) + + def test_already_exists(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +![Licence]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +![Licence]() +""" + ) + out, err = capfd.readouterr() + assert not err + assert not out + + def test_badge_followed_by_text(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +# Header + +Some text +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Licence]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +# Header + +![Licence]() + +Some text +""" + ) + + def test_predecessor_based_on_name(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +![Ruff]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![pre-commit]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +![Ruff]() +![pre-commit]() +""" + ) + + def test_recognized_gets_put_before_unknown(self, tmp_path: Path): + # Arrange + path = tmp_path / "README.md" + path.write_text("""\ +![Don't Know What This Is]() +""") + + # Act + with change_cwd(tmp_path): + add_badge( + Badge( + markdown="![Ruff]()", + ) + ) + + # Assert + assert ( + path.read_text() + == """\ +![Ruff]() +![Don't Know What This Is]() +""" + ) diff --git a/tests/usethis/_core/test_tool.py b/tests/usethis/_core/test_tool.py index ddb5dc2..a37310e 100644 --- a/tests/usethis/_core/test_tool.py +++ b/tests/usethis/_core/test_tool.py @@ -41,7 +41,8 @@ def test_subset_hook_names(self): class TestDeptry: class TestAdd: - def test_dependency_added(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_dependency_added(self, uv_init_dir: Path): # Act with change_cwd(uv_init_dir): use_deptry() @@ -50,11 +51,11 @@ def test_dependency_added(self, uv_init_dir: Path, vary_network_conn: None): (dev_dep,) = get_deps_from_group("dev") assert dev_dep == "deptry" + @pytest.mark.usefixtures("_vary_network_conn") def test_stdout( self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str], - vary_network_conn: None, ): # Act with change_cwd(uv_init_dir): @@ -67,7 +68,8 @@ def test_stdout( "☐ Call the 'deptry src' command to run deptry.\n" ) - def test_run_deptry_fail(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_run_deptry_fail(self, uv_init_dir: Path): # Arrange f = uv_init_dir / "bad.py" f.write_text("import broken_dependency") @@ -80,7 +82,8 @@ def test_run_deptry_fail(self, uv_init_dir: Path, vary_network_conn: None): with pytest.raises(subprocess.CalledProcessError): subprocess.run(["deptry", "."], cwd=uv_init_dir, check=True) - def test_run_deptry_pass(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_run_deptry_pass(self, uv_init_dir: Path): # Arrange f = uv_init_dir / "good.py" f.write_text("import sys") @@ -92,11 +95,9 @@ def test_run_deptry_pass(self, uv_init_dir: Path, vary_network_conn: None): # Assert subprocess.run(["deptry", "."], cwd=uv_init_dir, check=True) + @pytest.mark.usefixtures("_vary_network_conn") def test_pre_commit_after( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): # Act with change_cwd(uv_init_repo_dir): @@ -139,7 +140,8 @@ def test_pre_commit_after( ) class TestRemove: - def test_dep(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_dep(self, uv_init_dir: Path): with change_cwd(uv_init_dir): # Arrange add_deps_to_group(["deptry"], "dev") @@ -151,11 +153,9 @@ def test_dep(self, uv_init_dir: Path, vary_network_conn: None): assert not get_deps_from_group("dev") class TestPreCommitIntegration: + @pytest.mark.usefixtures("_vary_network_conn") def test_pre_commit_first( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): """Basically this checks that the placeholders gets removed.""" @@ -198,11 +198,9 @@ def test_pre_commit_first( "☐ Call the 'deptry src' command to run deptry.\n" ) + @pytest.mark.usefixtures("_vary_network_conn") def test_placeholder_removed( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): # Arrange (uv_init_repo_dir / ".pre-commit-config.yaml").write_text( @@ -252,12 +250,8 @@ def test_remove( class TestPreCommit: class TestUse: - def test_fresh( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_fresh(self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]): # Act with change_cwd(uv_init_repo_dir): use_pre_commit() @@ -293,7 +287,8 @@ def test_fresh( """ ) - def test_already_exists(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_already_exists(self, uv_init_repo_dir: Path): # Arrange (uv_init_repo_dir / ".pre-commit-config.yaml").write_text( """\ @@ -319,7 +314,8 @@ def test_already_exists(self, uv_init_repo_dir: Path, vary_network_conn: None): """ ) - def test_bad_commit(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_bad_commit(self, uv_init_repo_dir: Path): # Act with change_cwd(uv_init_repo_dir): use_pre_commit() @@ -339,7 +335,8 @@ def test_bad_commit(self, uv_init_repo_dir: Path, vary_network_conn: None): ) class TestRemove: - def test_config_file(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_config_file(self, uv_init_repo_dir: Path): # Arrange (uv_init_repo_dir / ".pre-commit-config.yaml").touch() @@ -350,7 +347,8 @@ def test_config_file(self, uv_init_repo_dir: Path, vary_network_conn: None): # Assert assert not (uv_init_repo_dir / ".pre-commit-config.yaml").exists() - def test_dep(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_dep(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): # Arrange add_deps_to_group(["pre-commit"], "dev") @@ -467,12 +465,8 @@ def test_remove( class TestPyprojectFmt: class TestAdd: class TestPyproject: - def test_added( - self, - uv_init_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_added(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): # Arrange with change_cwd(uv_init_dir), usethis_config.set(quiet=True): add_deps_to_group(["pyproject-fmt"], "dev") @@ -498,12 +492,8 @@ def test_added( ) class TestDeps: - def test_added( - self, - uv_init_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_added(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): with change_cwd(uv_init_dir): # Act use_pyproject_fmt() @@ -518,7 +508,8 @@ def test_added( ) class TestRemove: - def test_config_file(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_config_file(self, uv_init_dir: Path): # Arrange (uv_init_dir / "pyproject.toml").write_text( """\ @@ -535,11 +526,8 @@ def test_config_file(self, uv_init_dir: Path, vary_network_conn: None): assert (uv_init_dir / "pyproject.toml").read_text() == "" class TestPreCommitIntegration: - def test_use_first( - self, - uv_init_repo_dir: Path, - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_use_first(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): # Arrange use_pre_commit() @@ -553,11 +541,8 @@ def test_use_first( assert (uv_init_repo_dir / ".pre-commit-config.yaml").exists() assert "pyproject-fmt" in hook_names - def test_use_after( - self, - uv_init_repo_dir: Path, - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_use_after(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): # Arrange use_pyproject_fmt() @@ -571,11 +556,9 @@ def test_use_after( assert (uv_init_repo_dir / ".pre-commit-config.yaml").exists() assert "pyproject-fmt" in hook_names + @pytest.mark.usefixtures("_vary_network_conn") def test_remove( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): with change_cwd(uv_init_repo_dir): # Arrange @@ -593,14 +576,15 @@ def test_remove( assert not err assert out == ( "✔ Removing pyproject-fmt config from 'pyproject.toml'.\n" - "✔ Removing pyproject-fmt config from '.pre-commit-config.yaml'.\n" + "✔ Removing hook 'pyproject-fmt' from '.pre-commit-config.yaml'.\n" "✔ Removing 'pyproject-fmt' from the 'dev' dependency group.\n" ) class TestPytest: class TestAdd: - def test_dep(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_dep(self, uv_init_dir: Path): with change_cwd(uv_init_dir): use_pytest() @@ -610,7 +594,8 @@ def test_dep(self, uv_init_dir: Path, vary_network_conn: None): "coverage", } <= set(get_deps_from_group("test")) - def test_bitbucket_integration(self, uv_init_dir: Path, vary_network_conn): + @pytest.mark.usefixtures("_vary_network_conn") + def test_bitbucket_integration(self, uv_init_dir: Path): with change_cwd(uv_init_dir): # Arrange use_ci_bitbucket() @@ -661,7 +646,7 @@ def test_message( # Assert out, _ = capfd.readouterr() - assert out == ("✔ Disabling ruff rule 'PT' in 'pyproject.toml'.\n") + assert out == ("✔ Disabling Ruff rule 'PT' in 'pyproject.toml'.\n") class TestPyproject: def test_removed( @@ -761,7 +746,8 @@ def test_remove(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): class TestRuff: class TestAdd: - def test_dependency_added(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_dependency_added(self, uv_init_dir: Path): # Act with change_cwd(uv_init_dir): use_ruff() @@ -770,12 +756,8 @@ def test_dependency_added(self, uv_init_dir: Path, vary_network_conn: None): (dev_dep,) = get_deps_from_group("dev") assert dev_dep == "ruff" - def test_stdout( - self, - uv_init_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_stdout(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): # Act with change_cwd(uv_init_dir): use_ruff() @@ -784,18 +766,16 @@ def test_stdout( out, _ = capfd.readouterr() assert out == ( "✔ Adding 'ruff' to the 'dev' dependency group.\n" - "✔ Adding ruff config to 'pyproject.toml'.\n" - "✔ Enabling ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'EM', 'F', 'FURB', 'I', \n'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.\n" - "✔ Ignoring ruff rules 'PLR2004', 'SIM108' in 'pyproject.toml'.\n" - "☐ Call the 'ruff check --fix' command to run the ruff linter with autofixes.\n" - "☐ Call the 'ruff format' command to run the ruff formatter.\n" + "✔ Adding Ruff config to 'pyproject.toml'.\n" + "✔ Enabling Ruff rules 'A', 'C4', 'E4', 'E7', 'E9', 'EM', 'F', 'FURB', 'I', \n'PLE', 'PLR', 'RUF', 'SIM', 'UP' in 'pyproject.toml'.\n" + "✔ Ignoring Ruff rules 'PLR2004', 'SIM108' in 'pyproject.toml'.\n" + "☐ Call the 'ruff check --fix' command to run the Ruff linter with autofixes.\n" + "☐ Call the 'ruff format' command to run the Ruff formatter.\n" ) + @pytest.mark.usefixtures("_vary_network_conn") def test_pre_commit_first( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): # Act with change_cwd(uv_init_repo_dir): @@ -809,7 +789,8 @@ def test_pre_commit_first( assert "ruff" in hook_names class TestRemove: - def test_config_file(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_config_file(self, uv_init_dir: Path): # Arrange (uv_init_dir / "pyproject.toml").write_text( """\ @@ -825,7 +806,8 @@ def test_config_file(self, uv_init_dir: Path, vary_network_conn: None): # Assert assert (uv_init_dir / "pyproject.toml").read_text() == "" - def test_blank_slate(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_blank_slate(self, uv_init_dir: Path): # Arrange contents = (uv_init_dir / "pyproject.toml").read_text() @@ -836,7 +818,8 @@ def test_blank_slate(self, uv_init_dir: Path, vary_network_conn: None): # Assert assert (uv_init_dir / "pyproject.toml").read_text() == contents - def test_roundtrip(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_roundtrip(self, uv_init_dir: Path): # Arrange contents = (uv_init_dir / "pyproject.toml").read_text() @@ -858,11 +841,8 @@ def test_roundtrip(self, uv_init_dir: Path, vary_network_conn: None): ) class TestPrecommitIntegration: - def test_use_first( - self, - uv_init_repo_dir: Path, - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_use_first(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): # Arrange use_ruff() @@ -876,11 +856,8 @@ def test_use_first( assert "ruff-format" in hook_names assert "ruff" in hook_names - def test_use_after( - self, - uv_init_repo_dir: Path, - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_use_after(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): # Arrange use_pre_commit() @@ -894,11 +871,9 @@ def test_use_after( assert "ruff-format" in hook_names assert "ruff" in hook_names + @pytest.mark.usefixtures("_vary_network_conn") def test_remove( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, + self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str] ): with change_cwd(uv_init_repo_dir): # Arrange @@ -915,8 +890,8 @@ def test_remove( out, err = capfd.readouterr() assert not err assert out == ( - "✔ Removing ruff-format config from '.pre-commit-config.yaml'.\n" - "✔ Removing ruff config from '.pre-commit-config.yaml'.\n" - "✔ Removing ruff config from 'pyproject.toml'.\n" + "✔ Removing hook 'ruff-format' from '.pre-commit-config.yaml'.\n" + "✔ Removing hook 'ruff' from '.pre-commit-config.yaml'.\n" + "✔ Removing Ruff config from 'pyproject.toml'.\n" "✔ Removing 'ruff' from the 'dev' dependency group.\n" ) diff --git a/tests/usethis/_integrations/pre_commit/test_core.py b/tests/usethis/_integrations/pre_commit/test_core.py index f22b280..913f59b 100644 --- a/tests/usethis/_integrations/pre_commit/test_core.py +++ b/tests/usethis/_integrations/pre_commit/test_core.py @@ -57,12 +57,8 @@ def test_does_not_exist(self, tmp_path: Path): class TestInstallPreCommitHooks: - def test_message( - self, - uv_init_repo_dir: Path, - capfd: pytest.CaptureFixture[str], - vary_network_conn: None, - ): + @pytest.mark.usefixtures("_vary_network_conn") + def test_message(self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str]): # Arrange with change_cwd(uv_init_repo_dir): add_deps_to_group(["pre-commit"], "dev") @@ -85,11 +81,11 @@ def test_err(self, tmp_path: Path): class TestUninstallPreCommitHooks: + @pytest.mark.usefixtures("_vary_network_conn") def test_message_and_file( self, uv_init_repo_dir: Path, capfd: pytest.CaptureFixture[str], - vary_network_conn: None, ): # Arrange with change_cwd(uv_init_repo_dir): diff --git a/tests/usethis/_integrations/ruff/test_rules.py b/tests/usethis/_integrations/ruff/test_rules.py index ab0c52c..c9f3556 100644 --- a/tests/usethis/_integrations/ruff/test_rules.py +++ b/tests/usethis/_integrations/ruff/test_rules.py @@ -27,7 +27,7 @@ def test_message(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): # Assert out, _ = capfd.readouterr() - assert "✔ Enabling ruff rules 'A', 'B', 'C' in 'pyproject.toml" in out + assert "✔ Enabling Ruff rules 'A', 'B', 'C' in 'pyproject.toml" in out def test_blank_slate(self, tmp_path: Path): # Arrange diff --git a/tests/usethis/_integrations/uv/test_deps.py b/tests/usethis/_integrations/uv/test_deps.py index e3f0e3e..0189d62 100644 --- a/tests/usethis/_integrations/uv/test_deps.py +++ b/tests/usethis/_integrations/uv/test_deps.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + from usethis._integrations.uv.deps import ( add_deps_to_group, get_dep_groups, @@ -69,7 +71,8 @@ def test_multiple_groups(self, tmp_path: Path): class TestAddDepsToGroup: - def test_pyproject_changed(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_pyproject_changed(self, uv_init_dir: Path): with change_cwd(uv_init_dir): # Act add_deps_to_group(["pytest"], "test") @@ -79,7 +82,8 @@ def test_pyproject_changed(self, uv_init_dir: Path, vary_network_conn: None): class TestRemoveDepsFromGroup: - def test_pyproject_changed(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_pyproject_changed(self, uv_init_dir: Path): with change_cwd(uv_init_dir): # Arrange add_deps_to_group(["pytest"], "test") @@ -96,7 +100,8 @@ def test_no_group(self, uv_init_dir: Path): with change_cwd(uv_init_dir): assert not is_dep_in_any_group("pytest") - def test_in_group(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_in_group(self, uv_init_dir: Path): # Arrange with change_cwd(uv_init_dir): add_deps_to_group(["pytest"], "test") @@ -107,7 +112,8 @@ def test_in_group(self, uv_init_dir: Path, vary_network_conn: None): # Assert assert result - def test_not_in_group(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_not_in_group(self, uv_init_dir: Path): # Arrange with change_cwd(uv_init_dir): add_deps_to_group(["pytest"], "test") diff --git a/tests/usethis/_interface/test_tool.py b/tests/usethis/_interface/test_tool.py index 6d3098c..80410d7 100644 --- a/tests/usethis/_interface/test_tool.py +++ b/tests/usethis/_interface/test_tool.py @@ -8,7 +8,8 @@ class TestDeptry: - def test_cli(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_cli(self, uv_init_dir: Path): with change_cwd(uv_init_dir): if not usethis_config.offline: call_subprocess(["usethis", "tool", "deptry"]) @@ -17,7 +18,8 @@ def test_cli(self, uv_init_dir: Path, vary_network_conn: None): class TestPreCommit: - def test_cli_pass(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_cli_pass(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): if not usethis_config.offline: call_subprocess(["usethis", "tool", "pre-commit"]) @@ -26,7 +28,8 @@ def test_cli_pass(self, uv_init_repo_dir: Path, vary_network_conn: None): call_subprocess(["uv", "run", "pre-commit", "run", "--all-files"]) - def test_cli_fail(self, uv_init_repo_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_cli_fail(self, uv_init_repo_dir: Path): with change_cwd(uv_init_repo_dir): if not usethis_config.offline: call_subprocess(["usethis", "tool", "pre-commit"]) @@ -44,7 +47,8 @@ def test_cli_fail(self, uv_init_repo_dir: Path, vary_network_conn: None): class TestRuff: - def test_cli(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_cli(self, uv_init_dir: Path): with change_cwd(uv_init_dir): if not usethis_config.offline: call_subprocess(["usethis", "tool", "ruff"]) diff --git a/tests/usethis/test_tool.py b/tests/usethis/test_tool.py index c9039a4..26e9b11 100644 --- a/tests/usethis/test_tool.py +++ b/tests/usethis/test_tool.py @@ -164,7 +164,8 @@ def test_specific(self): ] class TestIsUsed: - def test_some_deps(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_some_deps(self, uv_init_dir: Path): # Arrange tool = MyTool() with change_cwd(uv_init_dir): @@ -176,7 +177,8 @@ def test_some_deps(self, uv_init_dir: Path, vary_network_conn: None): # Assert assert result - def test_non_managed_deps(self, uv_init_dir: Path, vary_network_conn: None): + @pytest.mark.usefixtures("_vary_network_conn") + def test_non_managed_deps(self, uv_init_dir: Path): # Arrange tool = MyTool() with change_cwd(uv_init_dir):