Skip to content

Commit b2cffdf

Browse files
Add initial implementation of extliner package with README, CLI, and line counting functionality
1 parent 8d02979 commit b2cffdf

File tree

9 files changed

+346
-0
lines changed

9 files changed

+346
-0
lines changed

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
Here’s a clean and informative `README.md` for your package **`extliner`**:
2+
3+
---
4+
5+
## 📦 extliner
6+
7+
**extliner** is a lightweight Python package that counts lines in files (with and without empty lines) grouped by file extension — perfect for analyzing codebases or text-heavy directories.
8+
9+
---
10+
11+
### 🚀 Features
12+
13+
* 📂 Recursive directory traversal
14+
* 🔍 Counts:
15+
16+
* Total lines **with** whitespace
17+
* Total lines **excluding** empty lines
18+
* 🎯 Extension-based grouping (`.py`, `.txt`, `NO_EXT`, etc.)
19+
* 🚫 Option to **ignore specific file extensions**
20+
* 📊 Beautiful **tabulated output**
21+
* 🧩 Easily extensible class-based design
22+
* 🧪 CLI support
23+
24+
---
25+
26+
### 📥 Installation
27+
28+
```bash
29+
pip install extliner
30+
```
31+
32+
(Or if using locally during development:)
33+
34+
```bash
35+
git clone https://github.com/extliner/extliner.git
36+
cd extliner
37+
pip install -e .
38+
```
39+
40+
---
41+
42+
### 🧑‍💻 Usage
43+
44+
#### ✅ CLI
45+
46+
```bash
47+
extliner -d <directory_path> --ignore .log .json
48+
```
49+
50+
#### Example
51+
52+
```bash
53+
extliner -d ./myproject --ignore .md .log
54+
```
55+
56+
#### Output
57+
58+
```
59+
+------------+---------------+-------------------+--------------+
60+
| Extension | With Spaces | Without Spaces | % of Total |
61+
+------------+---------------+-------------------+--------------+
62+
| .py | 320 | 280 | 65.31% |
63+
| .txt | 170 | 150 | 34.69% |
64+
+------------+---------------+-------------------+--------------+
65+
```
66+
67+
---
68+
69+
### 🧱 Python API
70+
71+
```python
72+
from linecountx.main import LineCounter
73+
from pathlib import Path
74+
75+
counter = LineCounter(ignore_extensions=[".log", ".json"])
76+
result = counter.count_lines(Path("./your_directory"))
77+
78+
print(counter.to_json(result))
79+
```
80+
81+
---
82+
83+
### ⚙️ Options
84+
85+
| Flag | Description | Example |
86+
| ---------- | ---------------------------- | ------------------------- |
87+
| `-d` | Directory to scan (required) | `-d ./src` |
88+
| `--ignore` | File extensions to ignore | `--ignore .log .md .json` |
89+
90+
91+
### 📄 License
92+
93+
MIT License
94+
95+
---
96+
97+
### 👨‍💻 Author
98+
99+
Made with ❤️ by [Deepak Raj](https://github.com/extliner)
100+

extliner/__init__.py

Whitespace-only changes.

extliner/cli.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import argparse
2+
from pathlib import Path
3+
from tabulate import tabulate
4+
5+
from extliner.main import LineCounter
6+
7+
def main():
8+
parser = argparse.ArgumentParser(description="Count lines in files by extension.")
9+
parser.add_argument(
10+
"-d", "--directory", type=Path, required=True,
11+
help="Directory to count lines in."
12+
)
13+
parser.add_argument(
14+
"--ignore", nargs="*", default=[],
15+
help="List of file extensions to ignore (e.g., .log .json)"
16+
)
17+
18+
args = parser.parse_args()
19+
20+
if not args.directory.is_dir():
21+
print(f"Error: {args.directory} is not a valid directory")
22+
return
23+
24+
counter = LineCounter(ignore_extensions=args.ignore)
25+
result = counter.count_lines(args.directory)
26+
27+
# Remove extensions with 0 lines
28+
result = {
29+
ext: counts for ext, counts in result.items()
30+
if counts["with_spaces"] > 0 or counts["without_spaces"] > 0
31+
}
32+
33+
# Sort result by extension
34+
result = dict(sorted(result.items()))
35+
36+
total_with_spaces = sum(counts["with_spaces"] for counts in result.values())
37+
38+
# sort the result by the number of lines with spaces
39+
result = dict(sorted(result.items(), key=lambda item: item[1]["with_spaces"], reverse=True))
40+
41+
table = []
42+
for ext, counts in result.items():
43+
with_spaces = counts["with_spaces"]
44+
without_spaces = counts["without_spaces"]
45+
percent = (with_spaces / total_with_spaces * 100) if total_with_spaces else 0
46+
table.append([ext, with_spaces, without_spaces, f"{percent:.2f}%"])
47+
48+
print(tabulate(
49+
table,
50+
headers=["Extension", "With Spaces", "Without Spaces", "% of Total"],
51+
tablefmt="grid"
52+
))
53+
54+
55+
if __name__ == "__main__":
56+
main()

extliner/main.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
import json
3+
from pathlib import Path
4+
from collections import defaultdict
5+
from typing import Dict, List, Optional
6+
7+
8+
class LineCounter:
9+
def __init__(self, ignore_extensions: Optional[List[str]] = None):
10+
self.ignore_extensions = set(ignore_extensions or [])
11+
self.with_spaces: Dict[str, int] = defaultdict(int)
12+
self.without_spaces: Dict[str, int] = defaultdict(int)
13+
14+
def count_lines(self, directory: Path) -> Dict[str, Dict[str, int]]:
15+
directory = Path(directory)
16+
if not directory.is_dir():
17+
raise ValueError(f"{directory} is not a valid directory")
18+
19+
for root, _, files in os.walk(directory):
20+
for file in files:
21+
filepath = Path(root) / file
22+
ext = filepath.suffix or "NO_EXT"
23+
24+
if ext in self.ignore_extensions:
25+
continue
26+
27+
try:
28+
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
29+
lines = f.readlines()
30+
self.with_spaces[ext] += len(lines)
31+
self.without_spaces[ext] += sum(1 for line in lines if line.strip())
32+
except Exception as e:
33+
print(f"Error reading {filepath}: {e}")
34+
35+
return self._build_result()
36+
37+
def _build_result(self) -> Dict[str, Dict[str, int]]:
38+
return {
39+
ext: {
40+
"with_spaces": self.with_spaces[ext],
41+
"without_spaces": self.without_spaces[ext],
42+
}
43+
for ext in sorted(set(self.with_spaces) | set(self.without_spaces))
44+
}
45+
46+
def to_json(self, data: Dict) -> str:
47+
return json.dumps(data, indent=2)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tabulate==0.9.0

scripts/release.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
check_command() {
3+
if [ ! -x "$(command -v $1)" ]; then
4+
echo "$1 is not installed"
5+
pip install $1
6+
exit 1
7+
fi
8+
}
9+
10+
check_directory() {
11+
if [ ! -d "$1" ]; then
12+
echo "$1 is not found"
13+
exit 1
14+
fi
15+
}
16+
17+
check_file() {
18+
if [ ! -f "$1" ]; then
19+
echo "$1 is not found"
20+
exit 1
21+
fi
22+
}
23+
24+
# check if the git is installed
25+
check_command git
26+
check_command flake8
27+
check_command twine
28+
check_file setup.py
29+
python setup.py sdist bdist_wheel
30+
check_directory dist
31+
python -m twine upload dist/* --verbose
32+
33+
rm -rf dist
34+
rm -rf build
35+
rm -rf *.egg-info
36+
find . -name "*.pyc" -exec rm -rf {}\;

scripts/test_release.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/bin/bash
2+
check_command() {
3+
if [ ! -x "$(command -v $1)" ]; then
4+
echo "$1 is not installed"
5+
pip install $1
6+
exit 1
7+
fi
8+
}
9+
10+
check_directory() {
11+
if [ ! -d "$1" ]; then
12+
echo "$1 is not found"
13+
exit 1
14+
fi
15+
}
16+
17+
check_file() {
18+
if [ ! -f "$1" ]; then
19+
echo "$1 is not found"
20+
exit 1
21+
fi
22+
}
23+
24+
# check if the git is installed
25+
check_command git
26+
check_command flake8
27+
check_command twine
28+
check_file ./setup.py
29+
python setup.py sdist bdist_wheel
30+
check_directory dist
31+
python -m twine upload --repository testpypi dist/*
32+
33+
rm -rf dist
34+
rm -rf build
35+
rm -rf *.egg-info
36+
find . -name "*.pyc" -exec rm -rf {}\;

setup.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from setuptools import setup, find_packages
2+
3+
# Read the long description from README.md
4+
with open("README.md", "r", encoding="utf-8") as fh:
5+
long_description = fh.read()
6+
7+
# Read the list of requirements from requirements.txt
8+
with open("requirements.txt", "r", encoding="utf-8") as fh:
9+
requirements = fh.read().splitlines()
10+
11+
setup(
12+
name="extliner",
13+
version="0.0.1",
14+
author="Deepak Raj",
15+
author_email="[email protected]",
16+
description=(
17+
"A simple command-line tool to count lines in files by extension, "
18+
),
19+
long_description=long_description,
20+
long_description_content_type="text/markdown",
21+
url="https://github.com/codeperfectplus/extliner",
22+
packages=find_packages(),
23+
include_package_data=True,
24+
install_requires=requirements,
25+
python_requires=">=3.6",
26+
classifiers=[
27+
"Development Status :: 5 - Production/Stable",
28+
"Programming Language :: Python :: 3",
29+
"License :: OSI Approved :: MIT License",
30+
"Operating System :: OS Independent",
31+
"Intended Audience :: Developers"
32+
],
33+
project_urls={
34+
"Documentation": "https://extliner.readthedocs.io/en/latest/",
35+
"Source": "https://github.com/codeperfectplus/extliner",
36+
"Tracker": "https://github.com/codeperfectplus/extliner/issues"
37+
},
38+
entry_points={
39+
"console_scripts": [
40+
"extliner=extliner.cli:main", # Update path if needed
41+
],
42+
},
43+
keywords=[
44+
"line count",
45+
"file analysis",
46+
"command line tool",
47+
"file extension",
48+
"python",
49+
"CLI",
50+
"file processing",
51+
52+
],
53+
license="MIT",
54+
)

test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
from extliner.main import LineCounter
3+
from pprint import pprint
4+
5+
# print("Testing line counting functionality...")
6+
# result = count_lines(os.getcwd())
7+
# print("Result:")
8+
# pprint(result)
9+
10+
counter = LineCounter(ignore_extensions=[".py"])
11+
result = counter.count_lines(os.getcwd())
12+
print("JSON Output:")
13+
14+
print(counter.to_json(result))
15+
16+
print(type(counter.to_json(result)))

0 commit comments

Comments
 (0)