Skip to content

Commit

Permalink
#73 Added percentages to API and cleaned up visual design of summary.
Browse files Browse the repository at this point in the history
- Added `xxx_percentage` for each `xxx_count` in API.
- Cleaned up visual design of summary to better match the previous one.
  - Removed column "Empty".
  - Reordered to "Code" and "Comments".
  - Added percentages.
  • Loading branch information
roskakori committed Apr 9, 2022
1 parent 035a96c commit 13dd7dc
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 50 deletions.
8 changes: 8 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ Changes

This chapter describes the changes coming with each new version of pygount.

Version 1.4.0, 2022-04-09

* Added progress bar during scan phase and improved visual design of
``--format=summary`` (contributed by Stanislav Zmiev, issue.
`#73 <https://github.com/roskakori/pygount/issues/73>`_).
* Added percentages to API. For example in addition to
``code_count`` now there also is ``code_percentage``.

Version 1.3.0, 2022-01-06

* Fixed computation of "lines per second", which was a copy and paste of
Expand Down
89 changes: 83 additions & 6 deletions pygount/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ def __init__(self, language: str):
self._documentation_count = 0
self._empty_count = 0
self._file_count = 0
self._file_percentage = 0.0
self._string_count = 0
self._is_pseudo_language = _PSEUDO_LANGUAGE_REGEX.match(self.language) is not None
self._has_up_to_date_percentages = False

@property
def language(self) -> str:
Expand All @@ -37,31 +39,70 @@ def code_count(self) -> int:
"""sum lines of code for this language"""
return self._code_count

@property
def code_percentage(self) -> float:
"""percentage of lines containing code for this language across entire project"""
return _percentage_or_0(self.code_count, self.line_count)

def _assert_has_up_to_date_percentages(self):
assert self._has_up_to_date_percentages, "update_percentages() must be called first"

@property
def documentation_count(self) -> int:
"""sum lines of documentation for this language"""
return self._documentation_count

@property
def documentation_percentage(self) -> float:
"""percentage of lines containing documentation for this language across entire project"""
return _percentage_or_0(self.documentation_count, self.line_count)

@property
def empty_count(self) -> int:
"""sum empty lines for this language"""
return self._empty_count

@property
def empty_percentage(self) -> float:
"""percentage of empty lines for this language across entire project"""
return _percentage_or_0(self.empty_count, self.line_count)

@property
def file_count(self) -> int:
"""number of source code files for this language"""
return self._file_count

@property
def file_percentage(self) -> float:
"""percentage of files in project"""
self._assert_has_up_to_date_percentages()
return self._file_percentage

@property
def line_count(self) -> int:
"""sum count of all lines of any kind for this language"""
return self.code_count + self.documentation_count + self.empty_count + self.string_count

@property
def string_count(self) -> int:
"""sum number of lines containing only strings for this language"""
"""sum number of lines containing strings for this language"""
return self._string_count

@property
def string_percentage(self) -> float:
"""percentage of lines containing strings for this language across entire project"""
return _percentage_or_0(self.string_count, self.line_count)

@property
def source_count(self) -> int:
"""sum number of source lines of code"""
return self.code_count + self.string_count

@property
def source_percentage(self) -> float:
"""percentage of source lines for code for this language across entire project"""
return _percentage_or_0(self.source_count, self.line_count)

@property
def is_pseudo_language(self) -> bool:
"""``True`` if the language is not a real programming language"""
Expand All @@ -84,13 +125,18 @@ def add(self, source_analysis: SourceAnalysis) -> None:
assert source_analysis is not None
assert source_analysis.language == self.language

self._has_up_to_date_percentages = False
self._file_count += 1
if source_analysis.is_countable:
self._code_count += source_analysis.code_count
self._documentation_count += source_analysis.documentation_count
self._empty_count += source_analysis.empty_count
self._string_count += source_analysis.string_count

def update_file_percentage(self, project_summary: "ProjectSummary"):
self._file_percentage = _percentage_or_0(self.file_count, project_summary.total_file_count)
self._has_up_to_date_percentages = True

def __repr__(self):
result = "{0}(language={1!r}, file_count={2}".format(self.__class__.__name__, self.language, self.file_count)
if not self.is_pseudo_language:
Expand All @@ -101,6 +147,12 @@ def __repr__(self):
return result


