commit 7e8027451e2855e050760c9ed7695312be806034 Author: Swen Kooij Date: Sat Feb 29 12:18:38 2020 +0200 First push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c60542b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env/ +venv/ +env/ + +*.pyc +__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d048fdf --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# pip-package-list + +A small and definitely faulty tool that tries to form a list of packages that you depend on. This is useful in mono-repo's where all dependencies are split into dozens of `requirements.txt` and `setup.py` files. diff --git a/pippackagelist/__init__.py b/pippackagelist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pippackagelist/__main__.py b/pippackagelist/__main__.py new file mode 100644 index 0000000..30ab11d --- /dev/null +++ b/pippackagelist/__main__.py @@ -0,0 +1,25 @@ +import sys + +from .setup_py_parser import parse_setup_py +from .requirements_txt_parser import parse_requirements_txt +from .requirements import RequirementsRecursiveEntry, RequirementsEditableEntry + + +def main() -> int: + input_file = sys.argv[1] + + aggregated_entries = [] + for requirement in parse_requirements_txt(input_file): + if isinstance(requirement, RequirementsRecursiveEntry): + aggregated_entries.extend(list(parse_requirements_txt(requirement.path))) + elif isinstance(requirement, RequirementsEditableEntry): + aggregated_entries.extend(list(parse_setup_py(requirement.path))) + else: + aggregated_entries.append(requirement) + + print(aggregated_entries) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pippackagelist/requirements.py b/pippackagelist/requirements.py new file mode 100644 index 0000000..0fcdf8d --- /dev/null +++ b/pippackagelist/requirements.py @@ -0,0 +1,131 @@ +import os + +from dataclasses import dataclass +from typing import Generator, Optional, List + + +@dataclass +class RequirementsEntrySource: + path: str + line: Optional[str] + line_number: Optional[str] + + +@dataclass +class RequirementsEntry: + source: Optional[RequirementsEntrySource] + + +@dataclass +class RequirementsRecursiveEntry(RequirementsEntry): + path: str + + +@dataclass +class RequirementsEditableEntry(RequirementsEntry): + path: str + + +@dataclass +class RequirementsVCSPackageEntry(RequirementsEntry): + vcs: str + uri: str + tag: Optional[str] + + +@dataclass +class RequirementsPackageEntry(RequirementsEntry): + name: str + operator: str + version: str + + +def parse_requirements(source: Optional[RequirementsEntrySource], lines: List[str]) -> Generator[RequirementsEntry, None, None]: + + for index, line in enumerate(lines): + stripped_line = line.strip() + + if not len(stripped_line) or stripped_line.startswith('#'): + continue + + line_source = None + if source: + line_source = RequirementsEntrySource( + path=source.path, + line=line, + line_number=index + 1, + ) + + if stripped_line.startswith('-r'): + yield parse_recursive_requirements_entry( + line_source, + stripped_line, + ) + + elif stripped_line.startswith('-e'): + yield parse_editable_requirements_entry( + line_source, + stripped_line, + ) + + # TODO: add support for other VCS's + elif stripped_line.startswith('git+'): + yield parse_vcs_requirements_entry( + line_source, + stripped_line, + ) + else: + yield parse_package_requirements_entry( + line_source, + stripped_line, + ) + + +def parse_recursive_requirements_entry(source: RequirementsEntrySource, line: str) -> RequirementsRecursiveEntry: + path = line.replace('-r', '').strip() + return RequirementsRecursiveEntry( + source=source, + path=os.path.join(os.path.dirname(source.path), path), + ) + + +def parse_editable_requirements_entry(source: RequirementsEntrySource, line: str) -> RequirementsEditableEntry: + path = line.replace('-e', '').strip() + return RequirementsEditableEntry( + source=source, + path=os.path.join(os.path.dirname(source.path), path), + ) + + +def parse_vcs_requirements_entry(source: RequirementsEntrySource, line: str) -> RequirementsVCSPackageEntry: + vcs_uri_split = line.split('+') + vcs = vcs_uri_split[0] + + uri_tag_split = vcs_uri_split[1].split('#') + uri = uri_tag_split[0] + + tag = None + if len(uri_tag_split) > 1: + tag = uri_tag_split[1] + + return RequirementsVCSPackageEntry( + source=source, + vcs=vcs, + uri=uri, + tag=tag, + ) + + +def parse_package_requirements_entry(source: RequirementsEntrySource, line: str) -> RequirementsPackageEntry: + operators = ["==", ">=", ">", "<=", "<"] + for operator in operators: + parts = line.split(operator) + if len(parts) == 1: + continue + + return RequirementsPackageEntry( + source=source, + name=parts[0], + operator=operator, + version=parts[1], + ) diff --git a/pippackagelist/requirements_txt_parser.py b/pippackagelist/requirements_txt_parser.py new file mode 100644 index 0000000..f6b6a11 --- /dev/null +++ b/pippackagelist/requirements_txt_parser.py @@ -0,0 +1,11 @@ +from typing import Generator + +from .requirements import parse_requirements, RequirementsEntry, RequirementsEntrySource + + +def parse_requirements_txt(file_path: str) -> Generator[RequirementsEntry, None, None]: + source = RequirementsEntrySource(path=file_path, line=None, line_number=None) + + with open(file_path, 'r') as fp: + for requirement in parse_requirements(source, fp.readlines()): + yield requirement diff --git a/pippackagelist/setup_py_parser.py b/pippackagelist/setup_py_parser.py new file mode 100644 index 0000000..705b2a7 --- /dev/null +++ b/pippackagelist/setup_py_parser.py @@ -0,0 +1,28 @@ +import os +import setuptools + +from typing import Generator + +from .requirements import RequirementsEntry, RequirementsEntrySource, parse_requirements + + +def parse_setup_py(file_path: str) -> Generator[RequirementsEntry, None, None]: + setup_kwargs = {} + + def _setup_proxy(*args, **kwargs): + setup_kwargs.update(dict(kwargs)) + setuptools.setup = _setup_proxy + + path = os.path.join(file_path, "setup.py") + with open(path, "r") as fp: + exec(fp.read()) + + source = RequirementsEntrySource( + path=path, + line=None, + line_number=None, + ) + + requirements = setup_kwargs.get("install_requires") or [] + for requirement in parse_requirements(source, requirements): + yield requirement