prybar: Create temporary pkg_resources
entry points at runtime¶
Use prybar to temporarily define pkg_resources
entry points
at runtime. The primary use case is testing code which works with entry points.
Entry points?¶
Entry points are Python’s way of advertising and consuming plugins. Python packages can advertise an object (function, class, data, etc) which can be discovered and loaded by another package.
Entry points are normally statically defined in package metadata (e.g. via
setup.py
). The static nature of entry points makes thoroughly testing code
that loads entry points awkward.
prybar allows entry points to be created and removed on the fly, making it easy to test your plugin loading code.
Installing¶
prybar is available from PyPi as prybar
:
$ pip install prybar
prybar requires Python 3.6 or greater.
Using prybar¶
prybar provides dynamic_entrypoint()
— a context manager which
creates an entry point when entered and removes it when left. It can also be
used as a function decorator, or via explicit .start()
and .stop()
calls.
Brief Example¶
pkg_resources.iter_entry_points
is the normal way to discover entry points.
We’ll define a function to load the first named entry point in a group
(category).
>>> from pkg_resources import iter_entry_points
>>> def load_entrypoint(group, name):
... return next((ep.load() for ep in
... iter_entry_points(group, name=name)), None)
Initially no entry point will exist:
>>> load_entrypoint('example.hash_types', 'sha256') is None
True
We can create it at runtime with prybar:
>>> from prybar import dynamic_entrypoint
>>> with dynamic_entrypoint('example.hash_types',
... name='sha256', module='hashlib'):
... hash = load_entrypoint('example.hash_types', 'sha256')
... hash(b'foo').hexdigest()[:6]
'2c26b4'
It’s gone again after leaving the with
block:
>>> load_entrypoint('example.hash_types', 'sha256') is None
True
More examples¶
Multiple entry points can be registered by nesting with
blocks, or using
contextlib.ExitStack
:
>>> from contextlib import ExitStack
>>> epoints = [dynamic_entrypoint('example.types',
... name=name, module='builtins')
... for name in ['int', 'float', 'str', 'bool']]
>>> with ExitStack() as stack:
... for ep in epoints:
... stack.enter_context(ep)
...
... for ep in iter_entry_points('example.types'):
... t = ep.load()
... t('12')
12
12.0
'12'
True
dynamic_entrypoint
can also be used as a decorator:
>>> @dynamic_entrypoint('example.hash_types',
... name='sha256', module='hashlib')
... def example_function():
... hash = load_entrypoint('example.hash_types', 'sha256')
... return hash(b'foo').hexdigest()[:6]
>>> load_entrypoint('example.hash_types', 'sha256') is None
True
>>> example_function()
'2c26b4'
And via start()
and stop()
methods:
>>> sha256_dep = dynamic_entrypoint(
... 'example.hash_types', name='sha256', module='hashlib')
>>> load_entrypoint('example.hash_types', 'sha256') is None
True
>>> sha256_dep.start()
>>> hash = load_entrypoint('example.hash_types', 'sha256')
>>> hash(b'foo').hexdigest()[:6]
'2c26b4'
>>> sha256_dep.stop()
>>> load_entrypoint('example.hash_types', 'sha256') is None
True
The entry point can be specified in several ways in addition to the name
and
module
seen above.
attribute
can be used if the desired entry point name differs from the
target’s name in the module:
>>> from collections import Counter
>>> with dynamic_entrypoint('example', name='thing',
... module='collections',
... attribute='Counter'):
... thing = load_entrypoint('example', 'thing')
... thing is Counter
True
A function or class can be passed as entrypoint
, from which the name
,
module
and attribute
are inferred:
>>> with dynamic_entrypoint('example', entrypoint=Counter):
... thing = load_entrypoint('example', 'Counter')
... thing is Counter
True
The name
attribute can be used to override the entry point’s name:
>>> with dynamic_entrypoint('example', name='foo',
... entrypoint=Counter):
... thing = load_entrypoint('example', 'foo')
... thing == Counter
True
Nested functions can be specified:
>>> with dynamic_entrypoint('example', module='collections',
... name='fromkeys',
... attribute=('Counter', 'fromkeys')):
... thing = load_entrypoint('example', 'fromkeys')
... thing == Counter.fromkeys
True
>>> with dynamic_entrypoint('example', entrypoint=Counter.fromkeys):
... thing = load_entrypoint('example', 'fromkeys')
... thing == Counter.fromkeys
True
>>> with dynamic_entrypoint('example', name='foo',
... entrypoint=Counter.fromkeys):
... thing = load_entrypoint('example', 'foo')
... thing == Counter.fromkeys
True
A string using the entry point syntax from setup.py
can be used:
>>> with dynamic_entrypoint(
... 'example',
... entrypoint='thing = collections:Counter.fromkeys'):
... thing = load_entrypoint('example', 'thing')
... thing == Counter.fromkeys
True
Or a pkg_resources.EntryPoint
object can be passed:
>>> from pkg_resources import EntryPoint
>>> ep = EntryPoint('thing', 'collections', attrs=('Counter', 'fromkeys'))
>>> with dynamic_entrypoint('example', entrypoint=ep):
... thing = load_entrypoint('example', 'thing')
... thing == Counter.fromkeys
True
API Reference¶
prybar¶
-
prybar.
dynamic_entrypoint
(group: str, entrypoint: Union[Callable, Type[object], str, pkg_resources.EntryPoint, None] = None, *, name: Optional[str] = None, module: Optional[str] = None, attribute: Optional[str] = None, scope: Optional[str] = None, working_set: Optional[pkg_resources.WorkingSet] = None) → prybar.DynamicEntrypoint[source]¶ prybar.dynamic_entrypoint()
registers and de-registerspkg_resources
entry points at runtime.It acts as a context manager and function decorator. The entrypoint is registered within the
with
statement, or while the decorated function is running. It can also be registered and de-registered manually using itsstart()
andstop()
methods.The context manager/decorator is re-entrant, i.e. a single instance can be used in multiple with statements, or decorate multiple functions, and each usage site can be entered multiple times while the same or other usage sites are still in use. The entry point is registered once when the first with block or decorated function is entered, and remains registered until all with blocks/decorated functions have been left.
In contrast, the
start()
/stop()
API immediatley registers and deregisters the entry point. Callingstart
multiple times has no effect after the first, neither does callingstop
.Use of the
start()
/stop()
API is incompatible with the context manager/decorator API. An error will be raised if an attempt is made tostart()
/stop()
while a with block or decorated function is active, and vice versa.The
group
must always be provided as a string, but he entrypoint can be specified in several ways:- By providing a
name
andmodule
. The target in the module can be specified withattribute
if it differs fromname
. - By passing a function or class as
entrypoint
. Thename
,module
andattribute
are then inferred automatically. Thename
can be overriden. - By passing a string to be parsed as
entrypoint
. The format is the same as used in setup.py, e.g."my_name = my_module.submodule:my_func"
. - By passing a pre-created
pkg_resources.EntryPoint
object asentrypoint
.
Parameters: - group – The name of the entrypoint group to register the entrypoint
under. For example,
myproject.plugins
. - name – The name of the entrypoint.
- module – The dotted path of the module the entrypoint references.
- attribute – The name of the object within the module the entrypoint references (defaults to name).
- entrypoint – Either a function (or other object), or an entrypoint string to parse, or a pre-created pkg_resources.Entrypoint object
- scope –
A name to scope your entrypoints within.
group
,name
pairs must be unique within a scope, but multiple entrypoints with the samegroup
andname
can be created in different scopes. The scope defaults toprybar.scope.default
if not specified.Note: internally this defines the name of the
pkg_resources.Distribution
that the entrypoint is registered under. If you specify a scope you should use your package’s name as the prefix to avoid conflicts with entry points from other packages. - working_set – The pkg_resources.WorkingSet to register entrypoints in. Defaults to the default pkg_resources.working_set.
Returns: The context manager/decorator — a
prybar.DynamicEntrypoint
, which also supportsstart()
andstop()
methods.- By providing a