diff --git a/poetry.lock b/poetry.lock index 1493d2b..a685edd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,20 +100,35 @@ unicode_backport = ["unicodedata2"] [[package]] name = "click" -version = "7.1.2" +version = "8.0.3" description = "Composable command line interface toolkit" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "coverage" version = "6.2" @@ -163,6 +178,14 @@ sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +[[package]] +name = "dataclasses" +version = "0.8" +description = "A backport of the dataclasses module for Python 3.6" +category = "main" +optional = false +python-versions = ">=3.6, <3.7" + [[package]] name = "distlib" version = "0.3.4" @@ -201,7 +224,7 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co [[package]] name = "identify" -version = "2.4.1" +version = "2.4.4" description = "File identification library for Python" category = "dev" optional = false @@ -377,7 +400,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.16.0" +version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -395,14 +418,14 @@ virtualenv = ">=20.0.8" [[package]] name = "pur" -version = "5.4.2" +version = "5.4.3" description = "Update packages in a requirements.txt file to latest versions." category = "dev" optional = false python-versions = "*" [package.dependencies] -click = ">=0.7,<8.0.0" +click = ">=8.0.0" [[package]] name = "py" @@ -422,7 +445,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.11.1" +version = "2.11.2" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false @@ -430,7 +453,7 @@ python-versions = ">=3.5" [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false @@ -518,7 +541,7 @@ md = ["cmarkgfm (>=0.5.0,<0.7.0)"] [[package]] name = "requests" -version = "2.27.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "dev" optional = false @@ -556,6 +579,24 @@ python-versions = "*" [package.extras] idna2008 = ["idna"] +[[package]] +name = "rich" +version = "10.16.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +colorama = ">=0.4.0,<0.5.0" +commonmark = ">=0.9.0,<0.10.0" +dataclasses = {version = ">=0.7,<0.9", markers = "python_version < \"3.7\""} +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=3.7.4,<5.0", markers = "python_version < \"3.8\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "secretstorage" version = "3.3.1" @@ -586,7 +627,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "4.3.2" +version = "4.4.0" description = "Python documentation generator" category = "dev" optional = false @@ -598,6 +639,7 @@ babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.18" imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" @@ -612,7 +654,7 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] @@ -756,13 +798,13 @@ tqdm = ">=4.14" name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -815,8 +857,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" -python-versions = "^3.6.1" -content-hash = "83211e5df89a594a5eab3f3dd78d10d0a8c6ccac60af824c8212a4a500c24d7a" +python-versions = "^3.6.2" +content-hash = "fab14f2faf74c73f079af9f858f97d0ca15f8dee469391ed0415a10d6c754482" [metadata.files] alabaster = [ @@ -908,13 +950,17 @@ charset-normalizer = [ {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] coverage = [ {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, @@ -990,6 +1036,10 @@ cryptography = [ {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, ] +dataclasses = [ + {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, + {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -1006,8 +1056,8 @@ filelock = [ {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"}, ] identify = [ - {file = "identify-2.4.1-py2.py3-none-any.whl", hash = "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c"}, - {file = "identify-2.4.1.tar.gz", hash = "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9"}, + {file = "identify-2.4.4-py2.py3-none-any.whl", hash = "sha256:aa68609c7454dbcaae60a01ff6b8df1de9b39fe6e50b1f6107ec81dcda624aa6"}, + {file = "identify-2.4.4.tar.gz", hash = "sha256:6b4b5031f69c48bf93a646b90de9b381c6b5f560df4cbe0ed3cf7650ae741e4d"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1133,12 +1183,12 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, - {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, ] pur = [ - {file = "pur-5.4.2-py3-none-any.whl", hash = "sha256:20fd0c84864320d403ffce5ee8cf03758b364fef28e7e777f56b944b53787319"}, - {file = "pur-5.4.2.tar.gz", hash = "sha256:6f8531da7664e6fc723ca2d3f712c050e2c5d195a98c83970d2b80ccb930b6b7"}, + {file = "pur-5.4.3-py2-none-any.whl", hash = "sha256:7b81ec2dc9cd30550b9e6d27c677f4d443e7d2f0d07386c50d0721b6d903935d"}, + {file = "pur-5.4.3.tar.gz", hash = "sha256:79a6ceb08a9295a9507b0aacc90c7044a6175c0e1857ecfacde0859a95da7e07"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1149,12 +1199,12 @@ pycparser = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pygments = [ - {file = "Pygments-2.11.1-py3-none-any.whl", hash = "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"}, - {file = "Pygments-2.11.1.tar.gz", hash = "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4"}, + {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"}, + {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1212,8 +1262,8 @@ readme-renderer = [ {file = "readme_renderer-32.0.tar.gz", hash = "sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85"}, ] requests = [ - {file = "requests-2.27.0-py2.py3-none-any.whl", hash = "sha256:f71a09d7feba4a6b64ffd8e9d9bc60f9bf7d7e19fd0e04362acb1cfc2e3d98df"}, - {file = "requests-2.27.0.tar.gz", hash = "sha256:8e5643905bf20a308e25e4c1dd379117c09000bf8a82ebccc462cfb1b34a16b5"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, @@ -1223,6 +1273,10 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +rich = [ + {file = "rich-10.16.2-py3-none-any.whl", hash = "sha256:c59d73bd804c90f747c8d7b1d023b88f2a9ac2454224a4aeaf959b21eeb42d03"}, + {file = "rich-10.16.2.tar.gz", hash = "sha256:720974689960e06c2efdb54327f8bf0cdbdf4eae4ad73b6c94213cad405c371b"}, +] secretstorage = [ {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, @@ -1236,8 +1290,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sphinx = [ - {file = "Sphinx-4.3.2-py3-none-any.whl", hash = "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"}, - {file = "Sphinx-4.3.2.tar.gz", hash = "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c"}, + {file = "Sphinx-4.4.0-py3-none-any.whl", hash = "sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe"}, + {file = "Sphinx-4.4.0.tar.gz", hash = "sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, @@ -1288,8 +1342,8 @@ typing-extensions = [ {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] virtualenv = [ {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, diff --git a/pygount/command.py b/pygount/command.py index 8bb71b4..1e9f192 100644 --- a/pygount/command.py +++ b/pygount/command.py @@ -8,6 +8,8 @@ import os import sys +from rich.progress import Progress + import pygount import pygount.analysis import pygount.common @@ -325,32 +327,29 @@ def execute(self): ) source_paths_and_groups_to_analyze = list(source_scanner.source_paths()) duplicate_pool = pygount.analysis.DuplicatePool() if not self.has_duplicates else None - if self.output == "STDOUT": - target_file = sys.stdout - has_target_file_to_close = False - else: - target_file = open(self.output, "w", encoding="utf-8", newline="") - has_target_file_to_close = True + writer_class = _OUTPUT_FORMAT_TO_WRITER_CLASS_MAP[self.output_format] - try: - writer_class = _OUTPUT_FORMAT_TO_WRITER_CLASS_MAP[self.output_format] - with writer_class(target_file) as writer: - for source_path, group in source_paths_and_groups_to_analyze: - statistics = pygount.analysis.SourceAnalysis.from_file( - source_path, - group, - self.default_encoding, - self.fallback_encoding, - generated_regexes=self._generated_regexs, - duplicate_pool=duplicate_pool, - ) - writer.add(statistics) - finally: - if has_target_file_to_close: + with Progress(transient=True) as progress: + if self.output == "STDOUT": + file_contextmanager = pygount.common.nullcontext(sys.stdout) + else: + file_contextmanager = open(self.output, "w", encoding="utf-8", newline="") + + with file_contextmanager as target_file, writer_class(target_file) as writer: try: - target_file.close() - except Exception as error: - raise OSError('cannot write output to "{0}": {1}'.format(self.output, error)) + for source_path, group in progress.track(source_paths_and_groups_to_analyze): + writer.add( + pygount.analysis.SourceAnalysis.from_file( + source_path, + group, + self.default_encoding, + self.fallback_encoding, + generated_regexes=self._generated_regexs, + duplicate_pool=duplicate_pool, + ) + ) + finally: + progress.stop() def pygount_command(arguments=None): diff --git a/pygount/common.py b/pygount/common.py index bb01a57..e27ad1c 100644 --- a/pygount/common.py +++ b/pygount/common.py @@ -1,12 +1,15 @@ """ Common classes and functions for pygount. """ +import contextlib + # Copyright (c) 2016-2022, Thomas Aglassinger. # All rights reserved. Distributed under the BSD License. import fnmatch import functools import inspect import re +import sys import warnings from typing import Generator, List, Optional, Pattern, Sequence, Union @@ -185,3 +188,29 @@ def new_func2(*args, **kwargs): return new_func2 else: raise TypeError(repr(type(reason))) + + +if sys.version_info < (3, 7): + + class nullcontext(contextlib.AbstractContextManager): # noqa: N801 + """Context manager that does no additional processing. + + Used as a stand-in for a normal context manager, when a particular + block of code is only sometimes used with a normal context manager: + + cm = optional_cm if condition else nullcontext() + with cm: + # Perform operation, using optional_cm if condition is True + """ + + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, *excinfo): + pass + +else: + nullcontext = contextlib.nullcontext diff --git a/pygount/write.py b/pygount/write.py index 9c41763..2353cd1 100644 --- a/pygount/write.py +++ b/pygount/write.py @@ -9,6 +9,9 @@ import os from xml.etree import ElementTree +from rich.console import Console +from rich.table import Table + import pygount from . import SourceAnalysis @@ -135,147 +138,39 @@ class SummaryWriter(BaseWriter): be read by humans. """ - _LANGUAGE_HEADING = "Language" - _FILE_COUNT_HEADING = "Files" - _CODE_HEADING = "Code" - _DOCUMENTATION_HEADING = "Comment" - _SUM_TOTAL_PSEUDO_LANGUAGE = "Sum total" - _PERCENTAGE_DIGITS_AFTER_DOT = 2 - - def __init__(self, target_stream): - super().__init__(target_stream) - self._max_language_width = max( - len(SummaryWriter._LANGUAGE_HEADING), len(SummaryWriter._SUM_TOTAL_PSEUDO_LANGUAGE) - ) - self._max_code_width = 0 - self._max_documentation_width = 0 + _COLUMNS_WITH_JUSTIFY = ( + ("Language", "left"), + ("Files", "right"), + ("Blank", "right"), + ("Comment", "right"), + ("Code", "right"), + ) def close(self): super().close() - # Compute maximum column widths - max_language_width = max( - len(SummaryWriter._LANGUAGE_HEADING), - len(SummaryWriter._SUM_TOTAL_PSEUDO_LANGUAGE), - ) - max_file_count_width = len(SummaryWriter._FILE_COUNT_HEADING) - max_code_width = len(SummaryWriter._CODE_HEADING) - max_documentation_width = len(SummaryWriter._DOCUMENTATION_HEADING) - for language, language_summary in self.project_summary.language_to_language_summary_map.items(): - language_width = len(language) - if language_width > max_language_width: - max_language_width = language_width - file_count_width = digit_width(language_summary.file_count) - if file_count_width > max_file_count_width: - max_file_count_width = file_count_width - code_width = digit_width(language_summary.code_count) - if code_width > max_code_width: - max_code_width = code_width - documentation_width = digit_width(language_summary.documentation_count) - if documentation_width > max_documentation_width: - max_documentation_width = documentation_width - digits_after_dot = self._PERCENTAGE_DIGITS_AFTER_DOT - percentage_width = 4 + digits_after_dot # To represent "100." we need 4 characters. - - summary_heading = ( - "{0:^{max_language_width}s} " - "{1:^{max_file_count_width}s} " - "{2:^{percentage_width}s} " - "{3:^{max_code_width}s} " - "{2:^{percentage_width}s} " - "{4:^{max_documentation_width}s} " - "{2:^{percentage_width}s}" - ).format( - SummaryWriter._LANGUAGE_HEADING, - SummaryWriter._FILE_COUNT_HEADING, - "%", - SummaryWriter._CODE_HEADING, - SummaryWriter._DOCUMENTATION_HEADING, - max_code_width=max_code_width, - max_file_count_width=max_file_count_width, - max_documentation_width=max_documentation_width, - max_language_width=max_language_width, - percentage_width=percentage_width, - ) - self._target_stream.write(summary_heading + os.linesep) - separator_line = " ".join( - [ - "-" * width - for width in ( - max_language_width, - max_file_count_width, - percentage_width, - max_code_width, - percentage_width, - max_documentation_width, - percentage_width, - ) - ] - ) - self._target_stream.write(separator_line + os.linesep) - language_line_template = ( - "{0:{max_language_width}s} " - "{1:>{max_file_count_width}d} " - "{2:>{percentage_width}.0{digits_after_dot}f} " - "{3:>{max_code_width}d} " - "{4:>{percentage_width}.0{digits_after_dot}f} " - "{5:>{max_documentation_width}d} " - "{6:>{percentage_width}.0{digits_after_dot}f}" - ) - for language_summary in sorted(self.project_summary.language_to_language_summary_map.values(), reverse=True): - code_count = language_summary.code_count - code_percentage = ( - code_count / self.project_summary.total_code_count * 100 - if self.project_summary.total_code_count != 0 - else 0.0 - ) - file_count = language_summary.file_count - assert ( - self.project_summary.total_file_count != 0 - ), "if there is at least 1 language summary, there must be a file count too" - file_percentage = file_count / self.project_summary.total_file_count * 100 - documentation_percentage = ( - language_summary.documentation_count / self.project_summary.total_documentation_count * 100 - if self.project_summary.total_documentation_count != 0 - else 0.0 - ) - line_to_write = language_line_template.format( + table = Table() + for column, justify in self._COLUMNS_WITH_JUSTIFY: + table.add_column(column, justify=justify, overflow="fold") + + language_summaries = sorted(self.project_summary.language_to_language_summary_map.values(), reverse=True) + for index, language_summary in enumerate(language_summaries, start=1): + table.add_row( language_summary.language, - file_count, - file_percentage, - code_count, - code_percentage, - language_summary.documentation_count, - documentation_percentage, - digits_after_dot=digits_after_dot, - max_code_width=max_code_width, - max_documentation_width=max_documentation_width, - max_file_count_width=max_file_count_width, - max_language_width=max_language_width, - percentage_width=percentage_width, + str(language_summary.file_count), + str(language_summary.empty_count), + str(language_summary.documentation_count), + str(language_summary.code_count), + end_section=(index == len(language_summaries)), ) - self._target_stream.write(line_to_write + os.linesep) - self._target_stream.write(separator_line + os.linesep) - summary_footer = ( - "{0:{max_language_width}s} " - "{1:>{max_file_count_width}d} " - "{2:{percentage_width}s} " - "{3:>{max_code_width}d} " - "{2:{percentage_width}s} " - "{4:>{max_documentation_width}d}" - ).format( - SummaryWriter._SUM_TOTAL_PSEUDO_LANGUAGE, - self.project_summary.total_file_count, - "", - self.project_summary.total_code_count, - self.project_summary.total_documentation_count, - max_documentation_width=max_documentation_width, - max_file_count_width=max_file_count_width, - max_language_width=max_language_width, - max_code_width=max_code_width, - percentage_width=percentage_width, + table.add_row( + "SUM", + str(self.project_summary.total_file_count), + str(self.project_summary.total_empty_count), + str(self.project_summary.total_documentation_count), + str(self.project_summary.total_code_count), ) - self._target_stream.write(summary_footer + os.linesep) + Console(file=self._target_stream, soft_wrap=True).print(table) class JsonWriter(BaseWriter): diff --git a/pyproject.toml b/pyproject.toml index 8829402..c754c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,9 +63,10 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.6.1" +python = "^3.6.2" pygments = "^2" chardet = "^4.0.0" +rich = "^10.16.2" [tool.poetry.dev-dependencies] wheel = "^0.37.1" diff --git a/tests/test_write.py b/tests/test_write.py index eeaa78c..462e691 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -4,13 +4,11 @@ # Copyright (c) 2016-2022, Thomas Aglassinger. # All rights reserved. Distributed under the BSD License. import io -import os import tempfile from collections import namedtuple +from pathlib import Path from xml.etree import ElementTree -import pytest - from pygount import analysis, write from ._common import TempFolderTest @@ -62,26 +60,22 @@ def test_can_compute_digit_width(): [ "language", "file_count", - "file_percent", + "blank_count", + "comment_count", "code_count", - "code_percent", - "documentation_count", - "documentation_percent", ], ) def _line_data_from(line): - line_parts = line.split() - assert len(line_parts) == 7, "line_parts={0}".format(line_parts) + line_parts = [word for word in line.split() if word.isalnum()] + assert len(line_parts) == 5, f"line_parts={line_parts}" return _LineData( line_parts[0], int(line_parts[1]), - float(line_parts[2]), + int(line_parts[2]), int(line_parts[3]), - float(line_parts[4]), - int(line_parts[5]), - float(line_parts[6]), + int(line_parts[4]), ) @@ -93,60 +87,31 @@ def test_can_write_summary(self): analysis.SourceAnalysis("other.py", "Python", "some", 500, 30, 5, 6, analysis.SourceState.analyzed, None), ) lines = self._summary_lines_for(source_analyses) - assert len(lines) == 6, "lines={}".format(lines) + assert len(lines) == 8, f"lines={lines}" - python_data = _line_data_from(lines[2]) + python_data = _line_data_from(lines[3]) assert python_data.language == "Python" assert python_data.file_count == 2 assert python_data.code_count == 800 - assert python_data.documentation_count == 75 - assert python_data.file_percent == pytest.approx(66.67) - assert python_data.code_percent == pytest.approx(80.0) - assert python_data.documentation_percent == pytest.approx(75.0) + assert python_data.comment_count == 75 - bash_data = _line_data_from(lines[3]) + bash_data = _line_data_from(lines[4]) assert bash_data.language == "Bash" assert bash_data.file_count == 1 assert bash_data.code_count == 200 - assert bash_data.documentation_count == 25 - assert bash_data.file_percent == pytest.approx(33.33) - assert bash_data.code_percent == pytest.approx(20.0) - assert bash_data.documentation_percent == pytest.approx(25.0) + assert bash_data.comment_count == 25 - sum_total_data = lines[-1].split() + sum_total_data = [word.strip() for word in lines[-2].split("│") if word] assert len(sum_total_data) >= 3 - assert sum_total_data[-2:] == ["1000", "100"] + assert sum_total_data[-2:] == ["100", "1000"] def _summary_lines_for(self, source_analyses): # NOTE: We need to write to a file because the lines containing the # actual data are only during close() at which point they would not be # accessible to StringIO.getvalue(). - summary_path = os.path.join(self.tests_temp_folder, "summary.tmp") - with open(summary_path, "w", encoding="utf-8") as target_summary_file: - with write.SummaryWriter(target_summary_file) as writer: + summary_path = Path(self.tests_temp_folder, "summary.tmp") + with summary_path.open("w", encoding="utf-8") as summary_file: + with write.SummaryWriter(summary_file) as writer: for source_analysis in source_analyses: writer.add(source_analysis) - with open(summary_path, "r", encoding="utf-8") as source_summary_file: - result = [line.rstrip("\n") for line in source_summary_file] - return result - - def test_can_format_data_longer_than_headings(self): - huge_number = int("1234567890" * 10) - long_language_name = "x" * 100 - min_line_length = len(long_language_name) + 2 * write.digit_width(huge_number) - - source_analyses = ( - analysis.SourceAnalysis( - "x", - long_language_name, - "some", - huge_number, - huge_number, - 0, - 0, - analysis.SourceState.analyzed, - None, - ), - ) - for line in self._summary_lines_for(source_analyses): - assert len(line) > min_line_length, "line={0}".format(line) + return summary_path.read_text("utf-8").splitlines()