Initial commit: Add python-stub-generator project
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
304
stubgen/cli.py
Normal file
304
stubgen/cli.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user