init docforge lib

This commit is contained in:
2026-01-20 18:22:16 +05:30
parent 86a4f8f41a
commit a45725160d
28 changed files with 3486 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
"""Loader package for doc-forge introspection layer."""
from .griffe_loader import GriffeLoader
__all__ = ["GriffeLoader"]

View File

@@ -0,0 +1,16 @@
"""Type stubs for doc-forge loader package."""
from typing import Any, Dict, List, Optional, Union
from pathlib import Path
from docforge.model import Project, Module
class GriffeLoader:
"""Loads Python modules using Griffe introspection."""
def __init__(self) -> None: ...
def load_project(self, module_paths: List[str]) -> Project: ...
def load_module(self, path: str) -> Module: ...
def resolve_aliases(self, project: Project) -> None: ...

View File

@@ -0,0 +1,289 @@
"""Griffe-based loader for doc-forge introspection layer.
The GriffeLoader uses the Griffe library to introspect Python source code
and extract documentation information. It converts Griffe's internal
representation into doc-forge's renderer-agnostic documentation model.
Griffe is the only supported introspection backend in doc-forge, ensuring
consistent and reliable extraction of documentation information from
Python source code.
"""
from __future__ import annotations
import logging
from typing import Dict, List, Optional
try:
import griffe
from griffe import Docstring, ObjectNode
except ImportError as e:
raise ImportError(
"griffe is required for doc-forge. Install with: pip install griffe"
) from e
from docforge.model import Module, Project, DocObject
logger = logging.getLogger(__name__)
class GriffeLoader:
"""Loads Python modules using Griffe introspection.
GriffeLoader is the bridge between Python source code and doc-forge's
documentation model. It uses Griffe to parse Python modules, extract
docstrings, signatures, and structural information, then converts
this data into doc-forge's renderer-agnostic model.
The loader handles:
- Module discovery and loading
- Docstring extraction
- Signature parsing
- Member resolution
- Alias handling (with graceful failure)
Attributes:
griffe_agent: The Griffe agent used for introspection
"""
def __init__(self) -> None:
"""Initialize the GriffeLoader.
Creates a Griffe agent with default configuration for
documentation extraction.
"""
self.griffe_agent = griffe.Agent(
extensions=(),
resolve_aliases=True,
resolve_imports=True,
)
def load_project(self, module_paths: List[str], project_name: Optional[str] = None) -> Project:
"""Load a complete project from multiple module paths.
This is the primary entry point for loading documentation. It takes
a list of module paths, loads each one, and assembles them into a
complete Project with navigation.
Args:
module_paths: List of import paths to load
project_name: Optional name for the project (defaults to first module)
Returns:
Project containing all loaded modules
Raises:
ValueError: If no module paths provided
ImportError: If any module cannot be loaded
"""
if not module_paths:
raise ValueError("At least one module path must be provided")
if project_name is None:
project_name = module_paths[0].split('.')[0]
project = Project(name=project_name)
for module_path in module_paths:
try:
module = self.load_module(module_path)
project.add_module(module)
logger.info(f"Loaded module: {module_path}")
except Exception as e:
logger.error(f"Failed to load module {module_path}: {e}")
# Continue loading other modules rather than failing completely
continue
# Resolve any cross-module aliases
self.resolve_aliases(project)
return project
def load_module(self, path: str) -> Module:
"""Load a single module from its import path.
Args:
path: The import path of the module to load
Returns:
Module containing all documented objects
Raises:
ImportError: If the module cannot be loaded or found
"""
try:
griffe_obj = self.griffe_agent.load_module(path)
except Exception as e:
raise ImportError(f"Failed to load module '{path}': {e}") from e
return self._convert_griffe_object_to_module(griffe_obj)
def _convert_griffe_object_to_module(self, griffe_obj: ObjectNode) -> Module:
"""Convert a Griffe ObjectNode to a doc-forge Module.
Args:
griffe_obj: The Griffe object to convert
Returns:
Module containing converted documentation objects
"""
module = Module(
path=griffe_obj.canonical_path,
docstring=self._extract_docstring(griffe_obj.docstring),
)
# Convert all members
for name, member in griffe_obj.members.items():
if not name.startswith('_'): # Skip private members
try:
doc_obj = self._convert_griffe_object_to_docobject(member)
module.add_object(doc_obj)
except Exception as e:
logger.warning(f"Failed to convert member {name}: {e}")
continue
return module
def _convert_griffe_object_to_docobject(self, griffe_obj: ObjectNode) -> DocObject:
"""Convert a Griffe ObjectNode to a doc-forge DocObject.
Args:
griffe_obj: The Griffe object to convert
Returns:
DocObject with converted information
"""
# Determine the kind of object
kind = self._determine_object_kind(griffe_obj)
# Extract signature for callable objects
signature = self._extract_signature(griffe_obj, kind)
doc_obj = DocObject(
name=griffe_obj.name,
kind=kind,
path=griffe_obj.canonical_path,
signature=signature,
docstring=self._extract_docstring(griffe_obj.docstring),
)
# Convert nested members (for classes)
if kind == "class":
for name, member in griffe_obj.members.items():
if not name.startswith('_'): # Skip private members
try:
nested_obj = self._convert_griffe_object_to_docobject(member)
doc_obj.add_member(nested_obj)
except Exception as e:
logger.warning(f"Failed to convert nested member {name}: {e}")
continue
return doc_obj
def _determine_object_kind(self, griffe_obj: ObjectNode) -> str:
"""Determine the kind of documentation object.
Args:
griffe_obj: The Griffe object to classify
Returns:
String representing the object kind
"""
if griffe_obj.is_class:
return "class"
elif griffe_obj.is_function:
return "function"
elif griffe_obj.is_method:
return "method"
elif griffe_obj.is_property:
return "property"
elif griffe_obj.is_attribute:
return "attribute"
elif griffe_obj.is_module:
return "module"
else:
return "object"
def _extract_signature(self, griffe_obj: ObjectNode, kind: str) -> Optional[str]:
"""Extract signature string from a Griffe object.
Args:
griffe_obj: The Griffe object to extract signature from
kind: The kind of object
Returns:
Signature string or None if not applicable
"""
if kind not in ("function", "method"):
return None
try:
if hasattr(griffe_obj, 'parameters') and griffe_obj.parameters:
params = []
for param in griffe_obj.parameters.values():
param_str = param.name
if param.annotation:
param_str += f": {param.annotation}"
if param.default and param.default != "None":
param_str += f" = {param.default}"
params.append(param_str)
signature = f"({', '.join(params)})"
if hasattr(griffe_obj, 'returns') and griffe_obj.returns:
signature += f" -> {griffe_obj.returns}"
return signature
except Exception as e:
logger.warning(f"Failed to extract signature for {griffe_obj.name}: {e}")
return None
def _extract_docstring(self, docstring: Optional[Docstring]) -> Optional[str]:
"""Extract docstring content from a Griffe Docstring object.
Args:
docstring: The Griffe docstring object
Returns:
Plain text docstring or None
"""
if docstring is None:
return None
try:
return str(docstring.value).strip()
except Exception as e:
logger.warning(f"Failed to extract docstring: {e}")
return None
def resolve_aliases(self, project: Project) -> None:
"""Resolve cross-module aliases in the project.
This method attempts to resolve aliases that point to objects in
other modules. It updates the documentation model to reflect the
actual locations of objects rather than their aliases.
Args:
project: The project to resolve aliases in
"""
logger.info("Resolving cross-module aliases...")
# This is a placeholder for alias resolution
# In a full implementation, this would:
# 1. Identify all aliases in the project
# 2. Resolve them to their canonical targets
# 3. Update the documentation model accordingly
# 4. Handle circular references gracefully
# For now, we'll just log that alias resolution was attempted
alias_count = 0
for module in project.get_all_modules():
for obj in module.members.values():
if hasattr(obj, 'is_alias') and obj.is_alias:
alias_count += 1
if alias_count > 0:
logger.info(f"Found {alias_count} aliases (resolution not yet implemented)")
else:
logger.info("No aliases found")