We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Broken plugin symlink -> `PluginError` in CLI
### Description
The `sopel-plugins` command should gracefully handle things that look like plugins but can't be loaded. It does this in some cases (like Python file plugins with syntax errors), but not all. There's room for improvement.

For example, I recently symlinked a file in `~/.sopel/plugins` so it would point to my local repository for that plugin (not yet converted to an entry-point or package) for testing. I did the `ln -s` command wrong, so the link was broken: `pluginfile.py -> ./pluginfile.py`, which is a circular reference.

### Logs
```
Traceback (most recent call last):
  File "/home/dgw/.local/bin/sopel-plugins", line 33, in <module>
    sys.exit(load_entry_point('sopel', 'console_scripts', 'sopel-plugins')())
  File "/home/dgw/github/sopel/sopel/cli/plugins.py", line 556, in main
    return handle_list(options)
  File "/home/dgw/github/sopel/sopel/cli/plugins.py", line 174, in handle_list
    for name, info in plugins.get_usable_plugins(settings).items()
  File "/home/dgw/github/sopel/sopel/plugins/__init__.py", line 225, in get_usable_plugins
    plugins_info = collections.OrderedDict(
  File "/home/dgw/github/sopel/sopel/plugins/__init__.py", line 225, in <genexpr>
    plugins_info = collections.OrderedDict(
  File "/home/dgw/github/sopel/sopel/plugins/__init__.py", line 185, in enumerate_plugins
    for plugin in all_plugins:
  File "/home/dgw/github/sopel/sopel/plugins/__init__.py", line 124, in find_directory_plugins
    yield handlers.PyFilePlugin(abspath)
  File "/home/dgw/github/sopel/sopel/plugins/handlers.py", line 440, in __init__
    raise exceptions.PluginError('Invalid Sopel plugin: %s' % filename)
sopel.plugins.exceptions.PluginError: Invalid Sopel plugin: /home/dgw/.sopel/plugins/bombbot.py
```

