fix: default profile report containers to empty

This commit is contained in:
Abdessamad Derraz committed 2026-10-06 01:39:55 +02:00
1 parent 2a56fccc85
commit fdc7fc8326
2 files changed
+51 -5

No files matched your search

+5 -5
View File
@@ -18,7 +18,7 @@ import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass, field
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
@@ -998,19 +998,19 @@ class EntryReport:
class ProfileReport: class ProfileReport:
name: str name: str
repo: str | None = None repo: str | None = None
repos: list[str] = None repos: list[str] = field(default_factory=list)
pinned_tag: str | None = None pinned_tag: str | None = None
host: str | None = None host: str | None = None
pin: str | None = None pin: str | None = None
pin_origin: str | None = None pin_origin: str | None = None
head: str | None = None head: str | None = None
entries: list[EntryReport] = None entries: list[EntryReport] = field(default_factory=list)
skipped: str | None = None skipped: str | None = None
counts: dict[str, int] = None counts: dict[str, int] = field(default_factory=dict)
# Declared repositories whose forge the tool cannot read. They are named # Declared repositories whose forge the tool cannot read. They are named
# rather than dropped: a `source` on an unknown host used to fall back to # rather than dropped: a `source` on an unknown host used to fall back to
# `upstream` in silence, and a divergence between the two went unseen. # `upstream` in silence, and a divergence between the two went unseen.
unread: list[str] = None unread: list[str] = field(default_factory=list)
def needs_review(self) -> int: def needs_review(self) -> int:
counts = self.counts or {} counts = self.counts or {}
+46
View File
@@ -0,0 +1,46 @@
"""A dataclass field typed as a container defaults to an empty one.
ProfileReport declared `entries: list[EntryReport] = None`: the annotation
promised a list, the public constructor gave None, and format_report raised
TypeError on the first loop.
"""
from __future__ import annotations
import ast
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def _is_dataclass(node: ast.ClassDef) -> bool:
for deco in node.decorator_list:
target = deco.func if isinstance(deco, ast.Call) else deco
name = target.attr if isinstance(target, ast.Attribute) else getattr(target, "id", "")
if name == "dataclass":
return True
return False
class ContainerFieldsAreNotNone(unittest.TestCase):
def test_no_container_annotation_defaults_to_none(self):
for path in sorted([*(REPO_ROOT / "scripts").rglob("*.py"), REPO_ROOT / "install.py"]):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if not (isinstance(node, ast.ClassDef) and _is_dataclass(node)):
continue
for stmt in node.body:
if not isinstance(stmt, ast.AnnAssign) or stmt.value is None:
continue
annotation = ast.unparse(stmt.annotation)
is_none = isinstance(stmt.value, ast.Constant) and stmt.value.value is None
if is_none and annotation.split("[")[0] in ("list", "dict", "set") \
and "None" not in annotation:
with self.subTest(module=path.name, cls=node.name,
field=ast.unparse(stmt.target)):
self.fail(f"{annotation} defaults to None")
if __name__ == "__main__":
unittest.main()