Skip to content

Commit

Permalink
Merge pull request #2261 from sopel-irc/remove-pkg_resources
Browse files Browse the repository at this point in the history
core: replace `pkg_resources` with `importlib.metadata`
  • Loading branch information
dgw authored Mar 1, 2022
2 parents b1e8753 + a7dc9ce commit aca1055
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 45 deletions.
2 changes: 1 addition & 1 deletion docs/source/plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Plugin glossary

Entry point plugin
A plugin that is an installed Python package and exposed through the
``sopel.plugins`` setuptools entry point.
``sopel.plugins`` entry point group.

Sopelunking
Action performed by a :term:`Sopelunker`.
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ geoip2>=4.0,<5.0
requests>=2.24.0,<3.0.0
dnspython<3.0
sqlalchemy>=1.4,<1.5
importlib_metadata>=3.6; python_version < '3.10'
packaging
8 changes: 6 additions & 2 deletions sopel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
import re
import sys

import pkg_resources
try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove fallback when dropping py3.7
import importlib_metadata

__all__ = [
'bot',
Expand All @@ -41,7 +45,7 @@
'something like "en_US.UTF-8".', file=sys.stderr)


__version__ = pkg_resources.get_distribution('sopel').version
__version__ = importlib_metadata.version('sopel')


def _version_info(version=__version__):
Expand Down
2 changes: 1 addition & 1 deletion sopel/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import traceback
from typing import Callable, Optional

from pkg_resources import parse_version
from packaging.version import parse as parse_version

from sopel import __version__

Expand Down
27 changes: 16 additions & 11 deletions sopel/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* extra directories defined in the settings
* homedir's ``plugins`` directory
* ``sopel.plugins`` setuptools entry points
* ``sopel.plugins`` entry point group
* ``sopel_modules``'s subpackages
* ``sopel.modules``'s core plugins
Expand All @@ -33,7 +33,12 @@
import itertools
import os

import pkg_resources
try:
import importlib_metadata
except ImportError:
# TODO: use stdlib only when possible, after dropping py3.9
# stdlib does not support `entry_points(group='filter')` until py3.10
import importlib.metadata as importlib_metadata

from . import exceptions, handlers, rules # noqa

Expand Down Expand Up @@ -93,17 +98,17 @@ def find_sopel_modules_plugins():


def find_entry_point_plugins(group='sopel.plugins'):
"""List plugins from a setuptools entry point group.
"""List plugins from an entry point group.
:param str group: setuptools entry point group to look for
(defaults to ``sopel.plugins``)
:param str group: entry point group to search in (defaults to
``sopel.plugins``)
:return: yield instances of :class:`~.handlers.EntryPointPlugin`
created from setuptools entry point given ``group``
created from each entry point in the ``group``
This function finds plugins declared under a setuptools entry point; by
default it uses the ``sopel.plugins`` entry point.
This function finds plugins declared under an entry point group; by
default it looks in the ``sopel.plugins`` group.
"""
for entry_point in pkg_resources.iter_entry_points(group):
for entry_point in importlib_metadata.entry_points(group=group):
yield handlers.EntryPointPlugin(entry_point)


Expand Down Expand Up @@ -139,7 +144,7 @@ def enumerate_plugins(settings):
* :func:`find_internal_plugins` for internal plugins
* :func:`find_sopel_modules_plugins` for ``sopel_modules.*`` plugins
* :func:`find_entry_point_plugins` for plugins exposed by setuptools
* :func:`find_entry_point_plugins` for plugins exposed via packages'
entry points
* :func:`find_directory_plugins` for plugins in ``$homedir/plugins``,
and in extra directories as defined by ``settings.core.extra``
Expand Down Expand Up @@ -201,7 +206,7 @@ def get_usable_plugins(settings):
* extra directories defined in the settings
* homedir's ``plugins`` directory
* ``sopel.plugins`` setuptools entry points
* ``sopel.plugins`` entry point group
* ``sopel_modules``'s subpackages
* ``sopel.modules``'s core plugins
Expand Down
42 changes: 22 additions & 20 deletions sopel/plugins/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
At the moment, three types of plugin are handled:
* :class:`PyModulePlugin`: manage plugins that can be imported as Python
* :class:`PyModulePlugin`: manages plugins that can be imported as Python
module from a Python package, i.e. where ``from package import name`` works
* :class:`PyFilePlugin`: manage plugins that are Python files on the filesystem
* :class:`PyFilePlugin`: manages plugins that are Python files on the filesystem
or Python directory (with an ``__init__.py`` file inside), that cannot be
directly imported and extra steps are necessary
* :class:`EntryPointPlugin`: manage plugins that are declared by a setuptools
entry point; other than that, it behaves like a :class:`PyModulePlugin`
* :class:`EntryPointPlugin`: manages plugins that are declared by an entry
point; it otherwise behaves like a :class:`PyModulePlugin`
All expose the same interface and thereby abstract the internal implementation
away from the rest of the application.
Expand Down Expand Up @@ -512,17 +512,17 @@ def reload(self):


class EntryPointPlugin(PyModulePlugin):
"""Sopel plugin loaded from a ``setuptools`` entry point.
"""Sopel plugin loaded from an entry point.
:param entry_point: a ``setuptools`` entry point object
:param entry_point: an entry point object
This handler loads a Sopel plugin exposed by a ``setuptools`` entry point.
It expects to be able to load a module object from the entry point, and to
This handler loads a Sopel plugin exposed by a package's entry point. It
expects to be able to load a module object from the entry point, and to
work as a :class:`~.PyModulePlugin` from that module.
By default, Sopel uses the entry point ``sopel.plugins``. To use that for
their plugin, developers must define an entry point either in their
``setup.py`` file or their ``setup.cfg`` file::
By default, Sopel searches within the entry point group ``sopel.plugins``.
To use that for their own plugins, developers must define an entry point
either in their ``setup.py`` file or their ``setup.cfg`` file::
# in setup.py file
setup(
Expand All @@ -537,11 +537,11 @@ class EntryPointPlugin(PyModulePlugin):
And this plugin can be loaded with::
>>> from pkg_resources import iter_entry_points
>>> from importlib_metadata import entry_points
>>> from sopel.plugins.handlers import EntryPointPlugin
>>> plugin = [
... EntryPointPlugin(ep)
... for ep in iter_entry_points('sopel.plugins', 'custom')
... for ep in entry_points(group='sopel.plugins', name='custom')
... ][0]
>>> plugin.load()
>>> plugin.name
Expand All @@ -556,10 +556,13 @@ class EntryPointPlugin(PyModulePlugin):
Sopel uses the :func:`~sopel.plugins.find_entry_point_plugins` function
internally to search entry points.
Entry point is a `standard feature of setuptools`__ for Python, used
by other applications (like ``pytest``) for their plugins.
Entry points are a `standard packaging mechanism`__ for Python, used by
other applications (such as ``pytest``) for their plugins.
.. __: https://setuptools.readthedocs.io/en/stable/setuptools.html#dynamic-discovery-of-services-and-plugins
The ``importlib_metadata`` backport package is used on Python versions
older than 3.10, but its API is the same as :mod:`importlib.metadata`.
.. __: https://packaging.python.org/en/latest/specifications/entry-points/
"""

