Report lines of code and the main languages from cloc.txt

The project page takes the size from cloc's sum and names the three to
five largest languages, leaving out prose and data files such as Markdown,
text, CSV and JSON, and anything under 1% of the code beyond the third.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alexanderandClaude Opus 5.5 committed 2026-09-24 21:14:14 +02:00
1 parent 9d78e9edf4
commit 2d88ffa953
4 files changed
+121 -15

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
"""Reads the project's size and languages out of the table cloc prints.
cloc writes one row per language -- files, blank, comment and code lines -- and a ``SUM:`` row under
them. Only the code lines are reported: that is what "lines of code" means to a reader, and blanks
and comments say nothing about how much there is to analyse.
"""
from __future__ import annotations
import re
from typing import Any
# How many languages the report names at most, and how many it names at least where there are that
# many. Between the two, a language has to carry :data:`MINIMUM_SHARE` of the code to be named.
MOST_LANGUAGES = 5
FEWEST_LANGUAGES = 3
MINIMUM_SHARE = 0.01
# What cloc counts that is not code anyone analyses: prose, and data files. Configuration and build
# files -- YAML, Dockerfiles, TOML -- are scanned like code and so stay in.
NOT_CODE = frozenset(
{
"AsciiDoc",
"CSV",
"JSON",
"JSON5",
"Markdown",
"reStructuredText",
"SVG",
"Text",
"TSV",
}
)
# A row of the table: a name, which may contain spaces, then the four counts.
_ROW = re.compile(r"^(?P<name>\S.*?)\s+(?P<files>\d+)\s+\d+\s+\d+\s+(?P<code>\d+)\s*$")
def parse_cloc(content: bytes | None) -> dict[str, Any] | None:
"""Read cloc's table.
Args:
content: The table as cloc printed it, or nothing where the scan stored none.
Returns:
The ``files`` and ``code`` lines of the whole project, and its ``languages``
with the same two counts each, largest first; ``None`` where there is no
table to read.
"""
if content is None:
return None
languages = []
total = None
for line in content.decode("utf-8", errors="replace").splitlines():
row = _ROW.match(line.strip())
if row is None:
continue
counts = {"files": int(row["files"]), "code": int(row["code"])}
if row["name"] == "SUM:":
total = counts
else:
languages.append({"name": row["name"], **counts})
if not languages:
return None
# cloc leaves the sum out when it counted a single language.
total = total or {
"files": sum(one["files"] for one in languages),
"code": sum(one["code"] for one in languages),
}
return {**total, "languages": sorted(languages, key=lambda one: -one["code"])}
def main_languages(cloc: dict[str, Any]) -> list[dict[str, Any]]:
"""The languages worth naming: the largest few that are code, leaving out the negligible ones."""
code = [one for one in cloc["languages"] if one["name"] not in NOT_CODE]
counted = sum(one["code"] for one in code) or 1
return [
one
for rank, one in enumerate(code[:MOST_LANGUAGES])
if rank < FEWEST_LANGUAGES or one["code"] / counted >= MINIMUM_SHARE
]