projectreadmegen 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- projectreadmegen-1.0.0/LICENSE +21 -0
- projectreadmegen-1.0.0/PKG-INFO +33 -0
- projectreadmegen-1.0.0/pyproject.toml +21 -0
- projectreadmegen-1.0.0/setup.cfg +4 -0
- projectreadmegen-1.0.0/src/__init__.py +0 -0
- projectreadmegen-1.0.0/src/badges.py +72 -0
- projectreadmegen-1.0.0/src/config.py +49 -0
- projectreadmegen-1.0.0/src/detector.py +212 -0
- projectreadmegen-1.0.0/src/generator.py +90 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/PKG-INFO +33 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/SOURCES.txt +18 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/dependency_links.txt +1 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/entry_points.txt +2 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/requires.txt +4 -0
- projectreadmegen-1.0.0/src/projectreadmegen.egg-info/top_level.txt +8 -0
- projectreadmegen-1.0.0/src/scanner.py +150 -0
- projectreadmegen-1.0.0/src/web/app.py +100 -0
- projectreadmegen-1.0.0/tests/test_detector.py +130 -0
- projectreadmegen-1.0.0/tests/test_generator.py +165 -0
- projectreadmegen-1.0.0/tests/test_scanner.py +58 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zenith Open Source Projects
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: projectreadmegen
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Auto-generate README files from folder structure
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Zenith Open Source Projects
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: typer[all]>=0.12
|
|
30
|
+
Requires-Dist: rich>=13
|
|
31
|
+
Requires-Dist: jinja2>=3.1
|
|
32
|
+
Requires-Dist: flask>=3.0
|
|
33
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "projectreadmegen"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Auto-generate README files from folder structure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"typer[all]>=0.12",
|
|
15
|
+
"rich>=13",
|
|
16
|
+
"jinja2>=3.1",
|
|
17
|
+
"flask>=3.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
readmegen = "cli:app"
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# src/badges.py
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def lang_badge(language: str) -> str:
|
|
5
|
+
"""Return a language badge."""
|
|
6
|
+
colors = {
|
|
7
|
+
"Python": "3776AB",
|
|
8
|
+
"JavaScript": "F7DF1E",
|
|
9
|
+
"TypeScript": "3178C6",
|
|
10
|
+
"C++": "00599C",
|
|
11
|
+
"C": "A8B9CC",
|
|
12
|
+
"Rust": "000000",
|
|
13
|
+
"Go": "00ADD8",
|
|
14
|
+
"Java": "007396",
|
|
15
|
+
"HTML/CSS": "E34F26",
|
|
16
|
+
"Shell": "4EAA25",
|
|
17
|
+
"PowerShell": "5391FE",
|
|
18
|
+
}
|
|
19
|
+
color = colors.get(language, "555555")
|
|
20
|
+
label = language.replace("+", "%2B").replace("/", "%2F").replace(" ", "%20")
|
|
21
|
+
return f""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def license_badge(license_id: str) -> str:
|
|
25
|
+
"""Return a license badge."""
|
|
26
|
+
if license_id in ["None", "Unknown", ""]:
|
|
27
|
+
return ""
|
|
28
|
+
label = license_id.replace("-", "--").replace(" ", "%20")
|
|
29
|
+
return f""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def platform_badge(platform: str) -> str:
|
|
33
|
+
"""Return a platform badge (Linux, Windows, Cross-Platform)."""
|
|
34
|
+
colors = {
|
|
35
|
+
"Linux": "FCC624",
|
|
36
|
+
"Windows": "0078D6",
|
|
37
|
+
"Cross-Platform":"brightgreen",
|
|
38
|
+
}
|
|
39
|
+
color = colors.get(platform, "lightgrey")
|
|
40
|
+
return f""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_badge_line(detection: dict) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Build the full badge line for a README from detection results.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
detection (dict): Output from detector.detect_stack().
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: One line of Markdown with all relevant badges space-separated.
|
|
52
|
+
"""
|
|
53
|
+
badges = []
|
|
54
|
+
|
|
55
|
+
lang = detection.get("primary_lang", "")
|
|
56
|
+
if lang and lang != "Unknown":
|
|
57
|
+
badges.append(lang_badge(lang))
|
|
58
|
+
|
|
59
|
+
lic = detection.get("license", "None")
|
|
60
|
+
if lic and lic != "None":
|
|
61
|
+
b = license_badge(lic)
|
|
62
|
+
if b:
|
|
63
|
+
badges.append(b)
|
|
64
|
+
|
|
65
|
+
return " ".join(badges)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
print(lang_badge("Python"))
|
|
70
|
+
print(lang_badge("C++"))
|
|
71
|
+
print(license_badge("MIT"))
|
|
72
|
+
print(build_badge_line({"primary_lang": "TypeScript", "license": "MIT"}))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# src/config.py
|
|
2
|
+
|
|
3
|
+
LANG_PATTERNS = {
|
|
4
|
+
"Python": ["requirements.txt", "setup.py", "pyproject.toml", "*.py", "Pipfile"],
|
|
5
|
+
"JavaScript": ["package.json", "*.js", ".eslintrc", ".babelrc"],
|
|
6
|
+
"TypeScript": ["tsconfig.json", "*.ts", "*.tsx"],
|
|
7
|
+
"C++": ["CMakeLists.txt", "*.cpp", "*.hpp", "Makefile"],
|
|
8
|
+
"C": ["*.c", "*.h", "Makefile"],
|
|
9
|
+
"Rust": ["Cargo.toml", "*.rs"],
|
|
10
|
+
"Go": ["go.mod", "*.go"],
|
|
11
|
+
"Java": ["pom.xml", "build.gradle", "*.java"],
|
|
12
|
+
"HTML/CSS": ["*.html", "*.css"],
|
|
13
|
+
"Shell": ["*.sh", "*.bash"],
|
|
14
|
+
"PowerShell": ["*.ps1"],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
LICENSE_NAMES = {
|
|
18
|
+
"LICENSE": "Unknown",
|
|
19
|
+
"LICENSE.md": "Unknown",
|
|
20
|
+
"LICENSE.txt": "Unknown",
|
|
21
|
+
"LICENSE-MIT": "MIT",
|
|
22
|
+
"LICENSE-APACHE": "Apache-2.0",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SKIP_DIRS = {
|
|
26
|
+
".git", ".svn", "__pycache__", "node_modules",
|
|
27
|
+
".venv", "venv", "env", ".env",
|
|
28
|
+
"dist", "build", ".next", ".nuxt",
|
|
29
|
+
"target", "bin", "obj",
|
|
30
|
+
".idea", ".vscode",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SKIP_FILES = {
|
|
34
|
+
".DS_Store", "Thumbs.db", "desktop.ini",
|
|
35
|
+
"*.pyc", "*.pyo", "*.class", "*.o",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
DEFAULT_CONFIG = {
|
|
39
|
+
"template": "standard",
|
|
40
|
+
"output_file": "README.md",
|
|
41
|
+
"include_tree": True,
|
|
42
|
+
"max_tree_depth": 3,
|
|
43
|
+
"include_badges": True,
|
|
44
|
+
"gemini_enabled": False,
|
|
45
|
+
"author": "",
|
|
46
|
+
"github_username": "",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
TEMPLATES_DIR = "templates"
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# src/detector.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
if __name__ != "__main__":
|
|
7
|
+
from src.config import LANG_PATTERNS, LICENSE_NAMES
|
|
8
|
+
else:
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
10
|
+
from src.config import LANG_PATTERNS, LICENSE_NAMES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect_stack(scan_result: dict) -> dict:
|
|
14
|
+
"""
|
|
15
|
+
Analyze scan result and detect languages, license, and project type.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
scan_result (dict): Output from scanner.scan_directory().
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
dict: {
|
|
22
|
+
"languages": list[str] — detected languages in priority order,
|
|
23
|
+
"primary_lang": str — most likely main language,
|
|
24
|
+
"license": str — license identifier or "None",
|
|
25
|
+
"project_type": str — one of: "library", "cli-tool", "web-app",
|
|
26
|
+
"telegram-bot", "game", "script", "unknown",
|
|
27
|
+
"description_hint": str — one sentence hint for README description,
|
|
28
|
+
"has_tests": bool,
|
|
29
|
+
"has_docs": bool,
|
|
30
|
+
"install_cmd": str — likely install command,
|
|
31
|
+
"run_cmd": str — likely run command,
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
files = scan_result["files"]
|
|
35
|
+
dirs = scan_result["dirs"]
|
|
36
|
+
exts = scan_result["file_extensions"]
|
|
37
|
+
name = scan_result["name"]
|
|
38
|
+
|
|
39
|
+
languages = _detect_languages(files, exts)
|
|
40
|
+
primary = languages[0] if languages else "Unknown"
|
|
41
|
+
license_id = _detect_license(files, scan_result["root"])
|
|
42
|
+
proj_type = _detect_project_type(files, dirs, languages, name)
|
|
43
|
+
desc_hint = _build_description_hint(name, primary, proj_type)
|
|
44
|
+
install_cmd = _detect_install_command(files, primary)
|
|
45
|
+
run_cmd = _detect_run_command(files, dirs, primary, proj_type)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"languages": languages,
|
|
49
|
+
"primary_lang": primary,
|
|
50
|
+
"license": license_id,
|
|
51
|
+
"project_type": proj_type,
|
|
52
|
+
"description_hint": desc_hint,
|
|
53
|
+
"has_tests": any(d in dirs for d in ["tests", "test", "__tests__", "spec"]),
|
|
54
|
+
"has_docs": any(d in dirs for d in ["docs", "doc", "documentation"]),
|
|
55
|
+
"install_cmd": install_cmd,
|
|
56
|
+
"run_cmd": run_cmd,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _detect_languages(files: list, extensions: list) -> list:
|
|
61
|
+
"""
|
|
62
|
+
Detect languages from files and extensions, returns sorted by confidence.
|
|
63
|
+
"""
|
|
64
|
+
scores = {}
|
|
65
|
+
|
|
66
|
+
for lang, patterns in LANG_PATTERNS.items():
|
|
67
|
+
score = 0
|
|
68
|
+
for pattern in patterns:
|
|
69
|
+
if pattern.startswith("*."):
|
|
70
|
+
ext = pattern[1:]
|
|
71
|
+
if ext in extensions:
|
|
72
|
+
score += 2
|
|
73
|
+
else:
|
|
74
|
+
if pattern in files:
|
|
75
|
+
score += 5
|
|
76
|
+
|
|
77
|
+
if score > 0:
|
|
78
|
+
scores[lang] = score
|
|
79
|
+
|
|
80
|
+
return sorted(scores.keys(), key=lambda l: scores[l], reverse=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _detect_license(files: list, root: str) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Detect license type from license files.
|
|
86
|
+
"""
|
|
87
|
+
for fname in files:
|
|
88
|
+
if fname.upper() in [f.upper() for f in LICENSE_NAMES.keys()]:
|
|
89
|
+
try:
|
|
90
|
+
license_path = Path(root) / fname
|
|
91
|
+
with open(license_path, "r", encoding="utf-8") as f:
|
|
92
|
+
first_line = f.readline().upper()
|
|
93
|
+
|
|
94
|
+
if "MIT" in first_line:
|
|
95
|
+
return "MIT"
|
|
96
|
+
elif "APACHE" in first_line:
|
|
97
|
+
return "Apache-2.0"
|
|
98
|
+
elif "GNU GENERAL PUBLIC" in first_line:
|
|
99
|
+
return "GPL-3.0"
|
|
100
|
+
elif "BSD" in first_line:
|
|
101
|
+
return "BSD-3-Clause"
|
|
102
|
+
else:
|
|
103
|
+
return "See LICENSE"
|
|
104
|
+
except (IOError, UnicodeDecodeError):
|
|
105
|
+
return "See LICENSE"
|
|
106
|
+
|
|
107
|
+
return "None"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _detect_project_type(files: list, dirs: list, languages: list, name: str) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Classify project type based on files, dirs, and detected languages.
|
|
113
|
+
"""
|
|
114
|
+
name_lower = name.lower()
|
|
115
|
+
|
|
116
|
+
if any(f in files for f in ["bot.py", "main.py"]) and "Python" in languages:
|
|
117
|
+
if any(kw in name_lower for kw in ["bot", "telegram", "monolith"]):
|
|
118
|
+
return "telegram-bot"
|
|
119
|
+
|
|
120
|
+
if any(f in files for f in ["package.json", "index.html", "tsconfig.json"]):
|
|
121
|
+
if any(d in dirs for d in ["src", "public", "pages", "components"]):
|
|
122
|
+
return "web-app"
|
|
123
|
+
|
|
124
|
+
if any(f in files for f in ["cli.py", "main.py", "setup.py", "pyproject.toml"]):
|
|
125
|
+
if "Python" in languages:
|
|
126
|
+
return "cli-tool"
|
|
127
|
+
|
|
128
|
+
if "CMakeLists.txt" in files or "Makefile" in files:
|
|
129
|
+
if "C++" in languages:
|
|
130
|
+
return "cli-tool"
|
|
131
|
+
|
|
132
|
+
if any(f.endswith(".sh") or f.endswith(".ps1") for f in files):
|
|
133
|
+
return "script"
|
|
134
|
+
|
|
135
|
+
if any(kw in name_lower for kw in ["game", "numsuko", "logic"]):
|
|
136
|
+
return "game"
|
|
137
|
+
|
|
138
|
+
return "unknown"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_description_hint(name: str, lang: str, proj_type: str) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Build a one-sentence description template based on detected metadata.
|
|
144
|
+
"""
|
|
145
|
+
type_phrases = {
|
|
146
|
+
"telegram-bot": f"A Telegram bot built with {lang}",
|
|
147
|
+
"web-app": f"A web application built with {lang}",
|
|
148
|
+
"cli-tool": f"A command-line tool written in {lang}",
|
|
149
|
+
"game": f"An interactive game written in {lang}",
|
|
150
|
+
"script": f"A collection of automation scripts",
|
|
151
|
+
"library": f"A {lang} library",
|
|
152
|
+
"unknown": f"A {lang} project",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
phrase = type_phrases.get(proj_type, f"A {lang} project")
|
|
156
|
+
return f"{phrase} — {name.replace('-', ' ').replace('_', ' ').title()}."
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _detect_install_command(files: list, primary_lang: str) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Return the most likely install command based on detected stack.
|
|
162
|
+
"""
|
|
163
|
+
if "requirements.txt" in files:
|
|
164
|
+
return "pip install -r requirements.txt"
|
|
165
|
+
elif "pyproject.toml" in files:
|
|
166
|
+
return "pip install ."
|
|
167
|
+
elif "package.json" in files:
|
|
168
|
+
return "npm install"
|
|
169
|
+
elif "Cargo.toml" in files:
|
|
170
|
+
return "cargo build"
|
|
171
|
+
elif "CMakeLists.txt" in files:
|
|
172
|
+
return "cmake -B build && cmake --build build"
|
|
173
|
+
elif "Makefile" in files:
|
|
174
|
+
return "make"
|
|
175
|
+
else:
|
|
176
|
+
return "# See setup instructions below"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _detect_run_command(files: list, dirs: list, primary_lang: str, proj_type: str) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Return the most likely run command.
|
|
182
|
+
"""
|
|
183
|
+
if proj_type == "cli-tool" and "cli.py" in files:
|
|
184
|
+
return "python cli.py"
|
|
185
|
+
elif proj_type == "cli-tool" and "main.py" in files:
|
|
186
|
+
return "python main.py"
|
|
187
|
+
elif proj_type == "telegram-bot":
|
|
188
|
+
return "python bot.py"
|
|
189
|
+
elif proj_type == "web-app" and "package.json" in files:
|
|
190
|
+
return "npm run dev"
|
|
191
|
+
elif "CMakeLists.txt" in files:
|
|
192
|
+
return "./build/projectname"
|
|
193
|
+
elif "Makefile" in files:
|
|
194
|
+
return "make run"
|
|
195
|
+
else:
|
|
196
|
+
return "# See usage section below"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
import sys
|
|
201
|
+
from src.scanner import scan_directory
|
|
202
|
+
|
|
203
|
+
path = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
204
|
+
scan = scan_directory(path, max_depth=3)
|
|
205
|
+
detec = detect_stack(scan)
|
|
206
|
+
|
|
207
|
+
print(f"Languages : {detec['languages']}")
|
|
208
|
+
print(f"Primary : {detec['primary_lang']}")
|
|
209
|
+
print(f"Type : {detec['project_type']}")
|
|
210
|
+
print(f"License : {detec['license']}")
|
|
211
|
+
print(f"Install : {detec['install_cmd']}")
|
|
212
|
+
print(f"Run : {detec['run_cmd']}")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# src/generator.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
6
|
+
|
|
7
|
+
if __name__ != "__main__":
|
|
8
|
+
from src.badges import build_badge_line
|
|
9
|
+
else:
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
11
|
+
from src.badges import build_badge_line
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_readme(scan_result: dict, detection: dict, config: dict) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Generate a complete README.md string from scan + detection data.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
scan_result (dict): Output from scanner.scan_directory().
|
|
20
|
+
detection (dict): Output from detector.detect_stack().
|
|
21
|
+
config (dict): Merged config from scanner.load_config().
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str: Full README.md content as a string.
|
|
25
|
+
"""
|
|
26
|
+
template_name = config.get("template", "standard")
|
|
27
|
+
template_file = f"{template_name}.md.j2"
|
|
28
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
29
|
+
|
|
30
|
+
if not (templates_dir / template_file).exists():
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
f"Template '{template_file}' not found in {templates_dir}. "
|
|
33
|
+
f"Available: minimal, standard, full, academic"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
env = Environment(
|
|
37
|
+
loader=FileSystemLoader(str(templates_dir)),
|
|
38
|
+
autoescape=select_autoescape([]),
|
|
39
|
+
trim_blocks=True,
|
|
40
|
+
lstrip_blocks=True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
template = env.get_template(template_file)
|
|
44
|
+
|
|
45
|
+
context = {
|
|
46
|
+
"project_name": _format_title(scan_result["name"]),
|
|
47
|
+
"project_raw_name": scan_result["name"],
|
|
48
|
+
"description": detection["description_hint"],
|
|
49
|
+
"author": config.get("author", ""),
|
|
50
|
+
"github_username": config.get("github_username", ""),
|
|
51
|
+
|
|
52
|
+
"badge_line": build_badge_line(detection) if config.get("include_badges") else "",
|
|
53
|
+
|
|
54
|
+
"primary_lang": detection["primary_lang"],
|
|
55
|
+
"all_languages": detection["languages"],
|
|
56
|
+
"project_type": detection["project_type"],
|
|
57
|
+
|
|
58
|
+
"install_cmd": detection["install_cmd"],
|
|
59
|
+
"run_cmd": detection["run_cmd"],
|
|
60
|
+
|
|
61
|
+
"folder_tree": scan_result["tree"] if config.get("include_tree") else "",
|
|
62
|
+
|
|
63
|
+
"has_license": scan_result["has_license"],
|
|
64
|
+
"has_contributing": scan_result["has_contributing"],
|
|
65
|
+
"has_tests": detection["has_tests"],
|
|
66
|
+
"has_docs": detection["has_docs"],
|
|
67
|
+
"license_id": detection["license"],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return template.render(**context)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_readme(content: str, output_path: str) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Write the generated README content to a file.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
content (str): The full README Markdown string.
|
|
79
|
+
output_path (str): Full path where README.md should be written.
|
|
80
|
+
"""
|
|
81
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
82
|
+
f.write(content)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _format_title(raw_name: str) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Convert a folder name like 'projectreadmegen' or 'my-project'
|
|
88
|
+
into a display title like 'Projectreadmegen' or 'My Project'.
|
|
89
|
+
"""
|
|
90
|
+
return raw_name.replace("-", " ").replace("_", " ").title()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: projectreadmegen
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Auto-generate README files from folder structure
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Zenith Open Source Projects
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: typer[all]>=0.12
|
|
30
|
+
Requires-Dist: rich>=13
|
|
31
|
+
Requires-Dist: jinja2>=3.1
|
|
32
|
+
Requires-Dist: flask>=3.0
|
|
33
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/__init__.py
|
|
4
|
+
src/badges.py
|
|
5
|
+
src/config.py
|
|
6
|
+
src/detector.py
|
|
7
|
+
src/generator.py
|
|
8
|
+
src/scanner.py
|
|
9
|
+
src/projectreadmegen.egg-info/PKG-INFO
|
|
10
|
+
src/projectreadmegen.egg-info/SOURCES.txt
|
|
11
|
+
src/projectreadmegen.egg-info/dependency_links.txt
|
|
12
|
+
src/projectreadmegen.egg-info/entry_points.txt
|
|
13
|
+
src/projectreadmegen.egg-info/requires.txt
|
|
14
|
+
src/projectreadmegen.egg-info/top_level.txt
|
|
15
|
+
src/web/app.py
|
|
16
|
+
tests/test_detector.py
|
|
17
|
+
tests/test_generator.py
|
|
18
|
+
tests/test_scanner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# src/scanner.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
if __name__ != "__main__":
|
|
9
|
+
from src.config import SKIP_DIRS, SKIP_FILES, DEFAULT_CONFIG
|
|
10
|
+
else:
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
12
|
+
from src.config import SKIP_DIRS, SKIP_FILES, DEFAULT_CONFIG
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def scan_directory(root_path: str, max_depth: int | None = None) -> dict:
|
|
16
|
+
"""
|
|
17
|
+
Walk a directory and collect metadata about its structure.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
root_path (str): Absolute or relative path to the project root.
|
|
21
|
+
max_depth (int): Maximum depth to traverse. None = unlimited.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict: {
|
|
25
|
+
"root": str — absolute path,
|
|
26
|
+
"name": str — folder name (used as project title),
|
|
27
|
+
"files": list[str] — all file names found (not paths),
|
|
28
|
+
"dirs": list[str] — all directory names found,
|
|
29
|
+
"tree": str — ASCII tree representation,
|
|
30
|
+
"has_license": bool,
|
|
31
|
+
"has_contributing": bool,
|
|
32
|
+
"has_gitignore": bool,
|
|
33
|
+
"has_existing_readme": bool,
|
|
34
|
+
"file_extensions": list[str] — unique extensions found,
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
root = Path(root_path).resolve()
|
|
38
|
+
|
|
39
|
+
all_files = []
|
|
40
|
+
all_dirs = []
|
|
41
|
+
extensions = set()
|
|
42
|
+
|
|
43
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
44
|
+
current_depth = len(Path(dirpath).relative_to(root).parts)
|
|
45
|
+
|
|
46
|
+
if max_depth is not None and current_depth >= max_depth:
|
|
47
|
+
dirnames.clear()
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith('.')]
|
|
51
|
+
|
|
52
|
+
for filename in filenames:
|
|
53
|
+
if filename.startswith('.') and filename not in ['.gitignore', '.env.example']:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
all_files.append(filename)
|
|
57
|
+
ext = Path(filename).suffix.lower()
|
|
58
|
+
if ext:
|
|
59
|
+
extensions.add(ext)
|
|
60
|
+
|
|
61
|
+
for dirname in dirnames:
|
|
62
|
+
all_dirs.append(dirname)
|
|
63
|
+
|
|
64
|
+
tree_str = _build_tree(root, max_depth)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"root": str(root),
|
|
68
|
+
"name": root.name,
|
|
69
|
+
"files": all_files,
|
|
70
|
+
"dirs": all_dirs,
|
|
71
|
+
"tree": tree_str,
|
|
72
|
+
"has_license": any(f in all_files for f in ["LICENSE", "LICENSE.md", "LICENSE.txt", "license"]),
|
|
73
|
+
"has_contributing": any(f.upper() in ["CONTRIBUTING.MD", "CONTRIBUTING.TXT"] for f in all_files),
|
|
74
|
+
"has_gitignore": ".gitignore" in all_files,
|
|
75
|
+
"has_existing_readme": any(f.lower() == "readme.md" for f in all_files),
|
|
76
|
+
"file_extensions": sorted(list(extensions)),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_tree(root: Path, max_depth: int | None = None, prefix: str = "", current_depth: int = 0) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Recursively build an ASCII directory tree string.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
root (Path): Current directory path.
|
|
86
|
+
max_depth (int): Maximum recursion depth.
|
|
87
|
+
prefix (str): Indentation prefix for the current level.
|
|
88
|
+
current_depth (int): Tracks current recursion depth.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: Multi-line ASCII tree string.
|
|
92
|
+
"""
|
|
93
|
+
if max_depth is not None and current_depth > max_depth:
|
|
94
|
+
return ""
|
|
95
|
+
|
|
96
|
+
lines = []
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
entries = sorted(root.iterdir(), key=lambda e: (e.is_file(), e.name.lower()))
|
|
100
|
+
except PermissionError:
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
entries = [e for e in entries
|
|
104
|
+
if not e.name.startswith('.')
|
|
105
|
+
and e.name not in SKIP_DIRS]
|
|
106
|
+
|
|
107
|
+
for i, entry in enumerate(entries):
|
|
108
|
+
is_last = (i == len(entries) - 1)
|
|
109
|
+
connector = "└── " if is_last else "├── "
|
|
110
|
+
lines.append(f"{prefix}{connector}{entry.name}")
|
|
111
|
+
|
|
112
|
+
if entry.is_dir():
|
|
113
|
+
extension = " " if is_last else "│ "
|
|
114
|
+
subtree = _build_tree(entry, max_depth, prefix + extension, current_depth + 1)
|
|
115
|
+
if subtree:
|
|
116
|
+
lines.append(subtree)
|
|
117
|
+
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def load_config(root_path: str) -> dict:
|
|
122
|
+
"""
|
|
123
|
+
Load readmegen.config.json from the project root if it exists,
|
|
124
|
+
otherwise return DEFAULT_CONFIG.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
root_path (str): Path to the project root.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
dict: Merged configuration (user config overrides defaults).
|
|
131
|
+
"""
|
|
132
|
+
config_path = Path(root_path) / "readmegen.config.json"
|
|
133
|
+
config = DEFAULT_CONFIG.copy()
|
|
134
|
+
|
|
135
|
+
if config_path.exists():
|
|
136
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
137
|
+
user_config = json.load(f)
|
|
138
|
+
config.update(user_config)
|
|
139
|
+
|
|
140
|
+
return config
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
import sys
|
|
145
|
+
path = sys.argv[1] if len(sys.argv) > 1 else "."
|
|
146
|
+
result = scan_directory(path, max_depth=3)
|
|
147
|
+
print(f"Project: {result['name']}")
|
|
148
|
+
print(f"Files found: {len(result['files'])}")
|
|
149
|
+
print(f"Extensions: {result['file_extensions']}")
|
|
150
|
+
print(f"\nTree:\n{result['tree']}")
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# src/web/app.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
8
|
+
|
|
9
|
+
from flask import Flask, render_template, request, jsonify
|
|
10
|
+
from src.scanner import scan_directory, load_config
|
|
11
|
+
from src.detector import detect_stack
|
|
12
|
+
from src.generator import generate_readme
|
|
13
|
+
|
|
14
|
+
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.route("/")
|
|
18
|
+
def index():
|
|
19
|
+
"""Render the main input page."""
|
|
20
|
+
return render_template("index.html")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.route("/generate", methods=["POST"])
|
|
24
|
+
def generate():
|
|
25
|
+
"""
|
|
26
|
+
Accept a folder tree string (textarea input),
|
|
27
|
+
run the generator, return rendered README as JSON.
|
|
28
|
+
"""
|
|
29
|
+
data = request.get_json()
|
|
30
|
+
tree_txt = data.get("tree", "").strip()
|
|
31
|
+
template = data.get("template", "standard")
|
|
32
|
+
author = data.get("author", "")
|
|
33
|
+
username = data.get("github_username", "")
|
|
34
|
+
|
|
35
|
+
if not tree_txt:
|
|
36
|
+
return jsonify({"error": "No folder tree provided."}), 400
|
|
37
|
+
|
|
38
|
+
scan_result = _parse_tree_to_scan(tree_txt)
|
|
39
|
+
|
|
40
|
+
config = {
|
|
41
|
+
"template": template,
|
|
42
|
+
"include_badges": True,
|
|
43
|
+
"include_tree": True,
|
|
44
|
+
"max_tree_depth": 3,
|
|
45
|
+
"output_file": "README.md",
|
|
46
|
+
"author": author,
|
|
47
|
+
"github_username": username,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
detection = detect_stack(scan_result)
|
|
51
|
+
readme = generate_readme(scan_result, detection, config)
|
|
52
|
+
|
|
53
|
+
return jsonify({
|
|
54
|
+
"readme": readme,
|
|
55
|
+
"language": detection["primary_lang"],
|
|
56
|
+
"type": detection["project_type"],
|
|
57
|
+
"license": detection["license"],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_tree_to_scan(tree_text: str) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Parse a pasted ASCII folder tree into a minimal scan_result dict.
|
|
64
|
+
"""
|
|
65
|
+
lines = tree_text.strip().splitlines()
|
|
66
|
+
files = []
|
|
67
|
+
dirs = []
|
|
68
|
+
extensions = set()
|
|
69
|
+
|
|
70
|
+
project_name = lines[0].strip().rstrip("/") if lines else "my-project"
|
|
71
|
+
|
|
72
|
+
for line in lines[1:]:
|
|
73
|
+
name = line.replace("├──", "").replace("└──", "").replace("│", "").replace(" ", "").strip()
|
|
74
|
+
|
|
75
|
+
if not name:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if "." in name:
|
|
79
|
+
files.append(name)
|
|
80
|
+
ext = "." + name.split(".")[-1].lower()
|
|
81
|
+
extensions.add(ext)
|
|
82
|
+
else:
|
|
83
|
+
dirs.append(name)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"root": "",
|
|
87
|
+
"name": project_name,
|
|
88
|
+
"files": files,
|
|
89
|
+
"dirs": dirs,
|
|
90
|
+
"tree": "\n".join(lines[1:]),
|
|
91
|
+
"has_license": any(f in files for f in ["LICENSE", "LICENSE.md", "license"]),
|
|
92
|
+
"has_contributing": any("contributing" in f.lower() for f in files),
|
|
93
|
+
"has_gitignore": ".gitignore" in files,
|
|
94
|
+
"has_existing_readme": any(f.lower() == "readme.md" for f in files),
|
|
95
|
+
"file_extensions": sorted(list(extensions)),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
app.run(debug=True, port=5000)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# tests/test_detector.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from src.detector import detect_stack
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestDetector:
|
|
13
|
+
def test_detect_python_project(self):
|
|
14
|
+
"""Test detection of Python project."""
|
|
15
|
+
scan_result = {
|
|
16
|
+
"files": ["main.py", "requirements.txt", "setup.py"],
|
|
17
|
+
"dirs": ["tests", "src"],
|
|
18
|
+
"file_extensions": [".py", ".txt"],
|
|
19
|
+
"name": "my-python-bot",
|
|
20
|
+
"root": "/path/to/project"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
result = detect_stack(scan_result)
|
|
24
|
+
|
|
25
|
+
assert "Python" in result["languages"]
|
|
26
|
+
assert result["primary_lang"] == "Python"
|
|
27
|
+
|
|
28
|
+
def test_detect_cpp_project(self):
|
|
29
|
+
"""Test detection of C++ project."""
|
|
30
|
+
scan_result = {
|
|
31
|
+
"files": ["main.cpp", "CMakeLists.txt", "Makefile"],
|
|
32
|
+
"dirs": ["src", "include"],
|
|
33
|
+
"file_extensions": [".cpp", ".txt"],
|
|
34
|
+
"name": "cpp-app",
|
|
35
|
+
"root": "/path/to/project"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result = detect_stack(scan_result)
|
|
39
|
+
|
|
40
|
+
assert "C++" in result["languages"]
|
|
41
|
+
assert result["primary_lang"] == "C++"
|
|
42
|
+
|
|
43
|
+
def test_detect_typescript_project(self):
|
|
44
|
+
"""Test detection of TypeScript project."""
|
|
45
|
+
scan_result = {
|
|
46
|
+
"files": ["index.ts", "tsconfig.json", "package.json"],
|
|
47
|
+
"dirs": ["src", "components"],
|
|
48
|
+
"file_extensions": [".ts", ".json"],
|
|
49
|
+
"name": "web-app",
|
|
50
|
+
"root": "/path/to/project"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result = detect_stack(scan_result)
|
|
54
|
+
|
|
55
|
+
assert "TypeScript" in result["languages"]
|
|
56
|
+
assert result["primary_lang"] == "TypeScript"
|
|
57
|
+
|
|
58
|
+
def test_detect_telegram_bot(self):
|
|
59
|
+
"""Test detection of telegram bot project."""
|
|
60
|
+
scan_result = {
|
|
61
|
+
"files": ["bot.py", "requirements.txt"],
|
|
62
|
+
"dirs": ["tests"],
|
|
63
|
+
"file_extensions": [".py", ".txt"],
|
|
64
|
+
"name": "telegram-bot",
|
|
65
|
+
"root": "/path/to/project"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result = detect_stack(scan_result)
|
|
69
|
+
|
|
70
|
+
assert result["project_type"] == "telegram-bot"
|
|
71
|
+
|
|
72
|
+
def test_detect_web_app(self):
|
|
73
|
+
"""Test detection of web application."""
|
|
74
|
+
scan_result = {
|
|
75
|
+
"files": ["package.json", "index.html", "tsconfig.json"],
|
|
76
|
+
"dirs": ["src", "public", "components"],
|
|
77
|
+
"file_extensions": [".json", ".html", ".ts"],
|
|
78
|
+
"name": "my-web-app",
|
|
79
|
+
"root": "/path/to/project"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result = detect_stack(scan_result)
|
|
83
|
+
|
|
84
|
+
assert result["project_type"] == "web-app"
|
|
85
|
+
|
|
86
|
+
def test_detect_cli_tool(self):
|
|
87
|
+
"""Test detection of CLI tool."""
|
|
88
|
+
scan_result = {
|
|
89
|
+
"files": ["cli.py", "setup.py", "requirements.txt"],
|
|
90
|
+
"dirs": ["tests"],
|
|
91
|
+
"file_extensions": [".py", ".txt"],
|
|
92
|
+
"name": "my-cli-tool",
|
|
93
|
+
"root": "/path/to/project"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
result = detect_stack(scan_result)
|
|
97
|
+
|
|
98
|
+
assert result["project_type"] == "cli-tool"
|
|
99
|
+
|
|
100
|
+
def test_detect_has_tests(self):
|
|
101
|
+
"""Test that has_tests is detected correctly."""
|
|
102
|
+
scan_result = {
|
|
103
|
+
"files": ["main.py"],
|
|
104
|
+
"dirs": ["tests", "src"],
|
|
105
|
+
"file_extensions": [".py"],
|
|
106
|
+
"name": "test-project",
|
|
107
|
+
"root": "/path/to/project"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
result = detect_stack(scan_result)
|
|
111
|
+
|
|
112
|
+
assert result["has_tests"] is True
|
|
113
|
+
|
|
114
|
+
def test_detect_has_docs(self):
|
|
115
|
+
"""Test that has_docs is detected correctly."""
|
|
116
|
+
scan_result = {
|
|
117
|
+
"files": ["main.py"],
|
|
118
|
+
"dirs": ["docs", "src"],
|
|
119
|
+
"file_extensions": [".py"],
|
|
120
|
+
"name": "doc-project",
|
|
121
|
+
"root": "/path/to/project"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result = detect_stack(scan_result)
|
|
125
|
+
|
|
126
|
+
assert result["has_docs"] is True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# tests/test_generator.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from src.generator import generate_readme
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestGenerator:
|
|
13
|
+
def test_generate_minimal_template(self):
|
|
14
|
+
"""Test that minimal template produces non-empty output."""
|
|
15
|
+
scan_result = {
|
|
16
|
+
"name": "test-project",
|
|
17
|
+
"tree": "├── src/\n│ └── main.py",
|
|
18
|
+
"has_license": False,
|
|
19
|
+
"has_contributing": False,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
detection = {
|
|
23
|
+
"primary_lang": "Python",
|
|
24
|
+
"languages": ["Python"],
|
|
25
|
+
"project_type": "cli-tool",
|
|
26
|
+
"description_hint": "A Python project — Test Project.",
|
|
27
|
+
"install_cmd": "pip install -r requirements.txt",
|
|
28
|
+
"run_cmd": "python main.py",
|
|
29
|
+
"has_tests": False,
|
|
30
|
+
"has_docs": False,
|
|
31
|
+
"license": "None",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
config = {
|
|
35
|
+
"template": "minimal",
|
|
36
|
+
"include_badges": False,
|
|
37
|
+
"include_tree": True,
|
|
38
|
+
"max_tree_depth": 3,
|
|
39
|
+
"author": "Test Author",
|
|
40
|
+
"github_username": "testuser",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
result = generate_readme(scan_result, detection, config)
|
|
44
|
+
|
|
45
|
+
assert isinstance(result, str)
|
|
46
|
+
assert len(result) > 0
|
|
47
|
+
assert "Test Project" in result
|
|
48
|
+
|
|
49
|
+
def test_generate_standard_template(self):
|
|
50
|
+
"""Test that standard template produces output."""
|
|
51
|
+
scan_result = {
|
|
52
|
+
"name": "my-cli-tool",
|
|
53
|
+
"tree": "├── main.py",
|
|
54
|
+
"has_license": True,
|
|
55
|
+
"has_contributing": False,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
detection = {
|
|
59
|
+
"primary_lang": "Python",
|
|
60
|
+
"languages": ["Python"],
|
|
61
|
+
"project_type": "cli-tool",
|
|
62
|
+
"description_hint": "A CLI tool written in Python.",
|
|
63
|
+
"install_cmd": "pip install .",
|
|
64
|
+
"run_cmd": "python cli.py",
|
|
65
|
+
"has_tests": True,
|
|
66
|
+
"has_docs": False,
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
config = {
|
|
71
|
+
"template": "standard",
|
|
72
|
+
"include_badges": True,
|
|
73
|
+
"include_tree": True,
|
|
74
|
+
"max_tree_depth": 2,
|
|
75
|
+
"author": "",
|
|
76
|
+
"github_username": "developer",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
result = generate_readme(scan_result, detection, config)
|
|
80
|
+
|
|
81
|
+
assert isinstance(result, str)
|
|
82
|
+
assert len(result) > 0
|
|
83
|
+
|
|
84
|
+
def test_generate_full_template(self):
|
|
85
|
+
"""Test that full template produces output."""
|
|
86
|
+
scan_result = {
|
|
87
|
+
"name": "web-app",
|
|
88
|
+
"tree": "src/\n index.ts",
|
|
89
|
+
"has_license": True,
|
|
90
|
+
"has_contributing": True,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
detection = {
|
|
94
|
+
"primary_lang": "TypeScript",
|
|
95
|
+
"languages": ["TypeScript", "JavaScript"],
|
|
96
|
+
"project_type": "web-app",
|
|
97
|
+
"description_hint": "A web application built with TypeScript.",
|
|
98
|
+
"install_cmd": "npm install",
|
|
99
|
+
"run_cmd": "npm run dev",
|
|
100
|
+
"has_tests": True,
|
|
101
|
+
"has_docs": True,
|
|
102
|
+
"license": "Apache-2.0",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
config = {
|
|
106
|
+
"template": "full",
|
|
107
|
+
"include_badges": True,
|
|
108
|
+
"include_tree": True,
|
|
109
|
+
"max_tree_depth": 3,
|
|
110
|
+
"author": "Dev",
|
|
111
|
+
"github_username": "devuser",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
result = generate_readme(scan_result, detection, config)
|
|
115
|
+
|
|
116
|
+
assert isinstance(result, str)
|
|
117
|
+
assert len(result) > 0
|
|
118
|
+
|
|
119
|
+
def test_generate_academic_template(self):
|
|
120
|
+
"""Test that academic template produces output."""
|
|
121
|
+
scan_result = {
|
|
122
|
+
"name": "academic-project",
|
|
123
|
+
"tree": "main.py",
|
|
124
|
+
"has_license": False,
|
|
125
|
+
"has_contributing": False,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
detection = {
|
|
129
|
+
"primary_lang": "Python",
|
|
130
|
+
"languages": ["Python"],
|
|
131
|
+
"project_type": "unknown",
|
|
132
|
+
"description_hint": "A Python project.",
|
|
133
|
+
"install_cmd": "pip install -r requirements.txt",
|
|
134
|
+
"run_cmd": "python main.py",
|
|
135
|
+
"has_tests": False,
|
|
136
|
+
"has_docs": False,
|
|
137
|
+
"license": "None",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
config = {
|
|
141
|
+
"template": "academic",
|
|
142
|
+
"include_badges": False,
|
|
143
|
+
"include_tree": True,
|
|
144
|
+
"max_tree_depth": 2,
|
|
145
|
+
"author": "Student",
|
|
146
|
+
"github_username": "student",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
result = generate_readme(scan_result, detection, config)
|
|
150
|
+
|
|
151
|
+
assert isinstance(result, str)
|
|
152
|
+
assert len(result) > 0
|
|
153
|
+
|
|
154
|
+
def test_generate_invalid_template_raises(self):
|
|
155
|
+
"""Test that invalid template raises error."""
|
|
156
|
+
scan_result = {"name": "test", "tree": "", "has_license": False, "has_contributing": False}
|
|
157
|
+
detection = {"primary_lang": "Python", "languages": ["Python"], "project_type": "unknown", "description_hint": "A Python project.", "install_cmd": "", "run_cmd": "", "has_tests": False, "has_docs": False, "license": "None"}
|
|
158
|
+
config = {"template": "nonexistent", "include_badges": False, "include_tree": False, "max_tree_depth": 3, "author": "", "github_username": ""}
|
|
159
|
+
|
|
160
|
+
with pytest.raises(FileNotFoundError):
|
|
161
|
+
generate_readme(scan_result, detection, config)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# tests/test_scanner.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from src.scanner import scan_directory, load_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestScanner:
|
|
13
|
+
def test_scan_directory_returns_dict(self):
|
|
14
|
+
"""Test that scan_directory returns a dictionary with expected keys."""
|
|
15
|
+
result = scan_directory("examples/sample-cpp-project", max_depth=2)
|
|
16
|
+
|
|
17
|
+
assert isinstance(result, dict)
|
|
18
|
+
assert "root" in result
|
|
19
|
+
assert "name" in result
|
|
20
|
+
assert "files" in result
|
|
21
|
+
assert "dirs" in result
|
|
22
|
+
assert "tree" in result
|
|
23
|
+
assert "file_extensions" in result
|
|
24
|
+
|
|
25
|
+
def test_scan_cpp_project_detects_extensions(self):
|
|
26
|
+
"""Test that scanner detects .cpp extensions."""
|
|
27
|
+
result = scan_directory("examples/sample-cpp-project", max_depth=2)
|
|
28
|
+
|
|
29
|
+
assert ".cpp" in result["file_extensions"]
|
|
30
|
+
|
|
31
|
+
def test_scan_python_bot_detects_py(self):
|
|
32
|
+
"""Test that scanner detects Python files."""
|
|
33
|
+
result = scan_directory("examples/sample-python-bot", max_depth=2)
|
|
34
|
+
|
|
35
|
+
assert ".py" in result["file_extensions"]
|
|
36
|
+
assert "requirements.txt" in result["files"]
|
|
37
|
+
|
|
38
|
+
def test_scan_web_project_detects_json(self):
|
|
39
|
+
"""Test that scanner detects web project files."""
|
|
40
|
+
result = scan_directory("examples/sample-web-project", max_depth=2)
|
|
41
|
+
|
|
42
|
+
assert ".json" in result["file_extensions"]
|
|
43
|
+
|
|
44
|
+
def test_load_config_returns_dict(self):
|
|
45
|
+
"""Test that load_config returns a dictionary."""
|
|
46
|
+
config = load_config("examples/sample-cpp-project")
|
|
47
|
+
|
|
48
|
+
assert isinstance(config, dict)
|
|
49
|
+
assert "template" in config
|
|
50
|
+
|
|
51
|
+
def test_scan_nonexistent_path_raises(self):
|
|
52
|
+
"""Test that scanning a nonexistent path raises an error."""
|
|
53
|
+
with pytest.raises(Exception):
|
|
54
|
+
scan_directory("nonexistent/path/12345")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
pytest.main([__file__, "-v"])
|