refactor: restructure CLI and improve documentation/typing

- Consolidated CLI commands into [build](cci:1://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/mkdocs/logic.py:48:0-58:46) and [serve](cci:1://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/commands.py:70:0-92:32) using `--mkdocs` and `--mcp` flags.
- Separated CLI orchestration logic into [docforge/cli/mkdocs/logic.py](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/mkdocs/logic.py:0:0-0:0) and [docforge/cli/mcp/logic.py](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/mcp/logic.py:0:0-0:0).
- Moved command definitions to [docforge/cli/commands.py](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/commands.py:0:0-0:0), making [main.py](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/main.py:0:0-0:0) a thin entry point.
- Aligned [.pyi](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/docforge/cli/main.pyi:0:0-0:0) type stubs with [.py](cci:7://file:///c:/Users/vishe/WorkSpace/code/aetos/doc-forge/tests/conftest.py:0:0-0:0) implementations across the package.
- Added missing docstrings to internal helper functions and core classes.
- Restructured tests into `tests/mkdocs/` and `tests/mcp/`.
- Updated navigation specification to reflect the new project structure.
This commit is contained in:
2026-01-21 17:59:46 +05:30
parent 15c59ab274
commit 03caf5ce4c
37 changed files with 2514 additions and 1593 deletions

View File

@@ -1,111 +0,0 @@
import json
import pytest
from pathlib import Path
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"]
@pytest.fixture
def fake_mcp_docs(tmp_path: Path) -> Path:
"""
Create a minimal valid MCP bundle in mcp_docs/.
"""
mcp_root = tmp_path / "mcp_docs"
modules_dir = mcp_root / "modules"
modules_dir.mkdir(parents=True)
(mcp_root / "index.json").write_text(
json.dumps({"project": "test", "type": "docforge-model"}),
encoding="utf-8",
)
(mcp_root / "nav.json").write_text(
json.dumps([]),
encoding="utf-8",
)
(modules_dir / "test.mod.json").write_text(
json.dumps({"module": "test.mod", "content": {}}),
encoding="utf-8",
)
return mcp_root
@pytest.fixture
def mock_mcp_server_run(monkeypatch):
"""
Mock MCPServer.run so no real server is started.
"""
called = {"value": False}
def fake_run(self, transport="streamable-http"):
called["value"] = True
monkeypatch.setattr(
"docforge.servers.MCPServer.run",
fake_run,
)
return lambda: called["value"]

View File

@@ -1,20 +0,0 @@
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

@@ -1,35 +1,31 @@
import json
from pathlib import Path
from click.testing import CliRunner
from docforge.cli.main import cli
def test_generate_command(cli_runner):
def test_mcp_build(cli_runner):
with cli_runner.isolated_filesystem():
cwd = Path.cwd()
# Create package structure
pkg = cwd / "testpkg"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "mod.py").write_text("def f(): ...\n")
docs_dir = cwd / "docs"
out_dir = cwd / "mcp_docs"
result = cli_runner.invoke(
cli,
[
"generate",
"build",
"--mcp",
"--module",
"testpkg",
"--docs-dir",
str(docs_dir),
"--out-dir",
str(out_dir),
],
)
assert result.exit_code == 0
md = docs_dir / "testpkg" / "mod.md"
assert md.exists()
content = md.read_text()
assert "::: testpkg.mod" in content
assert (out_dir / "index.json").exists()
assert (out_dir / "nav.json").exists()
assert (out_dir / "modules" / "testpkg.mod.json").exists()

View File

@@ -0,0 +1,54 @@
from pathlib import Path
from docforge.cli.main import cli
def test_mkdocs_build_full_flow(
cli_runner,
mock_mkdocs_build,
mock_mkdocs_load_config,
tmp_path,
):
# This test covers what used to be generate + mkdocs + build
with cli_runner.isolated_filesystem():
cwd = Path.cwd()
pkg = cwd / "testpkg"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
(pkg / "mod.py").write_text("def f(): ...\n")
nav_file = cwd / "docforge.nav.yml"
nav_file.write_text("home: testpkg/index.md\ngroups: {}\n")
# We need to create a dummy testpkg/index.md for nav resolution if it's there
# But generate_sources will create it.
# Wait, the current logic runs generate_sources first, THEN generate_config.
result = cli_runner.invoke(
cli,
[
"build",
"--mkdocs",
"--module",
"testpkg",
"--site-name",
"Test Site",
"--docs-dir",
"docs",
"--mkdocs-yml",
"mkdocs.yml",
],
)
assert result.exit_code == 0
assert mock_mkdocs_build() is True
assert (cwd / "mkdocs.yml").exists()
assert (cwd / "docs" / "testpkg" / "mod.md").exists()
def test_mkdocs_build_missing_module_fails(cli_runner):
result = cli_runner.invoke(cli, ["build", "--mkdocs", "--site-name", "Test"])
assert result.exit_code != 0
assert "--module is required" in result.output
def test_mkdocs_build_missing_site_name_fails(cli_runner):
result = cli_runner.invoke(cli, ["build", "--mkdocs", "--module", "testpkg"])
assert result.exit_code != 0
assert "--site-name is required" in result.output

View File

@@ -1,123 +0,0 @@
from pathlib import Path
import yaml
from click.testing import CliRunner
from docforge.cli.main import cli
def _write_nav_spec(path: Path) -> None:
path.write_text(
"""
home: openapi_first/index.md
groups:
Core:
- openapi_first/app.md
- openapi_first/client.md
""".strip(),
encoding="utf-8",
)
def _write_docs(docs: Path) -> None:
files = [
"openapi_first/index.md",
"openapi_first/app.md",
"openapi_first/client.md",
]
for rel in files:
p = docs / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(f"# {rel}", encoding="utf-8")
def test_mkdocs_uses_builtin_template(tmp_path: Path) -> None:
docs = tmp_path / "docs"
docs.mkdir()
nav_file = tmp_path / "docforge.nav.yml"
out_file = tmp_path / "mkdocs.yml"
_write_docs(docs)
_write_nav_spec(nav_file)
runner = CliRunner()
result = runner.invoke(
cli,
[
"mkdocs",
"--site-name",
"DocForge",
"--docs-dir",
str(docs),
"--nav",
str(nav_file),
"--out",
str(out_file),
],
)
assert result.exit_code == 0
assert out_file.exists()
def test_mkdocs_overrides_template(tmp_path: Path) -> None:
docs = tmp_path / "docs"
docs.mkdir()
nav_file = tmp_path / "docforge.nav.yml"
template = tmp_path / "custom.yml"
out_file = tmp_path / "mkdocs.yml"
_write_docs(docs)
_write_nav_spec(nav_file)
template.write_text(
"""
site_name: Custom Site
theme:
name: readthedocs
""".strip(),
encoding="utf-8",
)
runner = CliRunner()
result = runner.invoke(
cli,
[
"mkdocs",
"--site-name",
"Override Site",
"--docs-dir",
str(docs),
"--nav",
str(nav_file),
"--template",
str(template),
"--out",
str(out_file),
],
)
assert result.exit_code == 0
assert out_file.exists()
def test_mkdocs_missing_nav_fails(tmp_path: Path) -> None:
docs = tmp_path / "docs"
docs.mkdir()
runner = CliRunner()
result = runner.invoke(
cli,
[
"mkdocs",
"--site-name",
"DocForge",
"--docs-dir",
str(docs),
],
)
assert result.exit_code != 0
assert "Nav spec not found" in result.output

View File

@@ -1,7 +1,6 @@
from docforge.cli.main import cli
def test_serve_mcp(
def test_mcp_serve(
cli_runner,
fake_mcp_docs,
mock_mcp_server_run,
@@ -11,7 +10,7 @@ def test_serve_mcp(
result = cli_runner.invoke(
cli,
["serve-mcp"],
["serve", "--mcp", "--out-dir", str(fake_mcp_docs)],
)
assert result.exit_code == 0

View File

@@ -1,7 +1,6 @@
from docforge.cli.main import cli
def test_serve_command(
def test_mkdocs_serve(
cli_runner,
fake_mkdocs_yml,
mock_mkdocs_serve,
@@ -10,6 +9,7 @@ def test_serve_command(
cli,
[
"serve",
"--mkdocs",
"--mkdocs-yml",
str(fake_mkdocs_yml),
],

View File

@@ -1,24 +0,0 @@
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

View File

@@ -1,7 +1,9 @@
import sys
import json
import pytest
from pathlib import Path
import pytest
from click.testing import CliRunner
@pytest.fixture
@@ -16,3 +18,109 @@ def temp_package(tmp_path: Path):
sys.path.insert(0, str(tmp_path))
yield pkg
sys.path.remove(str(tmp_path))
@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"]
@pytest.fixture
def fake_mcp_docs(tmp_path: Path) -> Path:
"""
Create a minimal valid MCP bundle in mcp_docs/.
"""
mcp_root = tmp_path / "mcp_docs"
modules_dir = mcp_root / "modules"
modules_dir.mkdir(parents=True)
(mcp_root / "index.json").write_text(
json.dumps({"project": "test", "type": "docforge-model"}),
encoding="utf-8",
)
(mcp_root / "nav.json").write_text(
json.dumps([]),
encoding="utf-8",
)
(modules_dir / "test.mod.json").write_text(
json.dumps({"module": "test.mod", "content": {}}),
encoding="utf-8",
)
return mcp_root
@pytest.fixture
def mock_mcp_server_run(monkeypatch):
"""
Mock MCPServer.run so no real server is started.
"""
called = {"value": False}
def fake_run(self, transport="streamable-http"):
called["value"] = True
monkeypatch.setattr(
"docforge.servers.MCPServer.run",
fake_run,
)
return lambda: called["value"]