def _percentage_or_0(partial_count: int, total_count: int) -> float:
assert partial_count >= 0
assert total_count >= 0
return 100 * partial_count / total_count if total_count != 0 else 0.0


class ProjectSummary:
"""
Summary of source code counts for several languages and files.
Expand All @@ -126,22 +178,42 @@ def language_to_language_summary_map(self) -> Dict[str, LanguageSummary]:
def total_code_count(self) -> int:
return self._total_code_count

@property
def total_code_percentage(self) -> float:
return _percentage_or_0(self.total_code_count, self.total_line_count)

@property
def total_documentation_count(self) -> int:
return self._total_documentation_count

@property
def total_documentation_percentage(self) -> float:
return _percentage_or_0(self.total_documentation_count, self.total_line_count)

@property
def total_empty_count(self) -> int:
return self._total_empty_count

@property
def total_empty_percentage(self) -> float:
return _percentage_or_0(self.total_empty_count, self.total_line_count)

@property
def total_string_count(self) -> int:
return self._total_string_count

@property
def total_string_percentage(self) -> float:
return _percentage_or_0(self.total_string_count, self.total_line_count)

@property
def total_source_count(self) -> int:
return self.total_code_count + self.total_string_count

@property
def total_source_percentage(self) -> float:
return _percentage_or_0(self.total_source_count, self.total_line_count)

@property
def total_file_count(self) -> int:
return self._total_file_count
Expand Down Expand Up @@ -173,10 +245,15 @@ def add(self, source_analysis: SourceAnalysis) -> None:
)
self._total_string_count += source_analysis.string_count

def update_file_percentages(self) -> None:
"""Update percentages for all languages part of the project."""
for language_summary in self._language_to_language_summary_map.values():
language_summary.update_file_percentage(self)

def __repr__(self):
return "{0}(total_file_count={1}, total_line_count={2}, " "languages={3}".format(
self.__class__.__name__,
self.total_file_count,
self.total_line_count,
sorted(self.language_to_language_summary_map.keys()),
return (
f"{self.__class__.__name__}("
f"total_file_count={self.total_file_count}, "
f"total_line_count={self.total_line_count}, "
f"languages={sorted(self.language_to_language_summary_map.keys())})"
)
42 changes: 31 additions & 11 deletions pygount/write.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def add(self, source_analysis):
self.project_summary.add(source_analysis)

def close(self):
self.project_summary.update_file_percentages()
self.finished_at = datetime.datetime.utcnow()
self.duration = self.finished_at - self.started_at
self.duration_in_seconds = (
Expand Down Expand Up @@ -141,9 +142,11 @@ class SummaryWriter(BaseWriter):
_COLUMNS_WITH_JUSTIFY = (
("Language", "left"),
("Files", "right"),
("Blank", "right"),
("Comment", "right"),
("%", "right"),
("Code", "right"),
("%", "right"),
("Comment", "right"),
("%", "right"),
)

def close(self):
Expand All @@ -158,17 +161,21 @@ def close(self):
table.add_row(
language_summary.language,
str(language_summary.file_count),
str(language_summary.empty_count),
str(language_summary.documentation_count),
formatted_percentage(language_summary.file_percentage),
str(language_summary.code_count),
formatted_percentage(language_summary.code_percentage),
str(language_summary.documentation_count),
formatted_percentage(language_summary.documentation_percentage),
end_section=(index == len(language_summaries)),
)
table.add_row(
"SUM",
"Sum",
str(self.project_summary.total_file_count),
str(self.project_summary.total_empty_count),
str(self.project_summary.total_documentation_count),
formatted_percentage(100.0),
str(self.project_summary.total_code_count),
formatted_percentage(self.project_summary.total_code_percentage),
str(self.project_summary.total_documentation_count),
formatted_percentage(self.project_summary.total_documentation_percentage),
)
Console(file=self._target_stream, soft_wrap=True).print(table)

Expand All @@ -186,20 +193,20 @@ def add(self, source_analysis: SourceAnalysis):
super().add(source_analysis)
self.source_analyses.append(
{
"path": source_analysis.path,
"sourceCount": source_analysis.source_count,
"emptyCount": source_analysis.empty_count,
"documentationCount": source_analysis.documentation_count,
"group": source_analysis.group,
"isCountable": source_analysis.is_countable,
"language": source_analysis.language,
"path": source_analysis.path,
"state": source_analysis.state.name,
"stateInfo": source_analysis.state_info,
"sourceCount": source_analysis.source_count,
}
)

def close(self):
# NOTE: We are using camel case for naming here to follow JSLint's guidelines, see <https://www.jslint.com/>.
# NOTE: JSON names use camel case to follow JSLint's guidelines, see <https://www.jslint.com/>.
super().close()
json_map = {
"formatVersion": JSON_FORMAT_VERSION,
Expand All @@ -208,11 +215,15 @@ def close(self):
"languages": [
{
"documentationCount": language_summary.documentation_count,
"documentationPercentage": language_summary.documentation_percentage,
"emptyCount": language_summary.empty_count,
"emptyPercentage": language_summary.empty_percentage,
"fileCount": language_summary.file_count,
"filePercentage": language_summary.file_percentage,
"isPseudoLanguage": language_summary.is_pseudo_language,
"language": language_summary.language,
"sourceCount": language_summary.source_count,
"sourcePercentage": language_summary.source_percentage,
}
for language_summary in self.project_summary.language_to_language_summary_map.values()
],
Expand All @@ -225,14 +236,23 @@ def close(self):
},
"summary": {
"totalDocumentationCount": self.project_summary.total_documentation_count,
"totalDocumentationPercentage": self.project_summary.total_documentation_percentage,
"totalEmptyCount": self.project_summary.total_empty_count,
"totalEmptyPercentage": self.project_summary.total_empty_percentage,
"totalFileCount": self.project_summary.total_file_count,
"totalSourceCount": self.project_summary.total_source_count,
"totalSourcePercentage": self.project_summary.total_source_percentage,
},
}
json.dump(json_map, self._target_stream)


def digit_width(line_count):
def digit_width(line_count: int) -> int:
assert line_count >= 0
return math.ceil(math.log10(line_count + 1)) if line_count != 0 else 1


def formatted_percentage(percentage: float) -> str:
assert percentage >= 0.0
assert percentage <= 100.0
return f"{percentage:.01f}"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ testpaths = [

[tool.poetry]
name = "pygount"
version = "1.3.0"
version = "1.4.0"
description = "count source lines of code (SLOC) using pygments"
readme = "README.md"
authors = ["Thomas Aglassinger <roskakori@users.sourceforge.net>"]
Expand Down
14 changes: 12 additions & 2 deletions tests/test_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ def test_can_summarize_project_with_multiple_files_of_different_languages():
assert project_summary.total_empty_count == 15
assert project_summary.total_string_count == 5

assert (
repr(project_summary)
== "ProjectSummary(total_file_count=2, total_line_count=1340, languages=['Bash', 'Python'])"
)


def test_can_summarize_project_with_pseudo_languages():
source_analyses = (
SourceAnalysis("empty.py", "__empty__", "some", 0, 0, 0, 0, SourceState.empty),
SourceAnalysis("generated.py", "__generated__", "some", 1, 2, 3, 4, SourceState.generated, "generated by test"),
SourceAnalysis("binary .bin", "__binary__", "some", 0, 0, 0, 0, SourceState.binary),
SourceAnalysis("binary.bin", "__binary__", "some", 0, 0, 0, 0, SourceState.binary),
)
expected_languages = {source_analysis.language for source_analysis in source_analyses}

Expand All @@ -81,8 +86,13 @@ def test_can_summarize_project_with_pseudo_languages():
assert project_summary.total_empty_count == 0
assert project_summary.total_string_count == 0

assert repr(project_summary) == (
"ProjectSummary(total_file_count=3, total_line_count=0, "
"languages=['__binary__', '__empty__', '__generated__'])"
)


def test_can_repr_empty_project_summary():
project_summary = ProjectSummary()
assert repr(project_summary) == "ProjectSummary(total_file_count=0, total_line_count=0, languages=[]"
assert repr(project_summary) == "ProjectSummary(total_file_count=0, total_line_count=0, languages=[])"
assert repr(project_summary) == str(project_summary)
Loading

0 comments on commit 13dd7dc

Please sign in to comment.