Last active
December 24, 2023 12:52
-
-
Save panicoenlaxbox/17fbeca0f668353d5216569830b60d98 to your computer and use it in GitHub Desktop.
Get number of lines of code, classes and functions from a Python base code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ast | |
from dataclasses import dataclass | |
from functools import reduce | |
from pathlib import Path | |
from types import TracebackType | |
from typing import Self, TextIO | |
@dataclass | |
class Symbols: | |
classes: int = 0 | |
functions: int = 0 | |
class InsightsGenerator: | |
def __init__(self, source_dir: Path): | |
if not source_dir.exists() or not source_dir.is_dir(): | |
raise ValueError(f"{source_dir} is not a valid directory") | |
self._source_dir = source_dir | |
self._output_file_path = source_dir / "index.html" | |
self._f: TextIO | |
def __enter__(self) -> Self: | |
self._f = open(self._output_file_path, "w") | |
return self | |
def __exit__( | |
self, | |
exc_type: type[BaseException] | None, | |
exc_val: BaseException | None, | |
exc_tb: TracebackType | None, | |
) -> None: | |
self._f.close() | |
@staticmethod | |
def _is_dir_excluded(dir_: Path) -> bool: | |
excluded_dirs = ["__pycache__"] | |
return any(dir_.name.endswith(exclude) for exclude in excluded_dirs) | |
@classmethod | |
def _get_symbols(cls, path: Path) -> Symbols: | |
classes = 0 | |
functions = 0 | |
if path.is_file(): | |
return cls._get_symbols_from_file(path) | |
for file in path.glob("**/*.py"): | |
symbols = cls._get_symbols_from_file(file) | |
classes += symbols.classes | |
functions += symbols.functions | |
return Symbols(classes, functions) | |
@staticmethod | |
def _get_symbols_from_file(file_path: Path) -> Symbols: | |
with open(file_path, "r", errors="ignore") as f: | |
tree = ast.parse(f.read()) | |
classes = 0 | |
functions = 0 | |
for node in ast.walk(tree): | |
if isinstance(node, ast.ClassDef): | |
classes += 1 | |
elif isinstance(node, ast.FunctionDef): | |
functions += 1 | |
else: | |
continue | |
return Symbols(classes, functions) | |
@classmethod | |
def _get_kloc(cls, path: Path) -> int: | |
return cls._get_dir_kloc(path) if path.is_dir() else cls._get_file_kloc(path) | |
@staticmethod | |
def _get_file_kloc(file_path: Path) -> int: | |
with open(file_path, "r", errors="ignore") as f: | |
return len( | |
[ | |
line | |
for line in f.readlines() | |
if line.strip() != "" and not line.strip().startswith("#") | |
] | |
) | |
@classmethod | |
def _get_dir_kloc(cls, dir_: Path) -> int: | |
return reduce( | |
lambda x, y: x + y, | |
[cls._get_file_kloc(f) for f in dir_.glob(f"**/*.py")], | |
0, | |
) | |
@classmethod | |
def _skip_path(cls, path: Path) -> bool: | |
return (path.is_file() and not path.suffix == ".py") or ( | |
path.is_dir() and cls._is_dir_excluded(path) | |
) | |
def _to_html(self, path: Path) -> None: | |
for entry in path.iterdir(): | |
if self._skip_path(entry): | |
continue | |
print("Generating insights for", entry) | |
self._f.write( | |
f"""<tr> | |
<td>{('d' if entry.is_dir() else '')}</td> | |
<td><a href='{entry}'>{entry}</a></td> | |
<td>{self._get_kloc(entry)}</td>""" | |
) | |
symbols = self._get_symbols(entry) | |
self._f.write( | |
f""" | |
<td>{symbols.classes}</td> | |
<td>{symbols.functions}</td> | |
</tr>""" | |
) | |
if entry.is_dir(): | |
self._to_html(entry) | |
def generate(self) -> None: | |
style = r""" | |
table { | |
border: 1px solid #1C6EA4; | |
text-align: left; | |
border-collapse: collapse; | |
} | |
table td, table th { | |
border: 1px solid #AAAAAA; | |
padding: 3px 2px; | |
} | |
table tr:nth-child(even) { | |
background: #D0E4F5; | |
} | |
table thead { | |
background: #1C6EA4; | |
background: -moz-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%); | |
background: -webkit-linear-gradient(top, #5592bb 0%, #327cad 66%, #1C6EA4 100%); | |
background: linear-gradient(to bottom, #5592bb 0%, #327cad 66%, #1C6EA4 100%); | |
} | |
table thead th { | |
font-size: 15px; | |
font-weight: bold; | |
color: #FFFFFF; | |
} | |
table tbody td:nth-child(3), table tbody td:nth-child(4), table tbody td:nth-child(5) { | |
text-align: right; | |
} | |
""" | |
self._f.write( | |
f""" | |
<html> | |
<head> | |
<style> | |
{style} | |
</style> | |
</head> | |
<body> | |
<h1>{self._source_dir.name}</h1> | |
<table> | |
<thead> | |
<tr> | |
<th></th> | |
<th>path</th> | |
<th>kloc</th> | |
<th>classes</th> | |
<th>functions</th> | |
</thead> | |
<tbody> | |
<tr> | |
<td>d</td> | |
<td><a href='{self._source_dir}'>{self._source_dir}</a></td> | |
<td>{self._get_dir_kloc(self._source_dir)}</td> | |
""" | |
) | |
symbols = self._get_symbols(self._source_dir) | |
self._f.write( | |
f""" | |
<td>{symbols.classes}</td> | |
<td>{symbols.functions}</td> | |
</tr>""" | |
) | |
self._to_html(self._source_dir) | |
self._f.write( | |
""" | |
</tbody> | |
</table> | |
</body> | |
</html>""" | |
) | |
if __name__ == "__main__": | |
with InsightsGenerator( | |
Path(r"C:\Temp\requests\src\requests") | |
) as insights_generator: | |
insights_generator.generate() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment