From ac9b4e1249e9032703a7fd6ea573e28fdc65a658 Mon Sep 17 00:00:00 2001 From: dgw Date: Sat, 26 Feb 2022 12:41:09 -0600 Subject: [PATCH 1/7] init: replace `pkg_resources` version fetch with importlib Needs a backport package until we no longer support Python 3.7; stdlib gained `importlib.metadata` in Python 3.8. I should note that installing with `-r dev-requirements.txt` will always (currently) install the `importlib_metadata` backport because of Sphinx. --- requirements.txt | 1 + sopel/__init__.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9937ec9c3b..73268949aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ geoip2>=4.0,<5.0 requests>=2.24.0,<3.0.0 dnspython<3.0 sqlalchemy>=1.4,<1.5 +importlib_metadata; python_version < '3.8' diff --git a/sopel/__init__.py b/sopel/__init__.py index b5ad956c3b..82b8f0a0c4 100644 --- a/sopel/__init__.py +++ b/sopel/__init__.py @@ -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', @@ -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__): From 37575e66803de2f2157ee3ee5b400b7b776fcf9d Mon Sep 17 00:00:00 2001 From: dgw Date: Sat, 26 Feb 2022 13:09:27 -0600 Subject: [PATCH 2/7] lifecycle: replace `pkg_resources` version parsing with `packaging`'s Unfortunately this one is *not* yet part of the stdlib in any Python release, but there's no point in removing only *most* uses of the older `pkg_resources` library. Gotta do them *all*. --- requirements.txt | 1 + sopel/lifecycle.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73268949aa..96650c6b5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ requests>=2.24.0,<3.0.0 dnspython<3.0 sqlalchemy>=1.4,<1.5 importlib_metadata; python_version < '3.8' +packaging diff --git a/sopel/lifecycle.py b/sopel/lifecycle.py index a0cdb8e0e4..cbc51ce094 100644 --- a/sopel/lifecycle.py +++ b/sopel/lifecycle.py @@ -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__ From 307ad5a410d20c267c3ffde3e3d9972e03a5efd0 Mon Sep 17 00:00:00 2001 From: dgw Date: Sat, 26 Feb 2022 15:06:16 -0600 Subject: [PATCH 3/7] plugins: use importlib to find entry point plugins --- requirements.txt | 2 +- sopel/plugins/__init__.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 96650c6b5d..f2492374ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ geoip2>=4.0,<5.0 requests>=2.24.0,<3.0.0 dnspython<3.0 sqlalchemy>=1.4,<1.5 -importlib_metadata; python_version < '3.8' +importlib_metadata>=3.6; python_version < '3.10' packaging diff --git a/sopel/plugins/__init__.py b/sopel/plugins/__init__.py index 86e1db00c1..42348a5fd7 100644 --- a/sopel/plugins/__init__.py +++ b/sopel/plugins/__init__.py @@ -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 @@ -103,7 +108,7 @@ def find_entry_point_plugins(group='sopel.plugins'): This function finds plugins declared under a setuptools entry point; by default it uses the ``sopel.plugins`` entry point. """ - 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) From 6b15c3d4f660d9e69e4c3b2cafde187c3b119aa0 Mon Sep 17 00:00:00 2001 From: dgw Date: Sat, 26 Feb 2022 17:26:22 -0600 Subject: [PATCH 4/7] plugins.handlers: use importlib EntryPoint model Adapted the `get_meta_description()` for `EntryPointPlugin` to return the expected format. Could alternatively change the expectation of `test_get_label_entrypoint()`. --- sopel/plugins/handlers.py | 9 ++++++--- test/plugins/test_plugins_handlers.py | 14 +++++++++----- test/test_plugins.py | 14 +++++++++----- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/sopel/plugins/handlers.py b/sopel/plugins/handlers.py index f2195d315d..e02d80b7db 100644 --- a/sopel/plugins/handlers.py +++ b/sopel/plugins/handlers.py @@ -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 @@ -559,6 +559,9 @@ class EntryPointPlugin(PyModulePlugin): Entry point is a `standard feature of setuptools`__ for Python, used by other applications (like ``pytest``) for their 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://setuptools.readthedocs.io/en/stable/setuptools.html#dynamic-discovery-of-services-and-plugins """ @@ -598,6 +601,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 diff --git a/test/plugins/test_plugins_handlers.py b/test/plugins/test_plugins_handlers.py index 188a96e0dd..1bc26fcd3c 100644 --- a/test/plugins/test_plugins_handlers.py +++ b/test/plugins/test_plugins_handlers.py @@ -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 @@ -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: diff --git a/test/test_plugins.py b/test/test_plugins.py index 7110c1dc07..ced8914988 100644 --- a/test/test_plugins.py +++ b/test/test_plugins.py @@ -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 @@ -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: From f7c6ea3db8e1eb4454eb34882da301012596cf28 Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 27 Feb 2022 18:51:12 -0600 Subject: [PATCH 5/7] plugins.handlers: decouple "entry point" concept from `setuptools` As the Python packaging docs say, entry points are now a PyPA-defined interoperability specification not confined to packages built using `setuptools`, and we should update our docs to reflect that. See https://packaging.python.org/en/latest/specifications/entry-points/ --- sopel/plugins/handlers.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/sopel/plugins/handlers.py b/sopel/plugins/handlers.py index e02d80b7db..c16d7852fb 100644 --- a/sopel/plugins/handlers.py +++ b/sopel/plugins/handlers.py @@ -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. @@ -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( @@ -556,13 +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. 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://setuptools.readthedocs.io/en/stable/setuptools.html#dynamic-discovery-of-services-and-plugins + .. __: https://packaging.python.org/en/latest/specifications/entry-points/ """ @@ -586,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', From 53d5c26554b2a3c169b8b2f99c68231cced73bcc Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 27 Feb 2022 18:58:30 -0600 Subject: [PATCH 6/7] plugins: rewrite entry point-related references to `setuptools` --- sopel/plugins/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sopel/plugins/__init__.py b/sopel/plugins/__init__.py index 42348a5fd7..6a59149a6a 100644 --- a/sopel/plugins/__init__.py +++ b/sopel/plugins/__init__.py @@ -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 @@ -98,15 +98,15 @@ 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 importlib_metadata.entry_points(group=group): yield handlers.EntryPointPlugin(entry_point) @@ -144,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`` @@ -206,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 From a7dc9ce86fc9264bf9507d4e5bc4668f384c4210 Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 27 Feb 2022 18:59:15 -0600 Subject: [PATCH 7/7] docs: update entry point definition in plugin terms It's not a "setuptools entry point"; it's an "entry point group". --- docs/source/plugin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/plugin.rst b/docs/source/plugin.rst index c8422fbb42..fbbf60486d 100644 --- a/docs/source/plugin.rst +++ b/docs/source/plugin.rst @@ -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`.