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