Skip to content
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
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ jobs:
git config --global user.name github-actions[bot]
git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com

- name: Setup uv and handle its cache
uses: hynek/setup-cached-uv@49a39f911c85c6ec0c9aadd5a426ae2761afaba2 # v2.0.0
- name: "Set up uv"
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3.1.7
with:
version: "latest"

- name: "Set up Python"
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
Expand All @@ -31,12 +33,16 @@ jobs:
uv export --resolution ${{ matrix.resolution }} > requirements.txt
uv pip install --system --break-system-packages -r requirements.txt

- name: Run pre-commit
run: |
uv run --frozen pre-commit run --all-files

- name: Run pytest
uses: pavelzw/pytest-action@510c5e90c360a185039bea56ce8b3e7e51a16507 # v2.2.0
with:
custom-arguments: tests
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.12]
python-version: [3.12, 3.13]
resolution: [highest, lowest-direct]
30 changes: 30 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.21
hooks:
- id: validate-pyproject
additional_dependencies: ["validate-pyproject-schema-store[all]"]
- repo: local
hooks:
- id: ruff-format
name: ruff-format
entry: uv run --frozen ruff format
language: system
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: ruff-check
name: ruff-check
entry: uv run --frozen ruff check --fix
language: system
always_run: true
pass_filenames: false
- repo: local
hooks:
- id: deptry
name: deptry
entry: uv run --frozen deptry src
language: system
always_run: true
pass_filenames: false
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Automate Python package and project setup tasks that are otherwise performed man

The current interfaces are being considered:

- `usethis tool` to configure a tool, e.g. `usethis tool ruff`. Adding a tool will install it, as well as add relevant files and/or configuration to use the tool. Tools can interact, for example if you run `usethis tool pytest` it wo;; install `pytest`, add it as a testing dependency, etc. but if you then run `usethis tool ruff` then `usethis` will strategically configure `pytest` with `pytest`-specific linter rules. Also vice-versa - if you already have ruff configured but then run `usethis tool pytest`, then `usethis` will strategically add new ruff configuration to reflect the fact you are now using `pytest`. In this way, `usethis` calls are order-invariant.
- `usethis tool` to configure a tool, e.g. `usethis tool ruff`. Adding a tool will install it, as well as add relevant files and/or configuration to use the tool. Tools can interact, for example if you run `usethis tool pytest` it will install `pytest`, add it as a testing dependency, etc. but if you then run `usethis tool ruff` then `usethis` will strategically configure `pytest` with `pytest`-specific linter rules. Also vice-versa - if you already have ruff configured but then run `usethis tool pytest`, then `usethis` will strategically add new ruff configuration to reflect the fact you are now using `pytest`. In this way, `usethis` calls are order-invariant.
- `usethis badge` to add various badges. Note that you can often get the badge with `usethis tool ... --badge` when available for a tool.
- `usethis browse` to browse something, e.g. `usethis browse pypi ruff` would open the URL to the PyPI page for `ruff` in the browser.
- `usethis license` to choose a license, e.g. `usethis license mit` to use the MIT license.
Expand Down
17 changes: 16 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"gitpython>=3.1.43",
"mergedeep>=1.3.4",
"packaging>=24.1",
"pydantic>=2.9.2",
"requests>=2.32.3",
Expand All @@ -28,4 +28,19 @@ dev-dependencies = [
"pytest-md>=0.2.0",
"pytest-emoji>=0.2.0",
"deptry>=0.20.0",
"pre-commit>=4.0.1",
"ruff>=0.7.0",
"pytest-cov>=5.0.0",
"gitpython>=3.1.43",
]

[tool.coverage.run]
source = ["src"]
omit = ["*/pytest-of-*/*"]

[tool.ruff]
src = ["src"]
line-length = 88

[tool.ruff.lint]
select = ["C4", "E4", "E7", "E9", "F", "FURB", "I", "PLE", "PLR", "RUF", "SIM", "UP"]
1 change: 0 additions & 1 deletion src/usethis/_deptry/core.py

This file was deleted.

10 changes: 5 additions & 5 deletions src/usethis/_git.py → src/usethis/_github.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import requests


