Introdução ao Sistema de Processadores do mkdocstrings
O mkdocstrings é um plugin para o MkDocs que automatiza a geração de documentação a partir do código-fonte. Sua arquitetura baseada em processadores permite a extensão para diferentes linguagens de programação. Um processador atua como um módulo especializado em interpretar uma linguagem específica, extraindo metadados e estruturando-os para a geração de documentos.
Todos os processadores devem herdar da classe base BaseHandler. Esta classe define a interface principal que qualquer processador deve implementar para coletar e renderizar informações do código.
Componentes Fundamentais de um Processador
- Coletor de Símbolos: Responsável por parsear a fonte e identificar entidades (classes, funções, variáveis).
- Renderizador de Templates: Transforma os dados coletados em HTML usando o motor de templates Jinja2.
- Gerenciador de Inventário: Suporta referências cruzadas entre diferentes projetos de documentação.
Preparação do Ambiente de Desenvolvimento
Para iniciar o desenvolvimento de um processador customizado, é recomendado criar um pacote Python separado. A estrutura de diretórios sugerida promove organização e facilita a distribuição.
mkdocstrings-handler-customlang/
├── src/
│ └── customlang_handler/
│ ├── __init__.py
│ ├── handler.py
│ ├── collector.py
│ └── templates/
│ └── default/
│ ├── class.html
│ └── function.html
├── pyproject.toml
└── README.md
Implementação do Handler Personalizado
O núcleo do desenvolvimento reside na criação de uma classe que implementa a interface do BaseHandler. A classe deve definir metadados básicos e sobrescrever os métodos de coleta e renderização.
Definindo a Classe Handler
No arquivo handler.py, defina a classe principal do seu processador.
from mkdocstrings.handlers.base import BaseHandler
from typing import Any, Iterator
from pathlib import Path
class CustomLangHandler(BaseHandler):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
# Inicialização específica para a linguagem, como carregar um parser
self._symbol_parser = CustomSymbolExtractor()
@property
def name(self) -> str:
return "customlang"
@property
def domain(self) -> str:
return "cl"
def collect(self, identifier: str, config: dict) -> dict:
"""Método central para extrair informação de um símbolo dado seu caminho completo."""
module_path, symbol_name = self._split_identifier(identifier)
source_tree = self._load_module_source(module_path)
if symbol_name:
return self._symbol_parser.extract_symbol(source_tree, symbol_name, config)
return self._symbol_parser.extract_module_info(source_tree, config)
def render(self, data: dict, config: dict) -> str:
"""Renderiza os dados extraídos para HTML usando templates Jinja2."""
template_path = self._get_template_for_type(data["kind"])
template = self.env.get_template(template_path)
return template.render(**data, config=config)
def _split_identifier(self, dotted_path: str) -> tuple:
# Lógica para separar caminho do módulo do nome do símbolo
parts = dotted_path.rsplit(".", 1)
return (parts[0], parts[1]) if len(parts) > 1 else (parts[0], None)
Criando o Coletor de Símbolos
Para manter o código organizado, a lógica de parsing do código-fonte deve ser encapsulada em uma classe separada, como collector.py.
import ast
from typing import Optional
class CustomSymbolExtractor:
def extract_symbol(self, source_code: str, symbol_name: str, options: dict) -> dict:
"""Extrai informações detalhadas de uma classe ou função específica."""
tree = ast.parse(source_code)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name == symbol_name:
return self._node_to_dict(node)
raise ValueError(f"Símbolo '{symbol_name}' não encontrado no módulo fornecido.")
def _node_to_dict(self, node) -> dict:
# Converte um nó da AST em um dicionário estruturado
return {
"name": node.name,
"kind": type(node).__name__,
"lineno": node.lineno,
"docstring": ast.get_docstring(node) or "",
"bases": [self._get_base_name(b) for b in getattr(node, 'bases', [])],
}
def _get_base_name(self, node) -> str:
# Lógica para obter o nome da classe base
if isinstance(node, ast.Name):
return node.id
elif isinstance(node, ast.Attribute):
return f"{self._get_base_name(node.value)}.{node.attr}"
return "Unknown"
Desenvolvimento de Templates
Os templates determinam como a documentação final será apresentada. Crie templates dentro do diretório templates/default (ou dentro de um tema específico como material). Use Jinja2 para acessar os dados fornecidos pelo método render.
Exemplo para class.html:
<div class="customlang-class">
<h3 id="{{ name }}">{{ name }}</h3>
{% if bases %}
<p>Herda de: {{ bases | join(', ') }}</p>
{% endif %}
<div class="docstring">{{ docstring | markdown }}</div>
</div>
Registro e Configuração do Plugin
No arquivo __init__.py, exponha uma função fábrica para o mkdocstrings instanciar seu handler.
from .handler import CustomLangHandler
from mkdocstrings.handlers.base import BaseHandler
def get_handler(
handler_name: str,
theme: str,
custom_templates: Optional[str],
**kwargs
) -> BaseHandler:
return CustomLangHandler(theme=theme, custom_templates=custom_templates)
No pyproject.toml, registre o ponto de entrada para o plugin:
[project.entry-points."mkdocstrings.handlers"]
customlang = "customlang_handler:get_handler"
Testes e Validação
Implemente testes unitários para garantir a correção do seu parser e handler.
import pytest
from customlang_handler.collector import CustomSymbolExtractor
def test_extract_function():
source = """
def example_func(param1: int) -> str:
\"\"\"Uma função de exemplo.\"\"\"
return str(param1)
"""
extractor = CustomSymbolExtractor()
result = extractor.extract_symbol(source, "example_func", {})
assert result["name"] == "example_func"
assert result["kind"] == "FunctionDef"
assert "Uma função de exemplo" in result["docstring"]
Para teste de integração, crie um projeto MkDocs mínimo (mkdocs.yml):
site_name: Documentação CustomLang
plugins:
- mkdocstrings:
handlers:
customlang:
options:
show_source: true
Publicação do Pacote
Após a validação, empacote e publique seu handler no PyPI para uso comunitário.
python -m build
twine upload dist/*
Boas Práticas e Considerações Avançadas
- Tratamento de Erros: Implemente uma coleta resiliente que retorne informações parciais quando possível.
- Performance: Para linguagens grandes, consdiere caching do resultado do parsing ou uso de ferramentas externas mais eficientes.
- Compatibilidade de Temas: Forneça templates para os temas padrão (
readthedocs,material) para melhor experiência do usuário. - Suporte a Inventário: Implemente
load_inventoryse deseja que sua documentação seja referenciável por outros projetos.