added cli

This commit is contained in:
2026-01-20 20:47:28 +05:30
parent 7c027834c0
commit 8b2d6a5046
13 changed files with 390 additions and 0 deletions

View File

@@ -8,10 +8,12 @@ until their contracts are finalized.
from .loader import GriffeLoader
from .renderers import MkDocsRenderer
from .cli import main
from . import model
__all__ = [
"GriffeLoader",
"MkDocsRenderer",
"model",
"main",
]

View File

@@ -1,5 +1,6 @@
from .loader import GriffeLoader
from .renderers import MkDocsRenderer
from .cli import main
from . import model
__all__: list[str]

3
docforge/cli/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .main import main
__all__ = ["main"]

View File

@@ -0,0 +1,3 @@
from .main import main
__all__ = ["main"]

142
docforge/cli/main.py Normal file
View File

@@ -0,0 +1,142 @@
from __future__ import annotations
from pathlib import Path
from typing import Sequence, Optional
import click
from docforge.loader import GriffeLoader
from docforge.renderers.mkdocs import MkDocsRenderer
@click.group()
def cli() -> None:
"""doc-forge command-line interface."""
pass
# ---------------------------------------------------------------------
# tree
# ---------------------------------------------------------------------
@cli.command()
@click.option(
"--modules",
multiple=True,
required=True,
help="Python module import paths to introspect",
)
@click.option(
"--project-name",
help="Project name (defaults to first module)",
)
def tree(
modules: Sequence[str],
project_name: Optional[str],
) -> None:
"""Show introspection tree."""
loader = GriffeLoader()
project = loader.load_project(list(modules), project_name)
click.echo(project.name)
for module in project.get_all_modules():
click.echo(f"├── {module.path}")
for obj in module.get_all_objects():
_print_object(obj, indent="")
def _print_object(obj, indent: str) -> None:
click.echo(f"{indent}├── {obj.name}")
for member in obj.get_all_members():
_print_object(member, indent + "")
# ---------------------------------------------------------------------
# generate
# ---------------------------------------------------------------------
@cli.command()
@click.option(
"--modules",
multiple=True,
required=True,
help="Python module import paths to document",
)
@click.option(
"--project-name",
help="Project name (defaults to first module)",
)
@click.option(
"--docs-dir",
type=click.Path(path_type=Path),
default=Path("docs"),
)
def generate(
modules: Sequence[str],
project_name: Optional[str],
docs_dir: Path,
) -> None:
"""Generate documentation source files using MkDocs renderer."""
loader = GriffeLoader()
project = loader.load_project(list(modules), project_name)
renderer = MkDocsRenderer()
renderer.generate_sources(project, docs_dir)
click.echo(f"Documentation sources generated in {docs_dir}")
# ---------------------------------------------------------------------
# build
# ---------------------------------------------------------------------
@cli.command()
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
)
def build(mkdocs_yml: Path) -> None:
"""Build documentation using MkDocs."""
if not mkdocs_yml.exists():
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
from mkdocs.config import load_config
from mkdocs.commands.build import build as mkdocs_build
mkdocs_build(load_config(str(mkdocs_yml)))
click.echo("MkDocs build completed")
# ---------------------------------------------------------------------
# serve
# ---------------------------------------------------------------------
@cli.command()
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
)
def serve(mkdocs_yml: Path) -> None:
"""Serve documentation using MkDocs."""
if not mkdocs_yml.exists():
raise click.ClickException(f"mkdocs.yml not found: {mkdocs_yml}")
from mkdocs.commands.serve import serve as mkdocs_serve
mkdocs_serve(config_file=str(mkdocs_yml))
# ---------------------------------------------------------------------
# entry point
# ---------------------------------------------------------------------
def main() -> None:
cli()
if __name__ == "__main__":
main()

77
docforge/cli/main.pyi Normal file
View File

