Criando e descobrindo plug-ins

Frequentemente, ao criar uma aplicação ou biblioteca Python, você desejará ter a capacidade de fornecer personalizações ou recursos extras por meio de plug-ins. Como os pacotes Python podem ser distribuídos separadamente, sua aplicação ou biblioteca pode querer descobrir automaticamente todos os plugins disponíveis.

Existem três abordagens principais para fazer a descoberta automática de plugins:

  1. Usando convenção de nomenclatura.

  2. Usando pacotes de espaço de nomes.

  3. Usando metadados de pacote.

Usando convenção de nomenclatura

Se todos os plugins para sua aplicação seguem a mesma convenção de nomenclatura, você pode usar pkgutil.iter_modules() para descobrir todos os módulos de nível superior que correspondem à convenção de nomenclatura. Por exemplo, Flask usa a convenção de nomenclatura flask_{nome_plugin}. Se você quiser descobrir automaticamente todos os plugins Flask instalados:

import importlib
import pkgutil

discovered_plugins = {
    name: importlib.import_module(name)
    for finder, name, ispkg
    in pkgutil.iter_modules()
    if name.startswith('flask_')
}

Se você tivesse os plugins Flask-SQLAlchemy e Flask-Talisman instalados, então discover_plugins seria:

{
    'flask_sqlachemy': <module: 'flask_sqlalchemy'>,
    'flask_talisman': <module: 'flask_talisman'>,
}

Usar a convenção de nomenclatura para plugins também permite que você consulte a API simples do Índice de Pacotes Python para todos os pacotes que estão em conformidade com a sua convenção de nomenclatura.

Usando pacotes de espaço de nomes

Pacotes de espaços de nome podem ser usados para fornecer uma convenção de onde colocar plugins e também fornece uma maneira de realizar a descoberta. Por exemplo, se você fizer do subpacote myapp.plugins um pacote de espaço de nomes, então outras distribuições pode fornecer módulos e pacotes para aquele espaço de nomes. Uma vez instalado, você pode usar pkgutil.iter_modules() para descobrir todos os módulos e pacotes instalados sob esse espaço de nomes:

import importlib
import pkgutil

import myapp.plugins

def iter_namespace(ns_pkg):
    # Specifying the second argument (prefix) to iter_modules makes the
    # returned name an absolute name instead of a relative one. This allows
    # import_module to work without having to do additional modification to
    # the name.
    return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")

discovered_plugins = {
    name: importlib.import_module(name)
    for finder, name, ispkg
    in iter_namespace(myapp.plugins)
}

Especificar myapp.plugins.__path__ para iter_modules() faz com que ele procure apenas os módulos diretamente sob aquele espaço de nomes. Por exemplo, se você instalou distribuições que fornecem os módulos myapp.plugins.a e myapp.plugins.b, então found_plugins neste caso seria:

{
    'a': <module: 'myapp.plugins.a'>,
    'b': <module: 'myapp.plugins.b'>,
}

Este exemplo usa um subpacote como o pacote de espaço de nomes (myapp.plugins), mas também é possível usar um pacote de nível superior para este propósito (como myapp_plugins). Como escolher o espaço de nomes a ser usado é uma questão de preferência, mas não é recomendado fazer do pacote de nível superior principal do seu projeto (myapp, neste caso) um pacote de espaço de nomes para fins de plugins, como um plugin ruim poderia fazer com que todo o espaço de nomes seja interrompido, o que, por sua vez, tornaria seu projeto não importável. Para que a abordagem de “subpacote de espaço de nomes” funcione, os pacotes de plugin devem omitir o __init__.py para o diretório de pacote de nível superior (myapp, neste caso) e incluir o estilo de pacote de espaço de nomes __init__.py no diretório do subpacote de espaço de nomes (myapp/plugins). Isso também significa que os plugins precisarão passar explicitamente uma lista de pacotes para o argumento packages de setup() ao invés de usar setuptools.find_packages().

Aviso

Os pacotes de espaço de nomes são um recurso complexo e existem várias maneiras diferentes de criá-los. É altamente recomendado ler a documentação de Empacotando pacotes de espaço de nomes e documentar claramente qual abordagem é preferida para plugins em seu projeto.

Usando metadados de pacote

Setuptools fornece suporte especial para plugins. Fornecendo o argumento entry_points para setup() em setup.py, os plugins podem se registrar para serem descobertos.

Por exemplo, se você tem um pacote chamado myapp-plugin-a e ele inclui em seu setup.py:

setup(
    ...
    entry_points={'myapp.plugins': 'a = myapp_plugin_a'},
    ...
)

Então você pode descobrir e carregar todos os pontos de entrada registrados usando importlib.metadata.entry_points() (ou o backport importlib_metadata >= 3.6 para Python 3.6-3.9):

import sys
if sys.version_info < (3, 10):
    from importlib_metadata import entry_points
else:
    from importlib.metadata import entry_points

discovered_plugins = entry_points(group='myapp.plugins')

Neste exemplo, discover_plugins seria uma coleção do tipo importlib.metadata.EntryPoint:

(
    EntryPoint(name='a', value='myapp_plugin_a', group='myapp.plugins'),
    ...
)

Agora o módulo de sua escolha pode ser importado executando discovered_plugins['a'].Load().

Nota

A especificação entry_point em setup.py é bastante flexível e tem muitas opções. É recomendado ler toda a seção sobre pontos de entrada .

Nota

Visto que esta especificação é parte da biblioteca padrão, a maioria das ferramentas de empacotamento, exceto setuptools, fornecem suporte para definir pontos de entrada.