Initial upload: Auto README Generator CLI v0.1.0
Some checks failed
CI / build (push) Has been cancelled
CI / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-05 08:40:03 +00:00
parent a3fc5dbeb9
commit def81713ed

View File

@@ -0,0 +1,223 @@
"""Python dependency parser for requirements.txt and pyproject.toml."""
import re
from pathlib import Path
from typing import Optional
import tomllib
from . import BaseParser, Dependency
class PythonDependencyParser(BaseParser):
"""Parser for Python dependency files."""
SUPPORTED_FILES = ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg", "Pipfile"]
def can_parse(self, path: Path) -> bool:
"""Check if the file is a Python dependency file."""
return path.name.lower() in self.SUPPORTED_FILES
def parse(self, path: Path) -> list[Dependency]:
"""Parse dependencies from Python files."""
if not path.exists():
return []
if path.name.lower() == "requirements.txt":
return self._parse_requirements_txt(path)
elif path.name.lower() in ["pyproject.toml", "setup.cfg"]:
return self._parse_toml(path)
elif path.name.lower() == "pipfile":
return self._parse_pipfile(path)
elif path.name.lower() in ["setup.py", "setup.cfg"]:
return self._parse_setup_file(path)
return []
def _parse_requirements_txt(self, path: Path) -> list[Dependency]:
"""Parse requirements.txt file."""
dependencies = []
try:
content = path.read_text(encoding="utf-8")
for line in content.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
is_dev = any(x in line for x in ["-e", "--dev", "[dev]"])
version = None
name = line
if ">=" in line:
parts = line.split(">=")
name = parts[0].strip()
version = ">=" + parts[1].strip()
elif "<=" in line:
parts = line.split("<=")
name = parts[0].strip()
version = "<=" + parts[1].strip()
elif ">" in line:
parts = line.split(">")
name = parts[0].strip()
version = ">" + parts[1].strip()
elif "<" in line:
parts = line.split("<")
name = parts[0].strip()
version = "<" + parts[1].strip()
elif "==" in line:
parts = line.split("==")
name = parts[0].strip()
version = parts[1].strip()
elif "~=" in line:
parts = line.split("~=")
name = parts[0].strip()
version = "~=" + parts[1].strip()
name = self._clean_package_name(name)
if name:
dependencies.append(
Dependency(
name=name,
version=version,
is_dev=is_dev,
source_file=path,
)
)
except Exception:
pass
return dependencies
def _parse_toml(self, path: Path) -> list[Dependency]:
"""Parse pyproject.toml or setup.cfg file."""
dependencies = []
try:
if path.name.endswith(".toml"):
with open(path, "rb") as f:
data = tomllib.load(f)
else:
import configparser
config = configparser.ConfigParser()
config.read(path)
data = {s: dict(config[s]) for s in config.sections()}
except Exception:
return dependencies
if "project" in data:
project_data = data["project"]
if "dependencies" in project_data:
deps = project_data["dependencies"]
if isinstance(deps, list):
for dep in deps:
parsed = self._parse_pep508_string(dep)
if parsed:
dependencies.append(parsed)
elif isinstance(deps, str):
for dep in deps.split("\n"):
parsed = self._parse_pep508_string(dep.strip())
if parsed:
dependencies.append(parsed)
if "optional-dependencies" in project_data:
for group, optional_deps in project_data["optional-dependencies"].items():
if isinstance(optional_deps, list):
for dep in optional_deps:
parsed = self._parse_pep508_string(dep)
if parsed:
parsed.is_dev = True
parsed.is_optional = True
dependencies.append(parsed)
return dependencies
def _parse_pep508_string(self, dep_string: str) -> Optional[Dependency]:
"""Parse a PEP 508 dependency specification string."""
dep_string = dep_string.strip()
if not dep_string or dep_string.startswith("#"):
return None
is_dev = False
is_optional = False
if "[dev]" in dep_string:
is_dev = True
dep_string = dep_string.replace("[dev]", "")
elif "[tests]" in dep_string:
is_dev = True
dep_string = dep_string.replace("[tests]", "")
match = re.match(r"([a-zA-Z0-9_-]+)(.*)", dep_string)
if not match:
return None
name = match.group(1).strip()
version_part = match.group(2).strip()
version = self._normalize_version(version_part)
return Dependency(
name=name,
version=version,
is_dev=is_dev,
is_optional=is_optional,
)
def _parse_pipfile(self, path: Path) -> list[Dependency]:
"""Parse Pipfile."""
dependencies = []
try:
import tomli
with open(path, "rb") as f:
data = tomli.load(f)
except Exception:
return dependencies
for section in ["packages", "dev-packages"]:
if section in data:
for name, value in data[section].items():
if isinstance(value, str):
version = value
elif isinstance(value, dict):
version = value.get("version", None)
else:
version = None
dependencies.append(
Dependency(
name=name,
version=self._normalize_version(version),
is_dev="dev" in section,
source_file=path,
)
)
return dependencies
def _parse_setup_file(self, path: Path) -> list[Dependency]:
"""Parse setup.py or setup.cfg file."""
dependencies = []
try:
content = path.read_text(encoding="utf-8")
install_requires_match = re.search(
r"install_requires\s*=\s*\[([^\]]+)\]", content, re.DOTALL
)
if install_requires_match:
deps_str = install_requires_match.group(1)
for dep in deps_str.split(","):
dep = dep.strip().strip("'\"")
if dep:
parsed = self._parse_pep508_string(dep)
if parsed:
dependencies.append(parsed)
except Exception:
pass
return dependencies
def _clean_package_name(self, name: str) -> str:
"""Clean a package name."""
name = name.strip()
name = re.sub(r"^\.+", "", name)
name = re.sub(r"[<>=!~].*$", "", name)
name = name.strip()
return name if name else None