@@ -0,0 +1,77 @@
from typing import Sequence
from pathlib import Path
import click
@click.group()
def cli() -> None:
"""doc-forge command-line interface."""
@cli.command()
@click.option(
"--modules",
multiple=True,
help="Python module import paths to introspect",
)
@click.option(
"--project-name",
help="Project name (defaults to first module)",
)
def tree(
modules: Sequence[str],
project_name: str | None,
) -> None:
"""Show introspection tree."""
@cli.command()
@click.option(
"--modules",
multiple=True,
help="Python module import paths to document",
)
@click.option(
"--project-name",
help="Project name (defaults to first module)",
)
@click.option(
"--docs-dir",
type=click.Path(path_type=Path),
default=Path("docs"),
)
def generate(
modules: Sequence[str],
project_name: str | None,
docs_dir: Path,
) -> None:
"""Generate documentation source files using MkDocs renderer."""
@cli.command()
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
)
def build(
mkdocs_yml: Path,
) -> None:
"""Build documentation using MkDocs."""
@cli.command()
@click.option(
"--mkdocs-yml",
type=click.Path(path_type=Path),
default=Path("mkdocs.yml"),
)
def serve(
mkdocs_yml: Path,
) -> None:
"""Serve documentation using MkDocs."""
def main() -> None:
"""CLI entry point."""

0
tests/cli/__init__.py Normal file
View File

67
tests/cli/conftest.py Normal file
View File

@@ -0,0 +1,67 @@
from pathlib import Path
from typing import Callable
import pytest
from click.testing import CliRunner
@pytest.fixture
def cli_runner() -> CliRunner:
"""Click CLI runner."""
return CliRunner()
@pytest.fixture
def fake_mkdocs_yml(tmp_path: Path) -> Path:
"""Create a minimal mkdocs.yml file."""
yml = tmp_path / "mkdocs.yml"
yml.write_text(
"""
site_name: Test Docs
nav: []
plugins:
- mkdocstrings
""",
encoding="utf-8",
)
return yml
@pytest.fixture
def mock_mkdocs_load_config(monkeypatch):
"""Mock mkdocs.config.load_config."""
def fake_load_config(path):
return object() # dummy config object
monkeypatch.setattr(
"mkdocs.config.load_config",
fake_load_config,
)
@pytest.fixture
def mock_mkdocs_build(monkeypatch):
called = {"value": False}
def fake_build(config):
called["value"] = True
monkeypatch.setattr(
"mkdocs.commands.build.build",
fake_build,
)
return lambda: called["value"]
@pytest.fixture
def mock_mkdocs_serve(monkeypatch):
called = {"value": False}
def fake_serve(*args, **kwargs):
called["value"] = True
monkeypatch.setattr(
"mkdocs.commands.serve.serve",
fake_serve,
)
return lambda: called["value"]

20
tests/cli/test_build.py Normal file
View File

@@ -0,0 +1,20 @@
from docforge.cli.main import cli
def test_build_command(
cli_runner,
fake_mkdocs_yml,
mock_mkdocs_load_config,
mock_mkdocs_build,
):
result = cli_runner.invoke(
cli,
[
"build",
"--mkdocs-yml",
str(fake_mkdocs_yml),
],
)
assert result.exit_code == 0
assert mock_mkdocs_build() is True

View File

@@ -0,0 +1,32 @@
from pathlib import Path
from docforge.cli.main import cli
def test_generate_command(cli_runner, temp_package, tmp_path: Path):
(temp_package / "mod.py").write_text(
'''
def f(): ...
'''
)
docs_dir = tmp_path / "docs"
result = cli_runner.invoke(
cli,
[
"generate",
"--modules",
"testpkg.mod",
"--docs-dir",
str(docs_dir),
],
)
assert result.exit_code == 0
md = docs_dir / "testpkg" / "mod.md"
assert md.exists()
content = md.read_text()
assert "::: testpkg.mod" in content

19
tests/cli/test_serve.py Normal file
View File

@@ -0,0 +1,19 @@
from docforge.cli.main import cli
def test_serve_command(
cli_runner,
fake_mkdocs_yml,
mock_mkdocs_serve,
):
result = cli_runner.invoke(
cli,
[
"serve",
"--mkdocs-yml",
str(fake_mkdocs_yml),
],
)
assert result.exit_code == 0
assert mock_mkdocs_serve() is True

24
tests/cli/test_tree.py Normal file
View File

@@ -0,0 +1,24 @@
from docforge.cli.main import cli
def test_tree_command(cli_runner, temp_package):
(temp_package / "mod.py").write_text(
'''
class A:
def f(self): ...
'''
)
result = cli_runner.invoke(
cli,
[
"tree",
"--modules",
"testpkg.mod",
],
)
assert result.exit_code == 0
assert "testpkg" in result.output
assert "mod" in result.output
assert "A" in result.output