Desenvolvimento de um Processador Personalizado para mkdocstrings com Suporte a Novas Linguagens

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_inventory se deseja que sua documentação seja referenciável por outros projetos.

Tags: mkdocstrings Python MkDocs documentação automática processador de linguagem

Publicado em 6-25 01:26