"""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\S.*?)\s+(?P\d+)\s+\d+\s+\d+\s+(?P\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 ]