mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
* ci(lint): grandfather any-discipline with a per-file ratchet budget (50% headroom) The any-discipline gate previously failed on any Any-typed value touched on a changed line, which tripped on merely editing a legacy `X | Any` line. Switch it to a per-file budget: `any-discipline-budget.json` records each file's current Any count and a changed file fails only when its count exceeds `baseline + slack` (50% headroom, rounded up). New/unbudgeted files have baseline 0, so they stay airtight, while editing legacy files no longer forces cleaning pre-existing debt. Only changed files are re-type-checked (per-PR cost unchanged); the whole-tree scan to recapture the budget runs under `--update` (`make lint-any-budget-update`). The budget is a one-way ratchet guarded by `budget_ratchet_check.py`, matching the ruff/mypy/basedpyright budgets, and folds into `make lint-budget-update`. Also fixes a RecursionError in `contains_any` (recursive type aliases yield fresh objects per unfold, defeating the id() cycle guard) by walking iteratively with a depth cap, exposed by the whole-tree scan. * chore: make CLAUDE.md more concise * chore: rearrange Makefile * ci(lint): make any-budget --update git-failure-safe; clarify over-budget message all_litellm_py_files now returns None when git is unavailable (mirroring changed_line_map) instead of letting CalledProcessError/FileNotFoundError escape as a raw traceback, and update_budget reports a clean setup error (exit 2) for that case. The list-files dependency is injected so the path is unit-testable without monkeypatching. The over-budget diagnostic now reads "N value(s) total, over budget" so the count isn't misread as the excess over the ceiling. * ci(lint): exempt the file-keyed any-discipline budget from the ratchet's dropped-entry rule budget_ratchet_check treats a vanished budget entry as a loosening (an untracked rule whose ceiling is now unbounded). That holds for the rule-keyed budgets, but the any-discipline budget is keyed by file and its gate treats an absent file as ceiling 0 (the file must be Any-free). Cleaning a file to zero drops its entry on the next --update, so the generic rule flagged that as a regression: a false- positive red on exactly the cleanup the ratchet exists to encourage. Exempt the file-keyed budget from the dropped-entry rule while still catching a raised ceiling.
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
|
|
_MODULE_PATH = (
|
|
Path(__file__).resolve().parents[2] / "scripts" / "check_any_discipline.py"
|
|
)
|
|
_spec = importlib.util.spec_from_file_location("check_any_discipline", _MODULE_PATH)
|
|
mod = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(mod)
|
|
|
|
Violation = mod.Violation
|
|
|
|
|
|
def _v(path="litellm/x.py", line=10, code="LIT009"):
|
|
return Violation(Path(path), line, 0, code, "Any-typed value")
|
|
|
|
|
|
def test_violation_on_a_changed_line_is_in_scope():
|
|
assert mod._in_scope(_v(line=10), {"litellm/x.py": {10, 11}}) is True
|
|
|
|
|
|
def test_violation_on_an_unchanged_line_of_a_changed_file_is_out_of_scope():
|
|
assert mod._in_scope(_v(line=99), {"litellm/x.py": {10, 11}}) is False
|
|
|
|
|
|
def test_whole_new_file_puts_every_line_in_scope():
|
|
assert mod._in_scope(_v(line=99999), {"litellm/x.py": mod.ALL_LINES}) is True
|
|
|
|
|
|
def test_file_absent_from_line_map_is_out_of_scope():
|
|
# Regression: ALL_LINES is a distinct sentinel, so a path missing from the map
|
|
# (line_map.get -> None) is NOT mistaken for "whole file in scope".
|
|
assert mod._in_scope(_v(path="litellm/other.py"), {"litellm/x.py": {1}}) is False
|
|
|
|
|
|
def test_no_line_map_means_no_line_filtering():
|
|
assert mod._in_scope(_v(line=12345), None) is True
|
|
|
|
|
|
def test_build_error_is_always_in_scope():
|
|
assert mod._in_scope(_v(code="LIT000", line=1), {"litellm/x.py": {2}}) is True
|
|
|
|
|
|
# --- per-file Any budget ------------------------------------------------------
|
|
|
|
|
|
def test_slack_is_50_percent_rounded_up():
|
|
assert mod._slack_for(0) == 0
|
|
assert mod._slack_for(1) == 1 # ceil(0.5): even a 1-Any file gets a little room
|
|
assert mod._slack_for(3) == 2 # ceil(1.5)
|
|
assert mod._slack_for(20) == 10
|
|
assert mod._slack_for(5145) == 2573
|
|
|
|
|
|
def test_ceiling_is_baseline_plus_slack():
|
|
assert mod._ceiling({"baseline": 20, "slack": 10}) == 30
|
|
assert mod._ceiling({}) == 0 # an absent/empty entry means a zero ceiling
|
|
|
|
|
|
def test_lit009_counts_groups_by_file_and_ignores_other_codes():
|
|
violations = [
|
|
_v(path="litellm/a.py", line=1, code="LIT009"),
|
|
_v(path="litellm/a.py", line=2, code="LIT009"),
|
|
_v(path="litellm/a.py", line=3, code="LIT005"), # suppression hygiene, not an Any
|
|
_v(path="litellm/b.py", line=1, code="LIT009"),
|
|
_v(path="litellm/c.py", line=0, code="LIT000"), # build error, not an Any
|
|
]
|
|
assert mod.lit009_counts(violations) == {"litellm/a.py": 2, "litellm/b.py": 1}
|
|
|
|
|
|
def test_save_budget_omits_zero_count_files_and_round_trips(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "any-discipline-budget.json")
|
|
mod.save_budget({"litellm/a.py": 20, "litellm/b.py": 0, "litellm/c.py": 1})
|
|
loaded = mod.load_budget()
|
|
assert loaded == {
|
|
"litellm/a.py": {"baseline": 20, "slack": 10},
|
|
"litellm/c.py": {"baseline": 1, "slack": 1},
|
|
}
|
|
assert "litellm/b.py" not in loaded # zero-Any files are never baselined
|
|
|
|
|
|
def test_load_budget_missing_file_is_empty(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "nope.json")
|
|
assert mod.load_budget() == {}
|
|
|
|
|
|
def test_update_budget_reports_setup_error_when_git_is_unavailable():
|
|
# all_litellm_py_files returns None when git can't list files; --update must
|
|
# surface a clean setup error (exit 2), not crash with a raw traceback.
|
|
assert mod.update_budget(list_files=lambda: None) == 2
|