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

56 implement usethis badge ruff #158

Merged
merged 8 commits into from
Jan 11, 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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# usethis

[![PyPI Version](<https://img.shields.io/pypi/v/usethis.svg>)](<https://pypi.python.org/pypi/usethis>)
![PyPI License](<https://img.shields.io/pypi/l/usethis.svg>)
[![PyPI Supported Versions](<https://img.shields.io/pypi/pyversions/usethis.svg>)](<https://pypi.python.org/pypi/usethis>)
[![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>)
[![Ruff](<https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json>)](<https://github.com/astral-sh/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),
Expand Down Expand Up @@ -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.

Expand Down
186 changes: 186 additions & 0 deletions doc/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/usethis/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typer

import usethis._interface.badge
import usethis._interface.browse
import usethis._interface.ci
import usethis._interface.show
Expand All @@ -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"]
123 changes: 123 additions & 0 deletions src/usethis/_core/badge.py
Original file line number Diff line number Diff line change
@@ -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](<https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json>)](<https://github.com/astral-sh/ruff>)"
)
PRE_COMMIT_BADGE = Badge(
markdown="[![pre-commit](<https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit>)](<https://github.com/pre-commit/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
)
6 changes: 3 additions & 3 deletions src/usethis/_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
11 changes: 1 addition & 10 deletions src/usethis/_integrations/bitbucket/schema_utils.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
2 changes: 1 addition & 1 deletion src/usethis/_integrations/pre_commit/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 8 additions & 8 deletions src/usethis/_integrations/ruff/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@


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:
return

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:
return

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()))

Expand All @@ -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"])
Expand All @@ -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"])
Expand Down
5 changes: 1 addition & 4 deletions src/usethis/_integrations/yaml/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -32,9 +32,6 @@

from usethis._integrations.yaml.errors import InvalidYAMLError

T = TypeVar("T")


YAMLLiteral: TypeAlias = (
NoneType
| bool
Expand Down
26 changes: 26 additions & 0 deletions src/usethis/_interface/badge.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion src/usethis/_interface/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/usethis/_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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]:
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ 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")

offline = request.param is NetworkConn.OFFLINE

usethis_config.offline = offline
yield offline
yield
usethis_config.offline = False
Loading
Loading