Some checks failed
CI / test (push) Has been cancelled
- Remove unused imports across all modules - Remove unused variables (names, color, output, original_line, extras) - Fix regex syntax errors in parser files - Remove unused Dependency imports from parsers
212 lines
7.6 KiB
Python
212 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
else:
|
|
import tomli as tomllib
|
|
|
|
from depaudit.parsers import Parser, ParsedManifest
|
|
|
|
|
|
class PythonParser(Parser):
|
|
language = "python"
|
|
|
|
def can_parse(self, file_path: Path) -> bool:
|
|
return file_path.name in (
|
|
"requirements.txt",
|
|
"requirements-dev.txt",
|
|
"requirements.txt",
|
|
"setup.py",
|
|
"setup.cfg",
|
|
"pyproject.toml",
|
|
"Pipfile",
|
|
"Pipfile.lock",
|
|
)
|
|
|
|
def parse(self, file_path: Path) -> ParsedManifest:
|
|
manifest = ParsedManifest(
|
|
language=self.language,
|
|
file_path=file_path,
|
|
)
|
|
|
|
if file_path.name == "pyproject.toml":
|
|
self._parse_pyproject_toml(file_path, manifest)
|
|
elif file_path.name == "requirements.txt":
|
|
self._parse_requirements_txt(file_path, manifest)
|
|
elif file_path.name == "setup.py":
|
|
self._parse_setup_py(file_path, manifest)
|
|
elif file_path.name == "setup.cfg":
|
|
self._parse_setup_cfg(file_path, manifest)
|
|
elif file_path.name == "Pipfile":
|
|
self._parse_pipfile(file_path, manifest)
|
|
elif file_path.name == "Pipfile.lock":
|
|
self._parse_pipfile_lock(file_path, manifest)
|
|
|
|
return manifest
|
|
|
|
def _parse_pyproject_toml(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
with open(file_path, "rb") as f:
|
|
data = tomllib.load(f)
|
|
|
|
project = data.get("project", {})
|
|
manifest.project_name = project.get("name")
|
|
manifest.project_version = project.get("version")
|
|
|
|
dependencies = project.get("dependencies", [])
|
|
for dep in dependencies:
|
|
name, version = self._parse_requirement_string(dep)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version)
|
|
)
|
|
|
|
optional_dependencies = project.get("optional-dependencies", {})
|
|
for group, deps in optional_dependencies.items():
|
|
for dep in deps:
|
|
name, version = self._parse_requirement_string(dep)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(
|
|
file_path, name, version, optional=(group != "dev")
|
|
)
|
|
)
|
|
|
|
build = data.get("build-system", {})
|
|
if "requires" in build:
|
|
for req in build["requires"]:
|
|
if isinstance(req, str) and not req.startswith("setuptools"):
|
|
name, version = self._parse_requirement_string(req)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version)
|
|
)
|
|
|
|
manifest.raw_data = data
|
|
|
|
def _parse_requirements_txt(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
for line in content.split("\n"):
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
|
|
name, version = self._parse_requirement_string(line)
|
|
if name:
|
|
dev = "-dev" in line or "-dev" in name.lower()
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version, dev=dev)
|
|
)
|
|
|
|
def _parse_setup_py(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
content = file_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 re.findall(r'"([^"]+)"', deps_str):
|
|
name, version = self._parse_requirement_string(dep)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version)
|
|
)
|
|
|
|
name_match = re.search(r"name\s*=\s*[\"']([^\"']+)[\"']", content)
|
|
if name_match:
|
|
manifest.project_name = name_match.group(1)
|
|
|
|
version_match = re.search(r"version\s*=\s*[\"']([^\"']+)[\"']", content)
|
|
if version_match:
|
|
manifest.project_version = version_match.group(1)
|
|
|
|
def _parse_setup_cfg(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
|
|
install_requires_match = re.search(
|
|
r"install_requires\s*=\s*([^\n]+)", content
|
|
)
|
|
if install_requires_match:
|
|
deps = install_requires_match.group(1).split()
|
|
for dep in deps:
|
|
if dep.startswith("-e"):
|
|
continue
|
|
name, version = self._parse_requirement_string(dep)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version)
|
|
)
|
|
|
|
def _parse_pipfile(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
|
|
name_match = re.search(r"name\s*=\s*[\"']([^\"']+)[\"']", content)
|
|
if name_match:
|
|
manifest.project_name = name_match.group(1)
|
|
|
|
for section in ["packages", "dev-packages"]:
|
|
section_match = re.search(
|
|
rf'{section}\s*=\s*\{{([^}}]+)\}}', content, re.DOTALL
|
|
)
|
|
if section_match:
|
|
deps_block = section_match.group(1)
|
|
for match in re.finditer(r'"([^"]+)"', deps_block):
|
|
dep_str = match.group(1)
|
|
name, version = self._parse_requirement_string(dep_str)
|
|
if name:
|
|
manifest.dependencies.append(
|
|
self._create_dependency(
|
|
file_path, name, version, dev=(section == "dev-packages")
|
|
)
|
|
)
|
|
|
|
def _parse_pipfile_lock(self, file_path: Path, manifest: ParsedManifest) -> None:
|
|
import json
|
|
|
|
data = json.loads(file_path.read_text(encoding="utf-8"))
|
|
|
|
manifest.project_name = data.get("name")
|
|
manifest.project_version = data.get("version")
|
|
|
|
for name, info in data.get("default", {}).items():
|
|
version = info.get("version", "").replace("==", "")
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version)
|
|
)
|
|
|
|
for name, info in data.get("develop", {}).items():
|
|
version = info.get("version", "").replace("==", "")
|
|
manifest.dependencies.append(
|
|
self._create_dependency(file_path, name, version, dev=True)
|
|
)
|
|
|
|
def _parse_requirement_string(self, req: str) -> tuple[str, str]:
|
|
req = req.strip()
|
|
if not req:
|
|
return "", ""
|
|
|
|
extras_match = re.match(r"([^\[]+)(?:\[([^\]]+)\])?", req)
|
|
if extras_match:
|
|
name = extras_match.group(1)
|
|
else:
|
|
name = req
|
|
|
|
for op in ["==", ">=", "<=", "~=", "!=", ">", "<", "==="]:
|
|
if op in name:
|
|
parts = name.split(op)
|
|
name = parts[0].strip()
|
|
version = parts[1].strip() if len(parts) > 1 else ""
|
|
break
|
|
else:
|
|
version = ""
|
|
|
|
name = name.lower().replace("_", "-")
|
|
|
|
return name, version
|