Source code for rude.core.config
"""Configuration from pyproject.toml."""
from __future__ import annotations
import tomllib
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
[docs]
@dataclass
class Config:
"""Rude configuration from [tool.rude]."""
select: list[str] = field(default_factory=list)
ignore: list[str] = field(default_factory=list)
plugins: list[str] = field(default_factory=list)
local_rules: list[str] = field(default_factory=list)
rule_options: dict[str, dict[str, Any]] = field(default_factory=dict)
config_path: Path | None = None
def get_rule_options(self, code: str) -> dict[str, Any]:
return self.rule_options.get(code, {})
[docs]
def resolve_path(self, path: str) -> Path:
"""Resolve a local-rules path relative to the config file.
Raises ValueError if the resolved path escapes the project root.
"""
p = Path(path)
if not self.config_path:
resolved = p.resolve()
cwd = Path.cwd().resolve()
if not resolved.is_relative_to(cwd):
raise ValueError(f"local-rules path {path!r} resolves outside cwd ({cwd})")
return resolved
project_root = self.config_path.parent.resolve()
resolved = p.resolve() if p.is_absolute() else (project_root / p).resolve()
if not resolved.is_relative_to(project_root):
raise ValueError(
f"local-rules path {path!r} resolves outside project root ({project_root})"
)
return resolved
[docs]
def load_config(path: Path | str | None = None) -> Config:
"""Load config from pyproject.toml."""
config_path: Path | None
if path:
config_path = Path(path)
if config_path.is_dir():
config_path = config_path / "pyproject.toml"
else:
config_path = find_config_file()
if not config_path or not config_path.exists():
return Config()
return _load_from_file(config_path)
[docs]
def find_config_file(start: Path | None = None) -> Path | None:
"""Find pyproject.toml with [tool.rude] section."""
current = (start or Path.cwd()).resolve()
while True:
cfg = current / "pyproject.toml"
if cfg.exists():
try:
with open(cfg, "rb") as f:
data = tomllib.load(f)
if "tool" in data and "rude" in data["tool"]:
return cfg
except OSError:
pass
parent = current.parent
if parent == current:
return None
current = parent
def _load_from_file(path: Path) -> Config:
try:
with open(path, "rb") as f:
data = tomllib.load(f)
except (tomllib.TOMLDecodeError, OSError) as e:
warnings.warn(f"Failed to parse {path}: {e}", UserWarning, stacklevel=2)
return Config()
rude = data.get("tool", {}).get("rude", {})
if not rude:
return Config()
rules = rude.pop("rules", {})
def get_list(key: str) -> list[str]:
v = rude.get(key)
if v is None:
return []
return [str(x) for x in (v if isinstance(v, list) else [v])]
return Config(
select=get_list("select"),
ignore=get_list("ignore"),
plugins=get_list("plugins"),
local_rules=get_list("local-rules"),
rule_options=rules if isinstance(rules, dict) else {},
config_path=path,
)
__all__ = ["Config", "find_config_file", "load_config"]