pyproject-lens 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sam
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,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyproject-lens
3
+ Version: 0.1.0
4
+ Summary: A small health scanner for Python projects.
5
+ Author: Sam
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Sam3360/pyproject-lens
8
+ Project-URL: Repository, https://github.com/Sam3360/pyproject-lens
9
+ Project-URL: Issues, https://github.com/Sam3360/pyproject-lens/issues
10
+ Keywords: python,cli,project,pyproject,health
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: tomli; python_version < "3.11"
20
+ Dynamic: license-file
21
+
22
+ # pyproject-lens
23
+
24
+ A small, free health scanner for Python projects. Point it at a folder and it checks the basics: packaging metadata, imports versus declared dependencies, Python-version claims, project layout, and Git hygiene.
25
+
26
+ It is built for the first ten seconds of project review — not to replace Ruff, pytest, Bandit, or a human code review.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install pyproject-lens
32
+ ```
33
+
34
+ ## Use it
35
+
36
+ ```bash
37
+ pyproject-lens .
38
+ pyproject-lens ./another-project --json report.json
39
+ pyproject-lens . --markdown report.md
40
+ pyproject-lens . --ci --minimum-score 75
41
+ ```
42
+
43
+ You can also use it in Python:
44
+
45
+ ```python
46
+ from pyproject_lens import analyze
47
+
48
+ report = analyze(".")
49
+ print(report.score)
50
+ print(report.to_json())
51
+ ```
52
+
53
+ ## What the score means
54
+
55
+ Each of the five sections starts at 100. Detected issues reduce only the relevant section, and the project score is the rounded average. The rules are deliberately simple and visible in `src/pyproject_lens/analyzers.py`; it is a conversation starter, not a grade.
56
+
57
+ ## Scope for version 0.1
58
+
59
+ - Packaging: `pyproject.toml`, project name, Python version, README
60
+ - Dependencies: direct source imports compared with `project.dependencies`
61
+ - Compatibility: detects `match/case` used with a Python claim below 3.10
62
+ - Structure: `src/`, packages, root modules, and tests directory
63
+ - Repository hygiene: `.gitignore` and uncommitted changes
64
+
65
+ Everything is free and open source under the MIT license.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ python -m pip install -e .
71
+ python -m unittest discover -s tests
72
+ ```
73
+
74
+ Contributions are welcome. Please keep checks practical, explain what they found, and avoid claiming certainty when static analysis cannot prove something.
@@ -0,0 +1,53 @@
1
+ # pyproject-lens
2
+
3
+ A small, free health scanner for Python projects. Point it at a folder and it checks the basics: packaging metadata, imports versus declared dependencies, Python-version claims, project layout, and Git hygiene.
4
+
5
+ It is built for the first ten seconds of project review — not to replace Ruff, pytest, Bandit, or a human code review.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install pyproject-lens
11
+ ```
12
+
13
+ ## Use it
14
+
15
+ ```bash
16
+ pyproject-lens .
17
+ pyproject-lens ./another-project --json report.json
18
+ pyproject-lens . --markdown report.md
19
+ pyproject-lens . --ci --minimum-score 75
20
+ ```
21
+
22
+ You can also use it in Python:
23
+
24
+ ```python
25
+ from pyproject_lens import analyze
26
+
27
+ report = analyze(".")
28
+ print(report.score)
29
+ print(report.to_json())
30
+ ```
31
+
32
+ ## What the score means
33
+
34
+ Each of the five sections starts at 100. Detected issues reduce only the relevant section, and the project score is the rounded average. The rules are deliberately simple and visible in `src/pyproject_lens/analyzers.py`; it is a conversation starter, not a grade.
35
+
36
+ ## Scope for version 0.1
37
+
38
+ - Packaging: `pyproject.toml`, project name, Python version, README
39
+ - Dependencies: direct source imports compared with `project.dependencies`
40
+ - Compatibility: detects `match/case` used with a Python claim below 3.10
41
+ - Structure: `src/`, packages, root modules, and tests directory
42
+ - Repository hygiene: `.gitignore` and uncommitted changes
43
+
44
+ Everything is free and open source under the MIT license.
45
+
46
+ ## Development
47
+
48
+ ```bash
49
+ python -m pip install -e .
50
+ python -m unittest discover -s tests
51
+ ```
52
+
53
+ Contributions are welcome. Please keep checks practical, explain what they found, and avoid claiming certainty when static analysis cannot prove something.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyproject-lens"
7
+ version = "0.1.0"
8
+ description = "A small health scanner for Python projects."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = ["tomli; python_version < '3.11'"]
12
+ license = {text = "MIT"}
13
+ authors = [{name = "Sam"}]
14
+ keywords = ["python", "cli", "project", "pyproject", "health"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ ]
22
+
23
+ [project.scripts]
24
+ pyproject-lens = "pyproject_lens.cli:main"
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Sam3360/pyproject-lens"
28
+ Repository = "https://github.com/Sam3360/pyproject-lens"
29
+ Issues = "https://github.com/Sam3360/pyproject-lens/issues"
30
+
31
+ [tool.pyproject-lens]
32
+ minimum_score = 0
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """Public API for pyproject-lens."""
2
+
3
+ from .analyzers import analyze
4
+ from .models import Finding, Report, Section
5
+
6
+ __all__ = ["analyze", "Finding", "Report", "Section"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,163 @@
1
+ """The intentionally small, dependency-free project checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ try:
13
+ import tomllib
14
+ except ModuleNotFoundError: # pragma: no cover - Python 3.9/3.10
15
+ import tomli as tomllib # type: ignore[no-redef]
16
+
17
+ from .models import Report, Section
18
+
19
+ SKIP_DIRS = {".git", ".venv", "venv", "__pycache__", "build", "dist", ".tox", ".mypy_cache"}
20
+ STDLIB = set(getattr(sys, "stdlib_module_names", ()))
21
+
22
+
23
+ def _python_files(root: Path) -> list[Path]:
24
+ return [path for path in root.rglob("*.py") if not any(part in SKIP_DIRS for part in path.parts)]
25
+
26
+
27
+ def _read_toml(path: Path) -> dict[str, Any]:
28
+ try:
29
+ with path.open("rb") as handle:
30
+ return tomllib.load(handle)
31
+ except (OSError, tomllib.TOMLDecodeError):
32
+ return {}
33
+
34
+
35
+ def _top_level_imports(files: list[Path]) -> set[str]:
36
+ imports: set[str] = set()
37
+ for file in files:
38
+ try:
39
+ tree = ast.parse(file.read_text(encoding="utf-8"), filename=str(file))
40
+ except (OSError, UnicodeDecodeError, SyntaxError):
41
+ continue
42
+ for node in ast.walk(tree):
43
+ if isinstance(node, ast.Import):
44
+ imports.update(name.name.split(".")[0] for name in node.names)
45
+ elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
46
+ imports.add(node.module.split(".")[0])
47
+ return imports
48
+
49
+
50
+ def _distribution_name(value: str) -> str:
51
+ return re.split(r"[<>=!~;[ ]", value, maxsplit=1)[0].lower().replace("_", "-")
52
+
53
+
54
+ def _packaging(root: Path, config: dict[str, Any]) -> Section:
55
+ section = Section("Packaging")
56
+ pyproject = root / "pyproject.toml"
57
+ project = config.get("project", {})
58
+ if not pyproject.exists():
59
+ section.score = 30
60
+ section.add("warning", "No pyproject.toml found.", "Add one to describe how your project is built.")
61
+ return section
62
+ if not config:
63
+ section.score = 45
64
+ section.add("error", "pyproject.toml could not be read.", "Check its TOML syntax.", "pyproject.toml")
65
+ if not project.get("name"):
66
+ section.score -= 20
67
+ section.add("warning", "Project name is missing.", "Set project.name in pyproject.toml.", "pyproject.toml")
68
+ if not project.get("requires-python"):
69
+ section.score -= 15
70
+ section.add("warning", "Supported Python versions are not declared.", "Set project.requires-python.", "pyproject.toml")
71
+ if not (root / "README.md").exists() and not project.get("readme"):
72
+ section.score -= 15
73
+ section.add("warning", "No README was found.", "Add a short README with install and usage instructions.")
74
+ return section
75
+
76
+
77
+ def _dependencies(root: Path, config: dict[str, Any], files: list[Path]) -> Section:
78
+ section = Section("Dependencies")
79
+ declared = {_distribution_name(item) for item in config.get("project", {}).get("dependencies", [])}
80
+ imports = _top_level_imports(files)
81
+ local = {path.stem for path in files}
82
+ for file in files:
83
+ try:
84
+ relative = file.relative_to(root)
85
+ except ValueError:
86
+ continue
87
+ local.update(part for part in relative.parts[:-1] if part not in {"src", "tests"})
88
+ missing = sorted(name for name in imports if name not in STDLIB and name not in local and name.replace("_", "-") not in declared)
89
+ if missing:
90
+ section.score -= min(45, 10 * len(missing))
91
+ for name in missing[:8]:
92
+ section.add("warning", f"'{name}' is imported but not declared.", "Add it to project.dependencies if it is a runtime dependency.")
93
+ if (root / "requirements.txt").exists() and not declared:
94
+ section.score -= 10
95
+ section.add("info", "requirements.txt exists but project.dependencies is empty.", "Consider keeping runtime dependencies in pyproject.toml.")
96
+ return section
97
+
98
+
99
+ def _compatibility(config: dict[str, Any], files: list[Path]) -> Section:
100
+ section = Section("Python compatibility")
101
+ requires = config.get("project", {}).get("requires-python", "")
102
+ uses_match = False
103
+ for file in files:
104
+ try:
105
+ uses_match |= any(isinstance(node, ast.Match) for node in ast.walk(ast.parse(file.read_text(encoding="utf-8"))))
106
+ except (OSError, UnicodeDecodeError, SyntaxError):
107
+ continue
108
+ if uses_match and re.search(r">=3\.(?:[0-9]|10)\b", requires) and not re.search(r">=3\.(?:1[0-9]|[2-9][0-9])\b", requires):
109
+ section.score = 60
110
+ section.add("warning", "match/case syntax needs Python 3.10+.", "Raise requires-python to >=3.10 or avoid match/case.")
111
+ elif not requires:
112
+ section.score = 80
113
+ section.add("info", "Compatibility cannot be checked without requires-python.", "Declare supported Python versions in pyproject.toml.")
114
+ return section
115
+
116
+
117
+ def _structure(root: Path, files: list[Path]) -> Section:
118
+ section = Section("Project structure")
119
+ source_root = root / "src"
120
+ packages = [path for path in (source_root if source_root.is_dir() else root).iterdir() if path.is_dir() and (path / "__init__.py").exists()]
121
+ if source_root.is_dir() and not packages:
122
+ section.score -= 25
123
+ section.add("warning", "src/ exists but no package was detected.", "Put your package in src/ with an __init__.py file.", "src")
124
+ top_level = [path for path in files if path.parent == root and path.name not in {"setup.py", "conftest.py"}]
125
+ if len(top_level) > 6:
126
+ section.score -= 15
127
+ section.add("info", f"{len(top_level)} Python files live at the project root.", "Consider grouping application code into a package.")
128
+ tests = root / "tests"
129
+ if not tests.exists():
130
+ section.score -= 20
131
+ section.add("warning", "No tests/ directory found.", "Start with a small tests/ directory for important behavior.")
132
+ return section
133
+
134
+
135
+ def _git_health(root: Path) -> Section:
136
+ section = Section("Repository hygiene")
137
+ if not (root / ".git").exists():
138
+ section.score = 80
139
+ section.add("info", "This directory is not a Git repository.", "Initialize Git before sharing or releasing the project.")
140
+ return section
141
+ if not (root / ".gitignore").exists():
142
+ section.score -= 20
143
+ section.add("warning", "No .gitignore found.", "Ignore virtual environments, caches, and build output.")
144
+ try:
145
+ result = subprocess.run(["git", "status", "--porcelain"], cwd=root, text=True, capture_output=True, timeout=3, check=False)
146
+ changes = result.stdout.splitlines()
147
+ if changes:
148
+ section.score -= min(20, len(changes))
149
+ section.add("info", f"{len(changes)} uncommitted Git change(s).", "Commit or stash work when you reach a clean checkpoint.")
150
+ except (OSError, subprocess.TimeoutExpired):
151
+ section.add("info", "Git status could not be checked.")
152
+ return section
153
+
154
+
155
+ def analyze(path: str | Path = ".") -> Report:
156
+ """Analyze a project directory and return a report. Never changes the project."""
157
+ root = Path(path).expanduser().resolve()
158
+ if not root.is_dir():
159
+ raise ValueError(f"Not a directory: {root}")
160
+ files = _python_files(root)
161
+ config = _read_toml(root / "pyproject.toml")
162
+ sections = [_packaging(root, config), _dependencies(root, config, files), _compatibility(config, files), _structure(root, files), _git_health(root)]
163
+ return Report(path=root, sections=sections, files_scanned=len(files))
@@ -0,0 +1,69 @@
1
+ """Command line interface for pyproject-lens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ from .analyzers import analyze
10
+
11
+
12
+ def _print_report(report: object) -> None:
13
+ from .models import Report
14
+ report = report # keep the terminal-friendly type narrow below
15
+ assert isinstance(report, Report)
16
+ print("pyproject-lens — Project Health Report")
17
+ print(f"Project: {report.path.name} | Python files scanned: {report.files_scanned}")
18
+ print("=" * 58)
19
+ for section in report.sections:
20
+ print(f"\n{section.name.upper()} {section.score}/100")
21
+ if not section.findings:
22
+ print(" OK — nothing obvious found")
23
+ for finding in section.findings:
24
+ print(f" {finding.level.upper()}: {finding.message}")
25
+ if finding.recommendation:
26
+ print(f" Try: {finding.recommendation}")
27
+ warnings = sum(item.level in {"warning", "error"} for item in report.findings)
28
+ print(f"\nPROJECT HEALTH: {report.score}/100 | {warnings} warning(s)")
29
+
30
+
31
+ def main(argv: list[str] | None = None) -> int:
32
+ parser = argparse.ArgumentParser(description="A small health scanner for Python projects.")
33
+ parser.add_argument("path", nargs="?", default=".", help="project directory (default: current directory)")
34
+ parser.add_argument("--json", metavar="FILE", help="write a JSON report to FILE; use - for stdout")
35
+ parser.add_argument("--markdown", metavar="FILE", help="write a Markdown report to FILE; use - for stdout")
36
+ parser.add_argument("--ci", action="store_true", help="return an error if minimum_score is not met")
37
+ parser.add_argument("--minimum-score", type=int, help="score needed for --ci (overrides pyproject.toml)")
38
+ args = parser.parse_args(argv)
39
+ try:
40
+ report = analyze(args.path)
41
+ except ValueError as error:
42
+ parser.error(str(error))
43
+ if args.json:
44
+ text = report.to_json(None if args.json == "-" else args.json)
45
+ if args.json == "-":
46
+ print(text, end="")
47
+ elif args.markdown:
48
+ text = report.to_markdown(None if args.markdown == "-" else args.markdown)
49
+ if args.markdown == "-":
50
+ print(text, end="")
51
+ else:
52
+ _print_report(report)
53
+ if args.ci:
54
+ threshold = args.minimum_score
55
+ if threshold is None:
56
+ import tomllib
57
+ try:
58
+ with (Path(args.path) / "pyproject.toml").open("rb") as handle:
59
+ threshold = tomllib.load(handle).get("tool", {}).get("pyproject-lens", {}).get("minimum_score", 0)
60
+ except (OSError, tomllib.TOMLDecodeError):
61
+ threshold = 0
62
+ if report.score < threshold:
63
+ print(f"CI FAILED: score {report.score} is below minimum_score {threshold}", file=sys.stderr)
64
+ return 1
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
@@ -0,0 +1,74 @@
1
+ """Small data objects used by the scanner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+ import json
9
+
10
+
11
+ @dataclass
12
+ class Finding:
13
+ level: str
14
+ message: str
15
+ recommendation: str | None = None
16
+ path: str | None = None
17
+
18
+
19
+ @dataclass
20
+ class Section:
21
+ name: str
22
+ score: int = 100
23
+ findings: list[Finding] = field(default_factory=list)
24
+
25
+ def add(self, level: str, message: str, recommendation: str | None = None, path: str | None = None) -> None:
26
+ self.findings.append(Finding(level, message, recommendation, path))
27
+
28
+
29
+ @dataclass
30
+ class Report:
31
+ path: Path
32
+ sections: list[Section]
33
+ files_scanned: int
34
+
35
+ @property
36
+ def score(self) -> int:
37
+ if not self.sections:
38
+ return 100
39
+ return round(sum(section.score for section in self.sections) / len(self.sections))
40
+
41
+ @property
42
+ def findings(self) -> list[Finding]:
43
+ return [finding for section in self.sections for finding in section.findings]
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ return {
47
+ "path": str(self.path),
48
+ "score": self.score,
49
+ "files_scanned": self.files_scanned,
50
+ "sections": [asdict(section) for section in self.sections],
51
+ }
52
+
53
+ def to_json(self, output: str | Path | None = None) -> str:
54
+ text = json.dumps(self.to_dict(), indent=2) + "\n"
55
+ if output:
56
+ Path(output).write_text(text, encoding="utf-8")
57
+ return text
58
+
59
+ def to_markdown(self, output: str | Path | None = None) -> str:
60
+ lines = [f"# pyproject-lens report", "", f"**Project:** `{self.path.name}`", f"**Health:** {self.score}/100", ""]
61
+ for section in self.sections:
62
+ lines.extend([f"## {section.name} — {section.score}/100", ""])
63
+ if not section.findings:
64
+ lines.extend(["No issues detected.", ""])
65
+ continue
66
+ for finding in section.findings:
67
+ detail = f" — {finding.recommendation}" if finding.recommendation else ""
68
+ location = f" (`{finding.path}`)" if finding.path else ""
69
+ lines.append(f"- **{finding.level.upper()}**{location}: {finding.message}{detail}")
70
+ lines.append("")
71
+ text = "\n".join(lines)
72
+ if output:
73
+ Path(output).write_text(text, encoding="utf-8")
74
+ return text
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyproject-lens
3
+ Version: 0.1.0
4
+ Summary: A small health scanner for Python projects.
5
+ Author: Sam
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Sam3360/pyproject-lens
8
+ Project-URL: Repository, https://github.com/Sam3360/pyproject-lens
9
+ Project-URL: Issues, https://github.com/Sam3360/pyproject-lens/issues
10
+ Keywords: python,cli,project,pyproject,health
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: tomli; python_version < "3.11"
20
+ Dynamic: license-file
21
+
22
+ # pyproject-lens
23
+
24
+ A small, free health scanner for Python projects. Point it at a folder and it checks the basics: packaging metadata, imports versus declared dependencies, Python-version claims, project layout, and Git hygiene.
25
+
26
+ It is built for the first ten seconds of project review — not to replace Ruff, pytest, Bandit, or a human code review.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install pyproject-lens
32
+ ```
33
+
34
+ ## Use it
35
+
36
+ ```bash
37
+ pyproject-lens .
38
+ pyproject-lens ./another-project --json report.json
39
+ pyproject-lens . --markdown report.md
40
+ pyproject-lens . --ci --minimum-score 75
41
+ ```
42
+
43
+ You can also use it in Python:
44
+
45
+ ```python
46
+ from pyproject_lens import analyze
47
+
48
+ report = analyze(".")
49
+ print(report.score)
50
+ print(report.to_json())
51
+ ```
52
+
53
+ ## What the score means
54
+
55
+ Each of the five sections starts at 100. Detected issues reduce only the relevant section, and the project score is the rounded average. The rules are deliberately simple and visible in `src/pyproject_lens/analyzers.py`; it is a conversation starter, not a grade.
56
+
57
+ ## Scope for version 0.1
58
+
59
+ - Packaging: `pyproject.toml`, project name, Python version, README
60
+ - Dependencies: direct source imports compared with `project.dependencies`
61
+ - Compatibility: detects `match/case` used with a Python claim below 3.10
62
+ - Structure: `src/`, packages, root modules, and tests directory
63
+ - Repository hygiene: `.gitignore` and uncommitted changes
64
+
65
+ Everything is free and open source under the MIT license.
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ python -m pip install -e .
71
+ python -m unittest discover -s tests
72
+ ```
73
+
74
+ Contributions are welcome. Please keep checks practical, explain what they found, and avoid claiming certainty when static analysis cannot prove something.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/pyproject_lens/__init__.py
5
+ src/pyproject_lens/analyzers.py
6
+ src/pyproject_lens/cli.py
7
+ src/pyproject_lens/models.py
8
+ src/pyproject_lens.egg-info/PKG-INFO
9
+ src/pyproject_lens.egg-info/SOURCES.txt
10
+ src/pyproject_lens.egg-info/dependency_links.txt
11
+ src/pyproject_lens.egg-info/entry_points.txt
12
+ src/pyproject_lens.egg-info/requires.txt
13
+ src/pyproject_lens.egg-info/top_level.txt
14
+ tests/test_analyze.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyproject-lens = pyproject_lens.cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [:python_version < "3.11"]
3
+ tomli
@@ -0,0 +1 @@
1
+ pyproject_lens
@@ -0,0 +1,23 @@
1
+ import tempfile
2
+ import unittest
3
+ from pathlib import Path
4
+
5
+ from pyproject_lens import analyze
6
+
7
+
8
+ class AnalyzeTests(unittest.TestCase):
9
+ def test_analyze_returns_sections(self) -> None:
10
+ with tempfile.TemporaryDirectory() as temporary:
11
+ root = Path(temporary)
12
+ (root / "pyproject.toml").write_text('[project]\nname = "demo"\nrequires-python = ">=3.10"\n')
13
+ (root / "README.md").write_text("# Demo")
14
+ (root / "src" / "demo").mkdir(parents=True)
15
+ (root / "src" / "demo" / "__init__.py").write_text("")
16
+ report = analyze(root)
17
+ self.assertLessEqual(report.score, 100)
18
+ self.assertEqual([section.name for section in report.sections], ["Packaging", "Dependencies", "Python compatibility", "Project structure", "Repository hygiene"])
19
+
20
+ def test_json_is_valid(self) -> None:
21
+ with tempfile.TemporaryDirectory() as temporary:
22
+ report = analyze(temporary)
23
+ self.assertIn('"score"', report.to_json())