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:
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_sqlalchemy': <module: 'flask_sqlalchemy'>,
'flask_talisman': <module: 'flask_talisman'>,
}
Usar a convenção de nomenclatura para plugins também permite que você consulte a API de repositório 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¶
Os pacotes podem ter metadados para plugins descritos em Especificação de pontos de entrada. Ao especificá-los, um pacote anuncia que contém um tipo específico de plug-in. Outro pacote que suporta esse tipo de plug-in pode usar os metadados para descobrir esse plug-in.
Por exemplo, se você tem um pacote chamado myapp-plugin-a
e ele inclui o seguinte em seu pyproject.toml
:
[project.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.