#!/usr/bin/env python3 """Generate requirements files from pyproject.toml. For usage, see: python scripts/generate_requirements.py --help """ import argparse import sys from pathlib import Path try: import tomllib except ImportError: import tomli as tomllib # Python < 3.11 # Global flag for verbose output _VERBOSE = False def log(message: str) -> None: """Print message if verbose mode is enabled.""" if _VERBOSE: print(message) def generate_requirements( group: str | None = None, output: str | Path | None = None, pyproject: dict | str | Path = 'pyproject.toml', ) -> None: """Generate requirements file from pyproject.toml.""" log(f'Parsing pyproject.toml...') if not isinstance(pyproject, dict): pyproject = parse_pyproject(Path(pyproject)) log('Extracting indexes...') indexes = get_indexes(pyproject) log('Extracting dependencies...') deps = get_dependencies(pyproject, group) if not deps: print(f'Warning: No dependencies found for {group or "base"}', file=sys.stderr) log('Generating requirements file content...') content = generate_output(deps, indexes) log(f'Determining output filename...') output_file = get_output_name(group, output) log(f'Writing to {output_file}...') with Path(output_file).open('w') as f: f.write(content) print(f'Created {output_file} ({len(deps)} dependencies)') def get_dependencies(pyproject: dict, group: str | None = None) -> list[str]: """Extract dependencies from pyproject.toml sections.""" if not group: # [project.dependencies] return pyproject.get('project', {}).get('dependencies', []) else: # [dependency-groups] dep_groups = pyproject.get('dependency-groups', {}) if group not in dep_groups: print( f"Error: Dependency group '{group}' not found", file=sys.stderr, ) print(f'Available groups: {list(dep_groups.keys())}', file=sys.stderr) sys.exit(1) return dep_groups.get(group, []) def get_indexes(pyproject: dict) -> list[str]: """Extract package indexes from [[tool.uv.index]] sections.""" configs = pyproject.get('tool', {}).get('uv', {}).get('index', []) if isinstance(configs, dict): configs = [configs] indexes: set[str] = set() for config in configs: if isinstance(config, dict) and 'url' in config: indexes.add(config['url']) return list(indexes) def get_output_name( group: str | None = None, output_file: str | Path | None = None ) -> str: """Determine the output filename based on group and provided output_file.""" if output_file: return str(output_file) if group: return f'requirements-{group}.txt' return 'requirements.txt' def generate_output(dependencies: list[str] = [], indexes: list[str] = []) -> str: """Generate a requirements file from pyproject.toml.""" lines: list[str] = [] for index in indexes: lines.append(f'-i {index}') for dep in dependencies: lines.append(dep) return '\n'.join(lines) + '\n' def parse_pyproject(pyproject_path: Path) -> dict: """Parse pyproject.toml file.""" try: with pyproject_path.open('rb') as f: return tomllib.load(f) except FileNotFoundError: print(f'Error: {pyproject_path} not found', file=sys.stderr) sys.exit(1) except Exception as e: print(f'Error parsing {pyproject_path}: {e}', file=sys.stderr) sys.exit(1) def main(): """Entry point for the script.""" epilog = """ Examples: Generate base requirements: python scripts/generate_requirements.py Generate dev requirements: python scripts/generate_requirements.py --group dev Custom output file: python scripts/generate_requirements.py --group integration --output requirements-int.txt """ parser = argparse.ArgumentParser( description='Generate requirements files from pyproject.toml using uv', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog, ) parser.add_argument( '--group', '-g', help="Dependency group to export (e.g., 'dev', 'integration')", ) parser.add_argument( '--output', '-o', help='Output filename (default: requirements.txt or requirements-{group}.txt)', ) parser.add_argument( '--verbose', '-v', action='store_true', help='Enable verbose output', ) args = parser.parse_args() # Set global verbose flag global _VERBOSE _VERBOSE = args.verbose generate_requirements(group=args.group, output=args.output) if __name__ == '__main__': main()