Skip to content

Commit

Permalink
Implement usethis tool pytest
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanjmcdougall committed Oct 21, 2024
1 parent 871242f commit 71d31d9
Show file tree
Hide file tree
Showing 20 changed files with 886 additions and 394 deletions.
16 changes: 11 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ dev-dependencies = [
"pre-commit>=4.0.1",
]

[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"]


[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"--import-mode=importlib",
]

[tool.coverage.run]
source = ["src"]
omit = ["*/pytest-of-*/*"]
16 changes: 16 additions & 0 deletions src/usethis/_deptry/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from usethis import console
from usethis._pre_commit.hooks import get_hook_entry
from usethis._tool import PreCommitTool


def add_deptry_root_dir() -> None:
if not PreCommitTool().is_used():
return

entry = get_hook_entry()
if not entry.startswith("uv run --frozen deptry"):
console.print(
"☐ Reconfigure deptry in '.pre-commit-config.yaml' to run on the '/tests' directory.",
style="blue",
)
return
Empty file.
112 changes: 4 additions & 108 deletions src/usethis/_pre_commit/core.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import subprocess
from collections import Counter
from pathlib import Path

import ruamel.yaml
from ruamel.yaml.util import load_yaml_guess_indent

from usethis import console
from usethis._github import GitHubTagError, get_github_latest_tag
from usethis._pre_commit.config import PreCommitRepoConfig

_YAML_CONTENTS_TEMPLATE = """
repos:
Expand All @@ -20,20 +15,13 @@
# Manually bump this version when necessary
_VALIDATEPYPROJECT_VERSION = "v0.21"

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


def ensure_pre_commit_config() -> None:
def add_pre_commit_config() -> None:
if (Path.cwd() / ".pre-commit-config.yaml").exists():
# Early exit; the file already exists
return

console.print("✔ Creating .pre-commit-config.yaml file", style="green")
console.print("✔ Writing '.pre-commit-config.yaml'.", style="green")
try:
pkg_version = get_github_latest_tag("abravalheri", "validate-pyproject")
except GitHubTagError:
Expand All @@ -53,100 +41,8 @@ def remove_pre_commit_config() -> None:
(Path.cwd() / ".pre-commit-config.yaml").unlink()


def add_hook(config: PreCommitRepoConfig) -> None:
path = Path.cwd() / ".pre-commit-config.yaml"

with path.open(mode="r") as f:
content, sequence_ind, offset_ind = load_yaml_guess_indent(f)

yaml = ruamel.yaml.YAML(typ="rt")
yaml.indent(mapping=sequence_ind, sequence=sequence_ind, offset=offset_ind)

(hook_config,) = config.hooks
hook_name = hook_config.id

# Get an ordered list of the hooks already in the file
existing_hooks = get_hook_names(path.parent)

if not existing_hooks:
raise NotImplementedError

# Get the precendents, i.e. hooks occuring before the new hook
try:
hook_idx = _HOOK_ORDER.index(hook_name)
except ValueError:
raise NotImplementedError(f"Hook '{hook_name}' not recognized")
precedents = _HOOK_ORDER[:hook_idx]

# Find the last of the precedents in the existing hooks
existings_precedents = [hook for hook in existing_hooks if hook in precedents]
if existings_precedents:
last_precedent = existings_precedents[-1]
else:
# Use the last existing hook
last_precedent = existing_hooks[-1]

# Insert the new hook after the last precedent repo
# Do this by iterating over the repos and hooks, and inserting the new hook after
# the last precedent
new_repos = []
for repo in content["repos"]:
new_repos.append(repo)
for hook in repo["hooks"]:
if hook["id"] == last_precedent:
new_repos.append(config.model_dump(exclude_none=True))
content["repos"] = new_repos

# Dump the new content
yaml.dump(content, path)


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

with path.open(mode="r") as f:
content, sequence_ind, offset_ind = load_yaml_guess_indent(f)

yaml = ruamel.yaml.YAML(typ="rt")
yaml.indent(mapping=sequence_ind, sequence=sequence_ind, offset=offset_ind)

# search across the repos for any hooks with ID equal to name
for repo in content["repos"]:
for hook in repo["hooks"]:
if hook["id"] == name:
repo["hooks"].remove(hook)

# if repo has no hooks, remove it
if not repo["hooks"]:
content["repos"].remove(repo)

yaml.dump(content, path)


def get_hook_names(path: Path) -> list[str]:
yaml = ruamel.yaml.YAML()
with (path / ".pre-commit-config.yaml").open(mode="r") as f:
content = yaml.load(f)

hook_names = []
for repo in content["repos"]:
for hook in repo["hooks"]:
hook_names.append(hook["id"])

# Need to validate there are no duplciates
for name, count in Counter(hook_names).items():
if count > 1:
raise DuplicatedHookNameError(f"Hook name '{name}' is duplicated")

return hook_names


class DuplicatedHookNameError(ValueError):
"""Raised when a hook name is duplicated in a pre-commit configuration file."""


def install_pre_commit() -> None:
console.print("✔ Installing pre-commit hooks", style="green")
console.print("✔ Ensuring pre-commit hooks are installed.", style="green")
subprocess.run(
["uv", "run", "pre-commit", "install"],
check=True,
Expand All @@ -155,7 +51,7 @@ def install_pre_commit() -> None:


def uninstall_pre_commit() -> None:
console.print("✔ Uninstalling pre-commit hooks", style="green")
console.print("✔ Ensuring pre-commit hooks are uninstalled.", style="green")
subprocess.run(
["uv", "run", "pre-commit", "uninstall"],
check=True,
Expand Down
106 changes: 106 additions & 0 deletions src/usethis/_pre_commit/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from collections import Counter
from pathlib import Path

import ruamel.yaml
from ruamel.yaml.util import load_yaml_guess_indent

from usethis._pre_commit.config import PreCommitRepoConfig

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


class DuplicatedHookNameError(ValueError):
"""Raised when a hook name is duplicated in a pre-commit configuration file."""


def add_hook(config: PreCommitRepoConfig) -> None:
path = Path.cwd() / ".pre-commit-config.yaml"

with path.open(mode="r") as f:
content, sequence_ind, offset_ind = load_yaml_guess_indent(f)

yaml = ruamel.yaml.YAML(typ="rt")
yaml.indent(mapping=sequence_ind, sequence=sequence_ind, offset=offset_ind)

(hook_config,) = config.hooks
hook_name = hook_config.id

# Get an ordered list of the hooks already in the file
existing_hooks = get_hook_names(path.parent)

if not existing_hooks:
raise NotImplementedError

# Get the precendents, i.e. hooks occuring before the new hook
try:
hook_idx = _HOOK_ORDER.index(hook_name)
except ValueError:
raise NotImplementedError(f"Hook '{hook_name}' not recognized")
precedents = _HOOK_ORDER[:hook_idx]

# Find the last of the precedents in the existing hooks
existings_precedents = [hook for hook in existing_hooks if hook in precedents]
if existings_precedents:
last_precedent = existings_precedents[-1]
else:
# Use the last existing hook
last_precedent = existing_hooks[-1]

# Insert the new hook after the last precedent repo
# Do this by iterating over the repos and hooks, and inserting the new hook after
# the last precedent
new_repos = []
for repo in content["repos"]:
new_repos.append(repo)
for hook in repo["hooks"]:
if hook["id"] == last_precedent:
new_repos.append(config.model_dump(exclude_none=True))
content["repos"] = new_repos

# Dump the new content
yaml.dump(content, path)


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

with path.open(mode="r") as f:
content, sequence_ind, offset_ind = load_yaml_guess_indent(f)

yaml = ruamel.yaml.YAML(typ="rt")
yaml.indent(mapping=sequence_ind, sequence=sequence_ind, offset=offset_ind)

# search across the repos for any hooks with ID equal to name
for repo in content["repos"]:
for hook in repo["hooks"]:
if hook["id"] == name:
repo["hooks"].remove(hook)

# if repo has no hooks, remove it
if not repo["hooks"]:
content["repos"].remove(repo)

yaml.dump(content, path)


def get_hook_names(path: Path) -> list[str]:
yaml = ruamel.yaml.YAML()
with (path / ".pre-commit-config.yaml").open(mode="r") as f:
content = yaml.load(f)

hook_names = []
for repo in content["repos"]:
for hook in repo["hooks"]:
hook_names.append(hook["id"])

# Need to validate there are no duplciates
for name, count in Counter(hook_names).items():
if count > 1:
raise DuplicatedHookNameError(f"Hook name '{name}' is duplicated")

return hook_names
Empty file.
7 changes: 0 additions & 7 deletions src/usethis/_pyproject/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,3 @@
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
Loading

0 comments on commit 71d31d9

Please sign in to comment.