Expand All @@ -583,9 +586,8 @@ def get_meta_description(self):
:return: meta description information
:rtype: :class:`dict`
This returns the same keys as
:meth:`PyModulePlugin.get_meta_description`; the ``source`` key is
modified to contain the setuptools entry point::
This returns the output of :meth:`PyModulePlugin.get_meta_description`
but with the ``source`` key modified to reference the entry point::
{
'name': 'example',
Expand All @@ -598,6 +600,6 @@ def get_meta_description(self):
"""
data = super().get_meta_description()
data.update({
'source': str(self.entry_point),
'source': self.entry_point.name + ' = ' + self.entry_point.value,
})
return data
14 changes: 9 additions & 5 deletions test/plugins/test_plugins_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
import os
import sys

import pkg_resources
import pytest

try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove fallback when dropping py3.9
import importlib_metadata

from sopel.plugins import handlers


Expand Down Expand Up @@ -62,15 +67,14 @@ def test_get_label_pyfile_loaded(plugin_tmpfile):


def test_get_label_entrypoint(plugin_tmpfile):
# generate setuptools Distribution object
# set up for manual load/import
distrib_dir = os.path.dirname(plugin_tmpfile.strpath)
distrib = pkg_resources.Distribution(distrib_dir)
sys.path.append(distrib_dir)

# load the entry point
try:
entry_point = pkg_resources.EntryPoint(
'test_plugin', 'file_mod', dist=distrib)
entry_point = importlib_metadata.EntryPoint(
'test_plugin', 'file_mod', 'sopel.plugins')
plugin = handlers.EntryPointPlugin(entry_point)
plugin.load()
finally:
Expand Down
14 changes: 9 additions & 5 deletions test/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

import sys

import pkg_resources
import pytest

try:
import importlib.metadata as importlib_metadata
except ImportError:
# TODO: remove fallback when dropping py3.9
import importlib_metadata

from sopel import plugins


Expand Down Expand Up @@ -132,14 +137,13 @@ def test_plugin_load_entry_point(tmpdir):
mod_file = root.join('file_mod.py')
mod_file.write(MOCK_MODULE_CONTENT)

# generate setuptools Distribution object
distrib = pkg_resources.Distribution(root.strpath)
# set up for manual load/import
sys.path.append(root.strpath)

# load the entry point
try:
entry_point = pkg_resources.EntryPoint(
'test_plugin', 'file_mod', dist=distrib)
entry_point = importlib_metadata.EntryPoint(
'test_plugin', 'file_mod', 'sopel.plugins')
plugin = plugins.handlers.EntryPointPlugin(entry_point)
plugin.load()
finally:
Expand Down

0 comments on commit aca1055

Please sign in to comment.