### Environment
- Sopel `.version`: 8b463a0e49cac7a27dc528a180243a6a5183d1f9 
- Sopel installed via: `pip -e`
- Python version: 3.9.5
- Operating system: Ubuntu 20.04
- IRCd `/version`: n/a
- Relevant plugins: n/a

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sopel/plugins/__init__.py`
Content:
```
1 """Sopel's plugins interface.
2 
3 .. versionadded:: 7.0
4 
5 Sopel uses what are called Plugin Handlers as an interface between the bot and
6 its plugins (formerly called "modules"). This interface is defined by the
7 :class:`~.handlers.AbstractPluginHandler` abstract class.
8 
9 Plugins that can be used by Sopel are provided by :func:`~.get_usable_plugins`
10 in an :class:`ordered dict<collections.OrderedDict>`. This dict contains one
11 and only one plugin per unique name, using a specific order:
12 
13 * extra directories defined in the settings
14 * homedir's ``plugins`` directory
15 * ``sopel.plugins`` entry point group
16 * ``sopel_modules``'s subpackages
17 * ``sopel.builtins``'s core plugins
18 
19 (The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and
20 cannot be overridden.)
21 
22 To find all plugins (no matter their sources), the :func:`~.enumerate_plugins`
23 function can be used. For a more fine-grained search, ``find_*`` functions
24 exist for each type of plugin.
25 """
26 # Copyright 2019, Florian Strzelecki <florian.strzelecki@gmail.com>
27 #
28 # Licensed under the Eiffel Forum License 2.
29 from __future__ import annotations
30 
31 import collections
32 import importlib
33 import itertools
34 import os
35 
36 # TODO: use stdlib importlib.metadata when possible, after dropping py3.9.
37 # Stdlib does not support `entry_points(group='filter')` until py3.10, but
38 # fallback logic is more trouble than it's worth when e.g. clean Ubuntu
39 # py3.10 envs include old versions of this backport.
40 import importlib_metadata
41 
42 from . import exceptions, handlers, rules  # noqa
43 
44 
45 def _list_plugin_filenames(directory):
46     # list plugin filenames from a directory
47     # yield 2-value tuples: (name, absolute path)
48     base = os.path.abspath(directory)
49     for filename in os.listdir(base):
50         abspath = os.path.join(base, filename)
51 
52         if os.path.isdir(abspath):
53             if os.path.isfile(os.path.join(abspath, '__init__.py')):
54                 yield os.path.basename(filename), abspath
55         else:
56             name, ext = os.path.splitext(filename)
57             if ext == '.py' and name != '__init__':
58                 yield name, abspath
59 
60 
61 def find_internal_plugins():
62     """List internal plugins.
63 
64     :return: yield instances of :class:`~.handlers.PyModulePlugin`
65              configured for ``sopel.builtins.*``
66 
67     Internal plugins can be found under ``sopel.builtins``. This list does not
68     include the ``coretasks`` plugin.
69     """
70     builtins = importlib.util.find_spec('sopel.builtins')
71     if builtins is None or builtins.submodule_search_locations is None:
72         raise RuntimeError('Cannot resolve internal plugins')
73     plugin_list = itertools.chain.from_iterable(
74         _list_plugin_filenames(path)
75         for path in builtins.submodule_search_locations
76     )
77 
78     for name, _ in set(plugin_list):
79         yield handlers.PyModulePlugin(name, 'sopel.builtins')
80 
81 
82 def find_sopel_modules_plugins():
83     """List plugins from ``sopel_modules.*``.
84 
85     :return: yield instances of :class:`~.handlers.PyModulePlugin`
86              configured for ``sopel_modules.*``
87 
88     Before entry point plugins, the only way to package a plugin was to follow
89     :pep:`382` by using the ``sopel_modules`` namespace. This function is
90     responsible to load such plugins.
91     """
92     try:
93         import sopel_modules  # type: ignore[import]
94     except ImportError:
95         return
96 
97     for plugin_dir in set(sopel_modules.__path__):
98         for name, _ in _list_plugin_filenames(plugin_dir):
99             yield handlers.PyModulePlugin(name, 'sopel_modules')
100 
101 
102 def find_entry_point_plugins(group='sopel.plugins'):
103     """List plugins from an entry point group.
104 
105     :param str group: entry point group to search in (defaults to
106                       ``sopel.plugins``)
107     :return: yield instances of :class:`~.handlers.EntryPointPlugin`
108              created from each entry point in the ``group``
109 
110     This function finds plugins declared under an entry point group; by
111     default it looks in the ``sopel.plugins`` group.
112     """
113     for entry_point in importlib_metadata.entry_points(group=group):
114         yield handlers.EntryPointPlugin(entry_point)
115 
116 
117 def find_directory_plugins(directory):
118     """List plugins from a ``directory``.
119 
120     :param str directory: directory path to search
121     :return: yield instances of :class:`~.handlers.PyFilePlugin`
122              found in ``directory``
123 
124     This function looks for single file and folder plugins in a directory.
125     """
126     for _, abspath in _list_plugin_filenames(directory):
127         yield handlers.PyFilePlugin(abspath)
128 
129 
130 def enumerate_plugins(settings):
131     """Yield Sopel's plugins.
132 
133     :param settings: Sopel's configuration
134     :type settings: :class:`sopel.config.Config`
135     :return: yield 2-value tuple: an instance of
136              :class:`~.handlers.AbstractPluginHandler`, and if the plugin is
137              active or not
138 
139     This function uses the find functions to find all of Sopel's available
140     plugins. It uses the bot's ``settings`` to determine if the plugin is
141     enabled or disabled.
142 
143     .. seealso::
144 
145         The find functions used are:
146 
147         * :func:`find_internal_plugins` for internal plugins
148         * :func:`find_sopel_modules_plugins` for ``sopel_modules.*`` plugins
149         * :func:`find_entry_point_plugins` for plugins exposed via packages'
150           entry points
151         * :func:`find_directory_plugins` for plugins in ``$homedir/plugins``,
152           and in extra directories as defined by ``settings.core.extra``
153 
154     .. versionchanged:: 8.0
155 
156         Looks in ``$homedir/plugins`` instead of the ``$homedir/modules``
157         directory, reflecting Sopel's shift away from calling them "modules".
158 
159     """
160     from_internals = find_internal_plugins()
161     from_sopel_modules = find_sopel_modules_plugins()
162     from_entry_points = find_entry_point_plugins()
163     # load from directories
164     source_dirs = [
165         os.path.join(settings.homedir, 'plugins'),
166     ]
167     if settings.core.extra:
168         source_dirs = source_dirs + settings.core.extra
169 
170     from_directories = [
171         find_directory_plugins(source_dir)
172         for source_dir in source_dirs
173         if os.path.isdir(source_dir)
174     ]
175 
176     # Retrieve all plugins
177     all_plugins = itertools.chain(
178         from_internals,
179         from_sopel_modules,
180         from_entry_points,
181         *from_directories)
182 
183     # Get plugin settings
184     enabled = settings.core.enable
185     disabled = settings.core.exclude
186 
187     # Yield all found plugins with their enabled status (True/False)
188     for plugin in all_plugins:
189         name = plugin.name
190         is_enabled = name not in disabled and (not enabled or name in enabled)
191         yield plugin, is_enabled
192 
193     # And always yield coretasks
194     yield handlers.PyModulePlugin('coretasks', 'sopel'), True
195 
196 
197 def get_usable_plugins(settings):
198     """Get usable plugins, unique per name.
199 
200     :param settings: Sopel's configuration
201     :type settings: :class:`sopel.config.Config`
202     :return: an ordered dict of usable plugins
203     :rtype: collections.OrderedDict
204 
205     This function provides the plugins Sopel can use to load, enable,
206     or disable, as an :class:`ordered dict<collections.OrderedDict>`. This dict
207     contains one and only one plugin per unique name, using a specific order:
208 
209     * extra directories defined in the settings
210     * homedir's ``plugins`` directory
211     * ``sopel.plugins`` entry point group
212     * ``sopel_modules``'s subpackages
213     * ``sopel.builtins``'s core plugins
214 
215     (The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and
216     cannot be overridden.)
217 
218     .. seealso::
219 
220         The :func:`~.enumerate_plugins` function is used to generate a list
221         of all possible plugins, and its return value is used to populate
222         the :class:`ordered dict<collections.OrderedDict>`.
223 
224     """
225     # Use an OrderedDict to get one and only one plugin per name
226     # based on what plugins.enumerate_plugins does, external plugins are
227     # allowed to override internal plugins
228     plugins_info = collections.OrderedDict(
229         (plugin.name, (plugin, is_enabled))
230         for plugin, is_enabled in enumerate_plugins(settings))
231     # reset coretasks's position at the end of the loading queue
232     plugins_info.move_to_end('coretasks')
233 
234     return plugins_info
235
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/sopel/plugins/__init__.py b/sopel/plugins/__init__.py
--- a/sopel/plugins/__init__.py
+++ b/sopel/plugins/__init__.py
@@ -31,7 +31,9 @@
 import collections
 import importlib
 import itertools