class _GitHubTagError(Exception):
class GitHubTagError(Exception):
"""Custom exception for GitHub tag-related errors."""


class _NoTagsFoundError(_GitHubTagError):
class NoGitHubTagsFoundError(GitHubTagError):
"""Custom exception raised when no tags are found."""


def _get_github_latest_tag(owner: str, repo: str) -> str:
def get_github_latest_tag(owner: str, repo: str) -> str:
"""Get the name of the most recent tag on the default branch of a GitHub repository.

Args:
Expand All @@ -32,12 +32,12 @@ def _get_github_latest_tag(owner: str, repo: str) -> str:
response = requests.get(api_url, timeout=1)
response.raise_for_status() # Raise an error for HTTP issues
except requests.exceptions.HTTPError as err:
raise _GitHubTagError(f"Failed to fetch tags from GitHub API: {err}")
raise GitHubTagError(f"Failed to fetch tags from GitHub API: {err}")

tags = response.json()

if not tags:
raise _NoTagsFoundError(f"No tags found for repository '{owner}/{repo}'")
raise NoGitHubTagsFoundError(f"No tags found for repository '{owner}/{repo}'")

# Most recent tag's name
return tags[0]["name"]
18 changes: 11 additions & 7 deletions src/usethis/_pre_commit/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from ruamel.yaml.util import load_yaml_guess_indent

from usethis import console
from usethis._deptry.core import PRE_COMMIT_NAME as DEPTRY_PRE_COMMIT_NAME
from usethis._git import _get_github_latest_tag, _GitHubTagError
from usethis._github import GitHubTagError, get_github_latest_tag
from usethis._pre_commit.config import PreCommitRepoConfig

_YAML_CONTENTS_TEMPLATE = """
Expand All @@ -23,22 +22,29 @@

_HOOK_ORDER = [
"validate-pyproject",
DEPTRY_PRE_COMMIT_NAME,
"ruff-format",
"ruff-check",
"deptry",
]


def make_pre_commit_config() -> None:
console.print("✔ Creating .pre-commit-config.yaml file", style="green")
try:
pkg_version = _get_github_latest_tag("abravalheri", "validate-pyproject")
except _GitHubTagError:
pkg_version = get_github_latest_tag("abravalheri", "validate-pyproject")
except GitHubTagError:
# Fallback to last known working version
pkg_version = _VALIDATEPYPROJECT_VERSION
yaml_contents = _YAML_CONTENTS_TEMPLATE.format(pkg_version=pkg_version)

(Path.cwd() / ".pre-commit-config.yaml").write_text(yaml_contents)


def ensure_pre_commit_config() -> None:
if not (Path.cwd() / ".pre-commit-config.yaml").exists():
make_pre_commit_config()


def delete_hook(name: str) -> None:
path = Path.cwd() / ".pre-commit-config.yaml"

Expand All @@ -62,8 +68,6 @@ def delete_hook(name: str) -> None:


def add_single_hook(config: PreCommitRepoConfig) -> None:
# We should have a canonical sort order for all usethis-supported hooks to decide where to place the section. The main objective with the sort order is to ensure dependency relationships are satisfied. For example, valdiate-pyproject will check if the pyproject.toml is valid - if it isn't then some later tools might fail. It would be better to catch this earlier. A general principle is to move from the simpler hooks to the more complicated. Of course, this order might already be violated, or the config might include unrecognized repos - in any case, we aim to ensure the new tool is configured correctly, so it should be placed after the last of its precedents. This logic can be encoded in the adding function.

path = Path.cwd() / ".pre-commit-config.yaml"

with path.open(mode="r") as f:
Expand Down
15 changes: 15 additions & 0 deletions src/usethis/_pyproject/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Any

from pydantic import BaseModel


class PyProjectConfig(BaseModel):
id_keys: list[str]
main_contents: dict[str, Any]

@property
def contents(self) -> dict[str, Any]:
c = self.main_contents
for key in reversed(self.id_keys):
c = {key: c}
return c
2 changes: 1 addition & 1 deletion src/usethis/_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from typing import Generator


@contextmanager
Expand Down
Loading