From def81713ede3f9a8869a1f4903dc1235dc9e348b Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 08:40:03 +0000 Subject: [PATCH] Initial upload: Auto README Generator CLI v0.1.0 --- src/auto_readme/parsers/python_parser.py | 223 +++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/auto_readme/parsers/python_parser.py diff --git a/src/auto_readme/parsers/python_parser.py b/src/auto_readme/parsers/python_parser.py new file mode 100644 index 0000000..a35da69 --- /dev/null +++ b/src/auto_readme/parsers/python_parser.py @@ -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