Files
python-stub-generator/stubgen/cli.py
7000pctAUTO 085e7aa7d6
Some checks failed
CI / test (push) Has been cancelled
Initial commit: Add python-stub-generator project
2026-01-30 04:51:45 +00:00

305 lines
8.3 KiB
Python

"""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()