fix: add lists param for load_entry_points() function
wuttasync needs to let various projects define import/export handlers using the same key, so it needs a way to discover them all without this logic auto-discarding duplicate keys
This commit is contained in:
parent
17efdb9572
commit
35a0897b21
4 changed files with 74 additions and 14 deletions
|
|
@ -42,6 +42,7 @@ intersphinx_mapping = {
|
|||
"rattail": ("https://docs.wuttaproject.org/rattail/", None),
|
||||
"rattail-manual": ("https://docs.wuttaproject.org/rattail-manual/", None),
|
||||
"rich": ("https://rich.readthedocs.io/en/latest/", None),
|
||||
"setuptools": ("https://setuptools.pypa.io/en/latest/", None),
|
||||
"sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None),
|
||||
"wutta-continuum": ("https://docs.wuttaproject.org/wutta-continuum/", None),
|
||||
"wuttasync": ("https://docs.wuttaproject.org/wuttasync/", None),
|
||||
|
|
|
|||
|
|
@ -222,13 +222,9 @@ Glossary
|
|||
entry point
|
||||
This refers to a "setuptools-style" entry point specifically,
|
||||
which is a mechanism used to register "plugins" and the like.
|
||||
This lets the app / config discover features dynamically. Most
|
||||
notably used to register :term:`commands<command>` and
|
||||
:term:`subcommands<subcommand>`.
|
||||
This lets the app / config discover features dynamically.
|
||||
|
||||
For more info see the `Python Packaging User Guide`_.
|
||||
|
||||
.. _Python Packaging User Guide: https://packaging.python.org/en/latest/specifications/entry-points/
|
||||
For more info see :doc:`setuptools:userguide/entry_point` in the setuptools docs.
|
||||
|
||||
handler
|
||||
Similar to a "plugin" concept but only *one* handler may be used
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023-2025 Lance Edgar
|
||||
# Copyright © 2023-2026 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
|
|
@ -110,22 +110,36 @@ def get_value(obj, key):
|
|||
return getattr(obj, key)
|
||||
|
||||
|
||||
def load_entry_points(group, ignore_errors=False):
|
||||
def load_entry_points(group, lists=False, ignore_errors=False):
|
||||
"""
|
||||
Load a set of ``setuptools``-style entry points.
|
||||
Load a set of ``setuptools``-style :term:`entry points <entry
|
||||
point>`.
|
||||
|
||||
This is used to locate "plugins" and similar things, e.g. the set
|
||||
of subcommands which belong to a main command.
|
||||
This is used to locate "plugins" and similar things, e.g. discover
|
||||
which batch handlers are installed.
|
||||
|
||||
Logic will inspect the registered entry points and return a dict
|
||||
whose keys are the entry point names. By default the dict values
|
||||
will be the loaded objects as referenced by each entry point.
|
||||
|
||||
In some cases (notably, import handlers for wuttasync) the keys
|
||||
may not always be unique. This allows multiple projects to define
|
||||
entry points for the same key. If you specify ``lists=True`` then
|
||||
the dict values will each be *lists* of loaded objects instead.
|
||||
(Otherwise some entry points would be discarded when duplicate
|
||||
keys are found.)
|
||||
|
||||
:param group: The group (string name) of entry points to be
|
||||
loaded, e.g. ``'wutta.commands'``.
|
||||
|
||||
:param lists: Whether to return lists instead of single object
|
||||
values.
|
||||
|
||||
:param ignore_errors: If false (the default), any errors will be
|
||||
raised normally. If true, errors will be logged but not
|
||||
raised.
|
||||
|
||||
:returns: A dictionary whose keys are the entry point names, and
|
||||
values are the loaded entry points.
|
||||
:returns: A dict of entry points, as described above.
|
||||
"""
|
||||
entry_points = {}
|
||||
|
||||
|
|
@ -150,6 +164,15 @@ def load_entry_points(group, ignore_errors=False):
|
|||
raise
|
||||
log.warning("failed to load entry point: %s", entry_point, exc_info=True)
|
||||
else:
|
||||
if lists:
|
||||
entry_points.setdefault(entry_point.name, []).append(ep)
|
||||
else:
|
||||
if entry_point.name in entry_points:
|
||||
log.warning(
|
||||
"overwriting existing key '%s' with entry point: %s",
|
||||
entry_point.name,
|
||||
ep,
|
||||
)
|
||||
entry_points[entry_point.name] = ep
|
||||
|
||||
return entry_points
|
||||
|
|
|
|||
|
|
@ -184,6 +184,46 @@ class TestLoadEntryPoints(TestCase):
|
|||
entry_points.select.assert_called_once_with(group="wuttatest.thingers")
|
||||
entry_point.load.assert_called_once_with()
|
||||
|
||||
def test_duplicate_key(self):
|
||||
|
||||
# two entry points, with same name
|
||||
ep1 = MagicMock()
|
||||
ep2 = MagicMock()
|
||||
ep1.name = "not-quite-unique-key"
|
||||
ep2.name = "not-quite-unique-key"
|
||||
ep1.load.return_value = ep1
|
||||
ep2.load.return_value = ep2
|
||||
|
||||
# they both are included for "all" entry points
|
||||
entry_points = MagicMock()
|
||||
entry_points.select.return_value = [ep1, ep2]
|
||||
importlib = MagicMock()
|
||||
importlib.metadata.entry_points.return_value = entry_points
|
||||
with patch.dict("sys.modules", **{"importlib": importlib}):
|
||||
|
||||
# but only the 2nd entry point is returned
|
||||
result = mod.load_entry_points("console_scripts")
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertGreaterEqual(len(result), 1)
|
||||
self.assertIn("not-quite-unique-key", result)
|
||||
self.assertIs(result["not-quite-unique-key"], ep2)
|
||||
|
||||
def test_lists(self):
|
||||
|
||||
# classic behvaior
|
||||
result = mod.load_entry_points("console_scripts", lists=False)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("pip", result)
|
||||
self.assertTrue(callable(result["pip"]))
|
||||
|
||||
# lists behavior
|
||||
result = mod.load_entry_points("console_scripts", lists=True)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn("pip", result)
|
||||
self.assertIsInstance(result["pip"], list)
|
||||
self.assertEqual(len(result["pip"]), 1)
|
||||
self.assertTrue(callable(result["pip"][0]))
|
||||
|
||||
|
||||
class TestLoadObject(TestCase):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue