"""Command-line interface for stubgen.""" import sys from pathlib import Path import click from stubgen import __version__ from stubgen.config import load_config, should_exclude from stubgen.generator import StubGenerator from stubgen.inferrer import Inferrer @click.group() @click.version_option(version=__version__, prog_name="stubgen") @click.option( "--verbose", "-v", is_flag=True, help="Enable verbose output" ) @click.option( "--config", "-c", type=click.Path(exists=True, dir_okay=False), help="Path to configuration file" ) @click.pass_context def main(ctx: click.Context, verbose: bool, config: Path): """Generate Python type stub files (.pyi) from untyped Python code.""" ctx.ensure_object(dict) ctx.obj["verbose"] = verbose ctx.obj["config"] = config @main.command() @click.argument( "input_path", type=click.Path(exists=True, file_okay=True, dir_okay=True) ) @click.option( "--output", "-o", type=click.Path(file_okay=False), help="Output directory for generated stubs" ) @click.option( "--recursive", "-r", is_flag=True, default=True, help="Recursively process directories (default: True)" ) @click.option( "--no-recursive", is_flag=True, help="Disable recursive directory processing" ) @click.option( "--exclude", "-e", multiple=True, help="Patterns to exclude (can be specified multiple times)" ) @click.option( "--dry-run", is_flag=True, help="Show what would be generated without writing files" ) @click.option( "--interactive", "-i", is_flag=True, help="Prompt for confirmation on inferred types" ) @click.option( "--infer-depth", type=int, default=3, help="Depth of type inference (default: 3)" ) @click.option( "--verbose", "-v", is_flag=True, help="Enable verbose output" ) @click.pass_context def stubgen( ctx: click.Context, input_path: Path, output: Path, recursive: bool, no_recursive: bool, exclude: tuple, dry_run: bool, interactive: bool, infer_depth: int, verbose: bool ): """Generate type stubs for Python files. INPUT_PATH can be a Python file or a directory. """ ctx.ensure_object(dict) config_path = ctx.obj.get("config") do_recursive = not no_recursive is_dir = Path(input_path).is_dir() if is_dir and no_recursive: do_recursive = False exclude_patterns = list(exclude) config = None if config_path: config = load_config(config_path) if verbose: click.echo(f"Loaded config from {config_path}") if config: config_excludes = config.get("exclude_patterns", []) exclude_patterns.extend(config_excludes) if output is None and config.get("output_dir"): output = Path(config.get("output_dir")) if infer_depth == 3 and config.get("infer_depth"): infer_depth = config.get("infer_depth") if not interactive and config.get("interactive"): interactive = True input_path = Path(input_path) if not input_path.exists(): click.echo(f"Error: Path does not exist: {input_path}", err=True) sys.exit(1) if not is_dir: input_path = input_path.resolve() generator = StubGenerator(inferrer=Inferrer(infer_depth=infer_depth)) try: if input_path.is_file() and input_path.suffix == ".py": if should_exclude(input_path, exclude_patterns): if verbose: click.echo(f"Excluded: {input_path}") return if verbose: click.echo(f"Processing: {input_path}") output_path = None if output is not None: output_path = Path(output) / input_path.with_suffix(".pyi").name else: output_path = input_path.with_suffix(".pyi") if dry_run: from stubgen.parser import FileParser file_info = FileParser(input_path).parse() stub_content = generator.writer.generate(file_info) click.echo(f"Would generate: {output_path}") click.echo("---") click.echo(stub_content) click.echo("---") else: result = generator.generate_file(input_path, output_path) if verbose: click.echo(f"Generated: {result}") elif input_path.is_dir(): output_dir = output or input_path py_files = [] if do_recursive: py_files = list(input_path.rglob("*.py")) else: py_files = list(input_path.glob("*.py")) py_files = [f for f in py_files if not should_exclude(f, exclude_patterns)] py_files = sorted(py_files) if verbose: click.echo(f"Found {len(py_files)} Python files") for py_file in py_files: if verbose: click.echo(f"Processing: {py_file}") rel_path = py_file.relative_to(input_path) output_path = output_dir / rel_path.with_suffix(".pyi") if dry_run: from stubgen.parser import FileParser file_info = FileParser(py_file).parse() stub_content = generator.writer.generate(file_info) click.echo(f"Would generate: {output_path}") else: generator.generate_file(py_file, output_path) if verbose: click.echo(f"Generated: {output_path}") if dry_run: click.echo(f"Would generate {len(py_files)} stub files in {output_dir}") else: click.echo(f"Error: {input_path} is not a Python file or directory", err=True) sys.exit(1) except Exception as e: click.echo(f"Error: {e}", err=True) if verbose: import traceback traceback.print_exc() sys.exit(1) @main.command() @click.option( "--output", "-o", type=click.Path(file_okay=False), help="Output directory for generated stubs" ) @click.argument("files", nargs=-1, type=click.Path(exists=True)) @click.pass_context def generate( ctx: click.Context, output: Path, files: tuple ): """Generate type stubs for specified files.""" ctx.ensure_object(dict) if not files: click.echo("Error: No files specified", err=True) sys.exit(1) verbose = ctx.obj.get("verbose", False) generator = StubGenerator() output_dir = output or Path.cwd() for file_path in files: file_path = Path(file_path) if file_path.suffix != ".py": continue if verbose: click.echo(f"Processing: {file_path}") output_path = output_dir / file_path.with_suffix(".pyi").name try: result = generator.generate_file(file_path, output_path) if verbose: click.echo(f"Generated: {result}") except Exception as e: click.echo(f"Error processing {file_path}: {e}", err=True) @main.command("init-config") @click.option( "--path", "-p", type=click.Path(dir_okay=False), default="stubgen.toml", help="Path for the config file" ) def init_config(path: Path): """Generate a stubgen configuration file.""" config_content = '''# stubgen Configuration File [tool.stubgen] # Patterns to exclude from processing exclude_patterns = [ "tests/*", "*/__pycache__/*", "*/.venv/*", "*/venv/*", "*/node_modules/*", ] # Depth of type inference (default: 3) infer_depth = 3 # Enable strict mode (fail on errors) strict_mode = false # Enable interactive type confirmation interactive = false # Default output directory # output_dir = "./stubs" ''' path = Path(path) try: with open(path, 'w') as f: f.write(config_content) click.echo(f"Generated configuration file: {path}") except Exception as e: click.echo(f"Error: {e}", err=True) sys.exit(1) if __name__ == "__main__": main()