+import logging
 import os
+from typing import TYPE_CHECKING, Union
 
 # TODO: use stdlib importlib.metadata when possible, after dropping py3.9.
 # Stdlib does not support `entry_points(group='filter')` until py3.10, but
@@ -41,13 +43,22 @@
 
 from . import exceptions, handlers, rules  # noqa
 
+if TYPE_CHECKING:
+    from collections.abc import Iterable
 
-def _list_plugin_filenames(directory):
+
+LOGGER = logging.getLogger(__name__)
+
+
+def _list_plugin_filenames(directory: Union[str, os.PathLike]) -> Iterable[tuple[str, str]]:
     # list plugin filenames from a directory
     # yield 2-value tuples: (name, absolute path)
     base = os.path.abspath(directory)
     for filename in os.listdir(base):
-        abspath = os.path.join(base, filename)
+        abspath = os.path.realpath(os.path.join(base, filename))
+        if not os.path.exists(abspath):
+            LOGGER.warning("Plugin path does not exist, skipping: %r", abspath)
+            continue
 
         if os.path.isdir(abspath):
             if os.path.isfile(os.path.join(abspath, '__init__.py')):

{"golden_diff": "diff --git a/sopel/plugins/__init__.py b/sopel/plugins/__init__.py\n--- a/sopel/plugins/__init__.py\n+++ b/sopel/plugins/__init__.py\n@@ -31,7 +31,9 @@\n import collections\n import importlib\n import itertools\n+import logging\n import os\n+from typing import TYPE_CHECKING, Union\n \n # TODO: use stdlib importlib.metadata when possible, after dropping py3.9.\n # Stdlib does not support `entry_points(group='filter')` until py3.10, but\n@@ -41,13 +43,22 @@\n \n from . import exceptions, handlers, rules  # noqa\n \n+if TYPE_CHECKING:\n+    from collections.abc import Iterable\n \n-def _list_plugin_filenames(directory):\n+\n+LOGGER = logging.getLogger(__name__)\n+\n+\n+def _list_plugin_filenames(directory: Union[str, os.PathLike]) -> Iterable[tuple[str, str]]:\n     # list plugin filenames from a directory\n     # yield 2-value tuples: (name, absolute path)\n     base = os.path.abspath(directory)\n     for filename in os.listdir(base):\n-        abspath = os.path.join(base, filename)\n+        abspath = os.path.realpath(os.path.join(base, filename))\n+        if not os.path.exists(abspath):\n+            LOGGER.warning(\"Plugin path does not exist, skipping: %r\", abspath)\n+            continue\n \n         if os.path.isdir(abspath):\n             if os.path.isfile(os.path.join(abspath, '__init__.py')):\n", "issue": "Broken plugin symlink -> `PluginError` in CLI\n### Description\r\nThe `sopel-plugins` command should gracefully handle things that look like plugins but can't be loaded. It does this in some cases (like Python file plugins with syntax errors), but not all. There's room for improvement.\r\n\r\nFor example, I recently symlinked a file in `~/.sopel/plugins` so it would point to my local repository for that plugin (not yet converted to an entry-point or package) for testing. I did the `ln -s` command wrong, so the link was broken: `pluginfile.py -> ./pluginfile.py`, which is a circular reference.\r\n\r\n### Logs\r\n```\r\nTraceback (most recent call last):\r\n  File \"/home/dgw/.local/bin/sopel-plugins\", line 33, in <module>\r\n    sys.exit(load_entry_point('sopel', 'console_scripts', 'sopel-plugins')())\r\n  File \"/home/dgw/github/sopel/sopel/cli/plugins.py\", line 556, in main\r\n    return handle_list(options)\r\n  File \"/home/dgw/github/sopel/sopel/cli/plugins.py\", line 174, in handle_list\r\n    for name, info in plugins.get_usable_plugins(settings).items()\r\n  File \"/home/dgw/github/sopel/sopel/plugins/__init__.py\", line 225, in get_usable_plugins\r\n    plugins_info = collections.OrderedDict(\r\n  File \"/home/dgw/github/sopel/sopel/plugins/__init__.py\", line 225, in <genexpr>\r\n    plugins_info = collections.OrderedDict(\r\n  File \"/home/dgw/github/sopel/sopel/plugins/__init__.py\", line 185, in enumerate_plugins\r\n    for plugin in all_plugins:\r\n  File \"/home/dgw/github/sopel/sopel/plugins/__init__.py\", line 124, in find_directory_plugins\r\n    yield handlers.PyFilePlugin(abspath)\r\n  File \"/home/dgw/github/sopel/sopel/plugins/handlers.py\", line 440, in __init__\r\n    raise exceptions.PluginError('Invalid Sopel plugin: %s' % filename)\r\nsopel.plugins.exceptions.PluginError: Invalid Sopel plugin: /home/dgw/.sopel/plugins/bombbot.py\r\n```\r\n\r\n### Environment\r\n- Sopel `.version`: 8b463a0e49cac7a27dc528a180243a6a5183d1f9 \r\n- Sopel installed via: `pip -e`\r\n- Python version: 3.9.5\r\n- Operating system: Ubuntu 20.04\r\n- IRCd `/version`: n/a\r\n- Relevant plugins: n/a\n", "before_files": [{"content": "\"\"\"Sopel's plugins interface.\n\n.. versionadded:: 7.0\n\nSopel uses what are called Plugin Handlers as an interface between the bot and\nits plugins (formerly called \"modules\"). This interface is defined by the\n:class:`~.handlers.AbstractPluginHandler` abstract class.\n\nPlugins that can be used by Sopel are provided by :func:`~.get_usable_plugins`\nin an :class:`ordered dict<collections.OrderedDict>`. This dict contains one\nand only one plugin per unique name, using a specific order:\n\n* extra directories defined in the settings\n* homedir's ``plugins`` directory\n* ``sopel.plugins`` entry point group\n* ``sopel_modules``'s subpackages\n* ``sopel.builtins``'s core plugins\n\n(The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and\ncannot be overridden.)\n\nTo find all plugins (no matter their sources), the :func:`~.enumerate_plugins`\nfunction can be used. For a more fine-grained search, ``find_*`` functions\nexist for each type of plugin.\n\"\"\"\n# Copyright 2019, Florian Strzelecki <florian.strzelecki@gmail.com>\n#\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import annotations\n\nimport collections\nimport importlib\nimport itertools\nimport os\n\n# TODO: use stdlib importlib.metadata when possible, after dropping py3.9.\n# Stdlib does not support `entry_points(group='filter')` until py3.10, but\n# fallback logic is more trouble than it's worth when e.g. clean Ubuntu\n# py3.10 envs include old versions of this backport.\nimport importlib_metadata\n\nfrom . import exceptions, handlers, rules  # noqa\n\n\ndef _list_plugin_filenames(directory):\n    # list plugin filenames from a directory\n    # yield 2-value tuples: (name, absolute path)\n    base = os.path.abspath(directory)\n    for filename in os.listdir(base):\n        abspath = os.path.join(base, filename)\n\n        if os.path.isdir(abspath):\n            if os.path.isfile(os.path.join(abspath, '__init__.py')):\n                yield os.path.basename(filename), abspath\n        else:\n            name, ext = os.path.splitext(filename)\n            if ext == '.py' and name != '__init__':\n                yield name, abspath\n\n\ndef find_internal_plugins():\n    \"\"\"List internal plugins.\n\n    :return: yield instances of :class:`~.handlers.PyModulePlugin`\n             configured for ``sopel.builtins.*``\n\n    Internal plugins can be found under ``sopel.builtins``. This list does not\n    include the ``coretasks`` plugin.\n    \"\"\"\n    builtins = importlib.util.find_spec('sopel.builtins')\n    if builtins is None or builtins.submodule_search_locations is None:\n        raise RuntimeError('Cannot resolve internal plugins')\n    plugin_list = itertools.chain.from_iterable(\n        _list_plugin_filenames(path)\n        for path in builtins.submodule_search_locations\n    )\n\n    for name, _ in set(plugin_list):\n        yield handlers.PyModulePlugin(name, 'sopel.builtins')\n\n\ndef find_sopel_modules_plugins():\n    \"\"\"List plugins from ``sopel_modules.*``.\n\n    :return: yield instances of :class:`~.handlers.PyModulePlugin`\n             configured for ``sopel_modules.*``\n\n    Before entry point plugins, the only way to package a plugin was to follow\n    :pep:`382` by using the ``sopel_modules`` namespace. This function is\n    responsible to load such plugins.\n    \"\"\"\n    try:\n        import sopel_modules  # type: ignore[import]\n    except ImportError:\n        return\n\n    for plugin_dir in set(sopel_modules.__path__):\n        for name, _ in _list_plugin_filenames(plugin_dir):\n            yield handlers.PyModulePlugin(name, 'sopel_modules')\n\n\ndef find_entry_point_plugins(group='sopel.plugins'):\n    \"\"\"List plugins from an entry point group.\n\n    :param str group: entry point group to search in (defaults to\n                      ``sopel.plugins``)\n    :return: yield instances of :class:`~.handlers.EntryPointPlugin`\n             created from each entry point in the ``group``\n\n    This function finds plugins declared under an entry point group; by\n    default it looks in the ``sopel.plugins`` group.\n    \"\"\"\n    for entry_point in importlib_metadata.entry_points(group=group):\n        yield handlers.EntryPointPlugin(entry_point)\n\n\ndef find_directory_plugins(directory):\n    \"\"\"List plugins from a ``directory``.\n\n    :param str directory: directory path to search\n    :return: yield instances of :class:`~.handlers.PyFilePlugin`\n             found in ``directory``\n\n    This function looks for single file and folder plugins in a directory.\n    \"\"\"\n    for _, abspath in _list_plugin_filenames(directory):\n        yield handlers.PyFilePlugin(abspath)\n\n\ndef enumerate_plugins(settings):\n    \"\"\"Yield Sopel's plugins.\n\n    :param settings: Sopel's configuration\n    :type settings: :class:`sopel.config.Config`\n    :return: yield 2-value tuple: an instance of\n             :class:`~.handlers.AbstractPluginHandler`, and if the plugin is\n             active or not\n\n    This function uses the find functions to find all of Sopel's available\n    plugins. It uses the bot's ``settings`` to determine if the plugin is\n    enabled or disabled.\n\n    .. seealso::\n\n        The find functions used are:\n\n        * :func:`find_internal_plugins` for internal plugins\n        * :func:`find_sopel_modules_plugins` for ``sopel_modules.*`` plugins\n        * :func:`find_entry_point_plugins` for plugins exposed via packages'\n          entry points\n        * :func:`find_directory_plugins` for plugins in ``$homedir/plugins``,\n          and in extra directories as defined by ``settings.core.extra``\n\n    .. versionchanged:: 8.0\n\n        Looks in ``$homedir/plugins`` instead of the ``$homedir/modules``\n        directory, reflecting Sopel's shift away from calling them \"modules\".\n\n    \"\"\"\n    from_internals = find_internal_plugins()\n    from_sopel_modules = find_sopel_modules_plugins()\n    from_entry_points = find_entry_point_plugins()\n    # load from directories\n    source_dirs = [\n        os.path.join(settings.homedir, 'plugins'),\n    ]\n    if settings.core.extra:\n        source_dirs = source_dirs + settings.core.extra\n\n    from_directories = [\n        find_directory_plugins(source_dir)\n        for source_dir in source_dirs\n        if os.path.isdir(source_dir)\n    ]\n\n    # Retrieve all plugins\n    all_plugins = itertools.chain(\n        from_internals,\n        from_sopel_modules,\n        from_entry_points,\n        *from_directories)\n\n    # Get plugin settings\n    enabled = settings.core.enable\n    disabled = settings.core.exclude\n\n    # Yield all found plugins with their enabled status (True/False)\n    for plugin in all_plugins:\n        name = plugin.name\n        is_enabled = name not in disabled and (not enabled or name in enabled)\n        yield plugin, is_enabled\n\n    # And always yield coretasks\n    yield handlers.PyModulePlugin('coretasks', 'sopel'), True\n\n\ndef get_usable_plugins(settings):\n    \"\"\"Get usable plugins, unique per name.\n\n    :param settings: Sopel's configuration\n    :type settings: :class:`sopel.config.Config`\n    :return: an ordered dict of usable plugins\n    :rtype: collections.OrderedDict\n\n    This function provides the plugins Sopel can use to load, enable,\n    or disable, as an :class:`ordered dict<collections.OrderedDict>`. This dict\n    contains one and only one plugin per unique name, using a specific order:\n\n    * extra directories defined in the settings\n    * homedir's ``plugins`` directory\n    * ``sopel.plugins`` entry point group\n    * ``sopel_modules``'s subpackages\n    * ``sopel.builtins``'s core plugins\n\n    (The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and\n    cannot be overridden.)\n\n    .. seealso::\n\n        The :func:`~.enumerate_plugins` function is used to generate a list\n        of all possible plugins, and its return value is used to populate\n        the :class:`ordered dict<collections.OrderedDict>`.\n\n    \"\"\"\n    # Use an OrderedDict to get one and only one plugin per name\n    # based on what plugins.enumerate_plugins does, external plugins are\n    # allowed to override internal plugins\n    plugins_info = collections.OrderedDict(\n        (plugin.name, (plugin, is_enabled))\n        for plugin, is_enabled in enumerate_plugins(settings))\n    # reset coretasks's position at the end of the loading queue\n    plugins_info.move_to_end('coretasks')\n\n    return plugins_info\n", "path": "sopel/plugins/__init__.py"}], "after_files": [{"content": "\"\"\"Sopel's plugins interface.\n\n.. versionadded:: 7.0\n\nSopel uses what are called Plugin Handlers as an interface between the bot and\nits plugins (formerly called \"modules\"). This interface is defined by the\n:class:`~.handlers.AbstractPluginHandler` abstract class.\n\nPlugins that can be used by Sopel are provided by :func:`~.get_usable_plugins`\nin an :class:`ordered dict<collections.OrderedDict>`. This dict contains one\nand only one plugin per unique name, using a specific order:\n\n* extra directories defined in the settings\n* homedir's ``plugins`` directory\n* ``sopel.plugins`` entry point group\n* ``sopel_modules``'s subpackages\n* ``sopel.builtins``'s core plugins\n\n(The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and\ncannot be overridden.)\n\nTo find all plugins (no matter their sources), the :func:`~.enumerate_plugins`\nfunction can be used. For a more fine-grained search, ``find_*`` functions\nexist for each type of plugin.\n\"\"\"\n# Copyright 2019, Florian Strzelecki <florian.strzelecki@gmail.com>\n#\n# Licensed under the Eiffel Forum License 2.\nfrom __future__ import annotations\n\nimport collections\nimport importlib\nimport itertools\nimport logging\nimport os\nfrom typing import TYPE_CHECKING, Union\n\n# TODO: use stdlib importlib.metadata when possible, after dropping py3.9.\n# Stdlib does not support `entry_points(group='filter')` until py3.10, but\n# fallback logic is more trouble than it's worth when e.g. clean Ubuntu\n# py3.10 envs include old versions of this backport.\nimport importlib_metadata\n\nfrom . import exceptions, handlers, rules  # noqa\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\n\nLOGGER = logging.getLogger(__name__)\n\n\ndef _list_plugin_filenames(directory: Union[str, os.PathLike]) -> Iterable[tuple[str, str]]:\n    # list plugin filenames from a directory\n    # yield 2-value tuples: (name, absolute path)\n    base = os.path.abspath(directory)\n    for filename in os.listdir(base):\n        abspath = os.path.realpath(os.path.join(base, filename))\n        if not os.path.exists(abspath):\n            LOGGER.warning(\"Plugin path does not exist, skipping: %r\", abspath)\n            continue\n\n        if os.path.isdir(abspath):\n            if os.path.isfile(os.path.join(abspath, '__init__.py')):\n                yield os.path.basename(filename), abspath\n        else:\n            name, ext = os.path.splitext(filename)\n            if ext == '.py' and name != '__init__':\n                yield name, abspath\n\n\ndef find_internal_plugins():\n    \"\"\"List internal plugins.\n\n    :return: yield instances of :class:`~.handlers.PyModulePlugin`\n             configured for ``sopel.builtins.*``\n\n    Internal plugins can be found under ``sopel.builtins``. This list does not\n    include the ``coretasks`` plugin.\n    \"\"\"\n    builtins = importlib.util.find_spec('sopel.builtins')\n    if builtins is None or builtins.submodule_search_locations is None:\n        raise RuntimeError('Cannot resolve internal plugins')\n    plugin_list = itertools.chain.from_iterable(\n        _list_plugin_filenames(path)\n        for path in builtins.submodule_search_locations\n    )\n\n    for name, _ in set(plugin_list):\n        yield handlers.PyModulePlugin(name, 'sopel.builtins')\n\n\ndef find_sopel_modules_plugins():\n    \"\"\"List plugins from ``sopel_modules.*``.\n\n    :return: yield instances of :class:`~.handlers.PyModulePlugin`\n             configured for ``sopel_modules.*``\n\n    Before entry point plugins, the only way to package a plugin was to follow\n    :pep:`382` by using the ``sopel_modules`` namespace. This function is\n    responsible to load such plugins.\n    \"\"\"\n    try:\n        import sopel_modules  # type: ignore[import]\n    except ImportError:\n        return\n\n    for plugin_dir in set(sopel_modules.__path__):\n        for name, _ in _list_plugin_filenames(plugin_dir):\n            yield handlers.PyModulePlugin(name, 'sopel_modules')\n\n\ndef find_entry_point_plugins(group='sopel.plugins'):\n    \"\"\"List plugins from an entry point group.\n\n    :param str group: entry point group to search in (defaults to\n                      ``sopel.plugins``)\n    :return: yield instances of :class:`~.handlers.EntryPointPlugin`\n             created from each entry point in the ``group``\n\n    This function finds plugins declared under an entry point group; by\n    default it looks in the ``sopel.plugins`` group.\n    \"\"\"\n    for entry_point in importlib_metadata.entry_points(group=group):\n        yield handlers.EntryPointPlugin(entry_point)\n\n\ndef find_directory_plugins(directory):\n    \"\"\"List plugins from a ``directory``.\n\n    :param str directory: directory path to search\n    :return: yield instances of :class:`~.handlers.PyFilePlugin`\n             found in ``directory``\n\n    This function looks for single file and folder plugins in a directory.\n    \"\"\"\n    for _, abspath in _list_plugin_filenames(directory):\n        yield handlers.PyFilePlugin(abspath)\n\n\ndef enumerate_plugins(settings):\n    \"\"\"Yield Sopel's plugins.\n\n    :param settings: Sopel's configuration\n    :type settings: :class:`sopel.config.Config`\n    :return: yield 2-value tuple: an instance of\n             :class:`~.handlers.AbstractPluginHandler`, and if the plugin is\n             active or not\n\n    This function uses the find functions to find all of Sopel's available\n    plugins. It uses the bot's ``settings`` to determine if the plugin is\n    enabled or disabled.\n\n    .. seealso::\n\n        The find functions used are:\n\n        * :func:`find_internal_plugins` for internal plugins\n        * :func:`find_sopel_modules_plugins` for ``sopel_modules.*`` plugins\n        * :func:`find_entry_point_plugins` for plugins exposed via packages'\n          entry points\n        * :func:`find_directory_plugins` for plugins in ``$homedir/plugins``,\n          and in extra directories as defined by ``settings.core.extra``\n\n    .. versionchanged:: 8.0\n\n        Looks in ``$homedir/plugins`` instead of the ``$homedir/modules``\n        directory, reflecting Sopel's shift away from calling them \"modules\".\n\n    \"\"\"\n    from_internals = find_internal_plugins()\n    from_sopel_modules = find_sopel_modules_plugins()\n    from_entry_points = find_entry_point_plugins()\n    # load from directories\n    source_dirs = [\n        os.path.join(settings.homedir, 'plugins'),\n    ]\n    if settings.core.extra:\n        source_dirs = source_dirs + settings.core.extra\n\n    from_directories = [\n        find_directory_plugins(source_dir)\n        for source_dir in source_dirs\n        if os.path.isdir(source_dir)\n    ]\n\n    # Retrieve all plugins\n    all_plugins = itertools.chain(\n        from_internals,\n        from_sopel_modules,\n        from_entry_points,\n        *from_directories)\n\n    # Get plugin settings\n    enabled = settings.core.enable\n    disabled = settings.core.exclude\n\n    # Yield all found plugins with their enabled status (True/False)\n    for plugin in all_plugins:\n        name = plugin.name\n        is_enabled = name not in disabled and (not enabled or name in enabled)\n        yield plugin, is_enabled\n\n    # And always yield coretasks\n    yield handlers.PyModulePlugin('coretasks', 'sopel'), True\n\n\ndef get_usable_plugins(settings):\n    \"\"\"Get usable plugins, unique per name.\n\n    :param settings: Sopel's configuration\n    :type settings: :class:`sopel.config.Config`\n    :return: an ordered dict of usable plugins\n    :rtype: collections.OrderedDict\n\n    This function provides the plugins Sopel can use to load, enable,\n    or disable, as an :class:`ordered dict<collections.OrderedDict>`. This dict\n    contains one and only one plugin per unique name, using a specific order:\n\n    * extra directories defined in the settings\n    * homedir's ``plugins`` directory\n    * ``sopel.plugins`` entry point group\n    * ``sopel_modules``'s subpackages\n    * ``sopel.builtins``'s core plugins\n\n    (The ``coretasks`` plugin is *always* the one from ``sopel.coretasks`` and\n    cannot be overridden.)\n\n    .. seealso::\n\n        The :func:`~.enumerate_plugins` function is used to generate a list\n        of all possible plugins, and its return value is used to populate\n        the :class:`ordered dict<collections.OrderedDict>`.\n\n    \"\"\"\n    # Use an OrderedDict to get one and only one plugin per name\n    # based on what plugins.enumerate_plugins does, external plugins are\n    # allowed to override internal plugins\n    plugins_info = collections.OrderedDict(\n        (plugin.name, (plugin, is_enabled))\n        for plugin, is_enabled in enumerate_plugins(settings))\n    # reset coretasks's position at the end of the loading queue\n    plugins_info.move_to_end('coretasks')\n\n    return plugins_info\n", "path": "sopel/plugins/__init__.py"}]}