updates
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# flake8: noqa
|
||||
|
||||
__all__ = [
|
||||
'ExtensionManager',
|
||||
'EnabledExtensionManager',
|
||||
'NamedExtensionManager',
|
||||
'HookManager',
|
||||
'DriverManager',
|
||||
]
|
||||
|
||||
from .extension import ExtensionManager
|
||||
from .enabled import EnabledExtensionManager
|
||||
from .named import NamedExtensionManager
|
||||
from .hook import HookManager
|
||||
from .driver import DriverManager
|
||||
|
||||
import logging
|
||||
|
||||
# Configure a NullHandler for our log messages in case
|
||||
# the app we're used from does not set up logging.
|
||||
LOG = logging.getLogger('stevedore')
|
||||
|
||||
LOG.addHandler(logging.NullHandler())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
217
Backend/venv/lib/python3.12/site-packages/stevedore/_cache.py
Normal file
217
Backend/venv/lib/python3.12/site-packages/stevedore/_cache.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Use a cache layer in front of entry point scanning."""
|
||||
|
||||
import errno
|
||||
import glob
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import struct
|
||||
import sys
|
||||
from typing import TypeAlias
|
||||
from typing import TypedDict
|
||||
|
||||
log = logging.getLogger('stevedore._cache')
|
||||
|
||||
|
||||
def _get_cache_dir() -> str:
|
||||
"""Locate a platform-appropriate cache directory to use.
|
||||
|
||||
Does not ensure that the cache directory exists.
|
||||
"""
|
||||
# Linux, Unix, AIX, etc.
|
||||
if os.name == 'posix' and sys.platform != 'darwin':
|
||||
# use ~/.cache if empty OR not set
|
||||
base_path = os.environ.get('XDG_CACHE_HOME') or os.path.expanduser(
|
||||
'~/.cache'
|
||||
)
|
||||
return os.path.join(base_path, 'python-entrypoints')
|
||||
|
||||
# Mac OS
|
||||
elif sys.platform == 'darwin':
|
||||
return os.path.expanduser('~/Library/Caches/Python Entry Points')
|
||||
|
||||
# Windows (hopefully)
|
||||
else:
|
||||
base_path = os.environ.get('LOCALAPPDATA') or os.path.expanduser(
|
||||
'~\\AppData\\Local'
|
||||
)
|
||||
return os.path.join(base_path, 'Python Entry Points')
|
||||
|
||||
|
||||
def _get_mtime(name: str) -> float:
|
||||
try:
|
||||
s = os.stat(name)
|
||||
return s.st_mtime
|
||||
except OSError as err:
|
||||
if err.errno not in {errno.ENOENT, errno.ENOTDIR}:
|
||||
raise
|
||||
return -1.0
|
||||
|
||||
|
||||
def _ftobytes(f: float) -> bytes:
|
||||
return struct.Struct('f').pack(f)
|
||||
|
||||
|
||||
_PathHashEntryT: TypeAlias = list[tuple[str, float]]
|
||||
_PathHashSettingsT: TypeAlias = tuple[str, _PathHashEntryT]
|
||||
|
||||
|
||||
def _hash_settings_for_path(path: tuple[str, ...]) -> _PathHashSettingsT:
|
||||
"""Return a hash and the path settings that created it."""
|
||||
paths = []
|
||||
h = hashlib.sha256()
|
||||
|
||||
# Tie the cache to the python interpreter, in case it is part of a
|
||||
# virtualenv.
|
||||
h.update(sys.executable.encode('utf-8'))
|
||||
h.update(sys.prefix.encode('utf-8'))
|
||||
|
||||
for entry in path:
|
||||
mtime = _get_mtime(entry)
|
||||
h.update(entry.encode('utf-8'))
|
||||
h.update(_ftobytes(mtime))
|
||||
paths.append((entry, mtime))
|
||||
|
||||
for ep_file in itertools.chain(
|
||||
glob.iglob(os.path.join(entry, '*.dist-info', 'entry_points.txt')),
|
||||
glob.iglob(os.path.join(entry, '*.egg-info', 'entry_points.txt')),
|
||||
):
|
||||
mtime = _get_mtime(ep_file)
|
||||
h.update(ep_file.encode('utf-8'))
|
||||
h.update(_ftobytes(mtime))
|
||||
paths.append((ep_file, mtime))
|
||||
|
||||
return (h.hexdigest(), paths)
|
||||
|
||||
|
||||
_CacheEntry = TypedDict(
|
||||
'_CacheEntry',
|
||||
{
|
||||
'groups': dict[str, list[tuple[str, str, str]]],
|
||||
'sys.executable': str,
|
||||
'sys.prefix': str,
|
||||
'path_values': _PathHashEntryT,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _build_cacheable_data() -> _CacheEntry:
|
||||
entry_points = importlib.metadata.entry_points()
|
||||
groups: dict[str, list[tuple[str, str, str]]] = {}
|
||||
for group in entry_points.groups:
|
||||
existing = set()
|
||||
groups[group] = []
|
||||
for ep in entry_points.select(group=group):
|
||||
# Filter out duplicates that can occur when testing a
|
||||
# package that provides entry points using tox, where the
|
||||
# package is installed in the virtualenv that tox builds
|
||||
# and is present in the path as '.'.
|
||||
item = ep.name, ep.value, ep.group # convert to tuple
|
||||
if item in existing:
|
||||
continue
|
||||
existing.add(item)
|
||||
groups[group].append(item)
|
||||
|
||||
return {
|
||||
'groups': groups,
|
||||
'sys.executable': sys.executable,
|
||||
'sys.prefix': sys.prefix,
|
||||
'path_values': [],
|
||||
}
|
||||
|
||||
|
||||
class Cache:
|
||||
def __init__(self, cache_dir: str | None = None) -> None:
|
||||
if cache_dir is None:
|
||||
cache_dir = _get_cache_dir()
|
||||
self._dir = cache_dir
|
||||
self._internal: dict[tuple[str, ...], _CacheEntry] = {}
|
||||
self._disable_caching = False
|
||||
|
||||
# Caching can be disabled by either placing .disable file into the
|
||||
# target directory or when python executable is under /tmp (this is the
|
||||
# case when executed from ansible)
|
||||
if any(
|
||||
[
|
||||
os.path.isfile(os.path.join(self._dir, '.disable')),
|
||||
sys.executable[0:4] == '/tmp', # noqa: S108,
|
||||
]
|
||||
):
|
||||
self._disable_caching = True
|
||||
|
||||
def _get_data_for_path(self, path: tuple[str, ...] | None) -> _CacheEntry:
|
||||
internal_key = tuple(sys.path) if path is None else tuple(path)
|
||||
|
||||
if internal_key in self._internal:
|
||||
return self._internal[internal_key]
|
||||
|
||||
digest, path_values = _hash_settings_for_path(internal_key)
|
||||
filename = os.path.join(self._dir, digest)
|
||||
try:
|
||||
log.debug('reading %s', filename)
|
||||
with open(filename) as f:
|
||||
data: _CacheEntry = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
data = _build_cacheable_data()
|
||||
data['path_values'] = path_values
|
||||
if not self._disable_caching:
|
||||
try:
|
||||
log.debug('writing to %s', filename)
|
||||
os.makedirs(self._dir, exist_ok=True)
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(data, f)
|
||||
except OSError:
|
||||
# Could not create cache dir or write file.
|
||||
pass
|
||||
|
||||
self._internal[internal_key] = data
|
||||
return data
|
||||
|
||||
def get_group_all(
|
||||
self, group: str, path: tuple[str, ...] | None = None
|
||||
) -> list[importlib.metadata.EntryPoint]:
|
||||
result = []
|
||||
data = self._get_data_for_path(path)
|
||||
group_data = data.get('groups', {}).get(group, [])
|
||||
for vals in group_data:
|
||||
result.append(importlib.metadata.EntryPoint(*vals))
|
||||
return result
|
||||
|
||||
def get_group_named(
|
||||
self, group: str, path: tuple[str, ...] | None = None
|
||||
) -> dict[str, importlib.metadata.EntryPoint]:
|
||||
result = {}
|
||||
for ep in self.get_group_all(group, path=path):
|
||||
if ep.name not in result:
|
||||
result[ep.name] = ep
|
||||
return result
|
||||
|
||||
def get_single(
|
||||
self, group: str, name: str, path: tuple[str, ...] | None = None
|
||||
) -> importlib.metadata.EntryPoint:
|
||||
for name, ep in self.get_group_named(group, path=path).items():
|
||||
if name == name:
|
||||
return ep
|
||||
raise ValueError(f'No entrypoint {group!r} in group {name!r}')
|
||||
|
||||
|
||||
_c = Cache()
|
||||
get_group_all = _c.get_group_all
|
||||
get_group_named = _c.get_group_named
|
||||
get_single = _c.get_single
|
||||
265
Backend/venv/lib/python3.12/site-packages/stevedore/dispatch.py
Normal file
265
Backend/venv/lib/python3.12/site-packages/stevedore/dispatch.py
Normal file
@@ -0,0 +1,265 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Concatenate
|
||||
from typing import ParamSpec
|
||||
from typing import TypeVar
|
||||
|
||||
from .enabled import EnabledExtensionManager
|
||||
from .exception import NoMatches
|
||||
from .extension import Extension
|
||||
from .extension import OnLoadFailureCallbackT
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
P = ParamSpec('P')
|
||||
Q = ParamSpec('Q')
|
||||
|
||||
|
||||
class DispatchExtensionManager(EnabledExtensionManager[T]):
|
||||
"""Loads all plugins and filters on execution.
|
||||
|
||||
This is useful for long-running processes that need to pass
|
||||
different inputs to different extensions.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
"""
|
||||
|
||||
def map( # type: ignore[override]
|
||||
self,
|
||||
filter_func: Callable[Concatenate[Extension[T], P], bool],
|
||||
func: Callable[Concatenate[Extension[T], Q], U],
|
||||
*args: Any,
|
||||
**kwds: Any,
|
||||
) -> list[U]:
|
||||
"""Iterate over the extensions invoking func() for any where
|
||||
filter_func() returns True.
|
||||
|
||||
The signature of filter_func() should be::
|
||||
|
||||
def filter_func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to filter_func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension`
|
||||
instance. filter_func() should return True if the extension
|
||||
should be invoked for the input arguments.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param filter_func: Callable to test each extension.
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
if not self.extensions:
|
||||
# FIXME: Use a more specific exception class here.
|
||||
raise NoMatches(f'No {self.namespace} extensions found')
|
||||
response: list[U] = []
|
||||
for e in self.extensions:
|
||||
if filter_func(e, *args, **kwds):
|
||||
self._invoke_one_plugin(
|
||||
response.append, func, e, *args, **kwds
|
||||
)
|
||||
return response
|
||||
|
||||
def map_method( # type: ignore[override]
|
||||
self,
|
||||
filter_func: Callable[Concatenate[Extension[T], P], bool],
|
||||
method_name: str,
|
||||
*args: Any,
|
||||
**kwds: Any,
|
||||
) -> Any:
|
||||
"""Iterate over the extensions invoking each one's object method called
|
||||
`method_name` for any where filter_func() returns True.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param filter_func: Callable to test each extension.
|
||||
:param method_name: The extension method name to call
|
||||
for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(
|
||||
filter_func,
|
||||
self._call_extension_method,
|
||||
method_name,
|
||||
*args,
|
||||
**kwds,
|
||||
)
|
||||
|
||||
|
||||
class NameDispatchExtensionManager(DispatchExtensionManager[T]):
|
||||
"""Loads all plugins and filters on execution.
|
||||
|
||||
This is useful for long-running processes that need to pass
|
||||
different inputs to different extensions and can predict the name
|
||||
of the extensions before calling them.
|
||||
|
||||
The check_func argument should return a boolean, with ``True``
|
||||
indicating that the extension should be loaded and made available
|
||||
and ``False`` indicating that the extension should be ignored.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
check_func: Callable[[Extension[T]], bool],
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
):
|
||||
invoke_args = () if invoke_args is None else invoke_args
|
||||
invoke_kwds = {} if invoke_kwds is None else invoke_kwds
|
||||
super().__init__(
|
||||
namespace=namespace,
|
||||
check_func=check_func,
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
)
|
||||
|
||||
def _init_plugins(self, extensions: list[Extension[T]]) -> None:
|
||||
super()._init_plugins(extensions)
|
||||
self.by_name = {e.name: e for e in self.extensions}
|
||||
|
||||
def map( # type: ignore[override]
|
||||
self,
|
||||
names: Sequence[str],
|
||||
func: Callable[Concatenate[Extension[T], P], U],
|
||||
*args: P.args,
|
||||
**kwds: P.kwargs,
|
||||
) -> list[U]:
|
||||
"""Iterate over the extensions invoking func() for any where
|
||||
the name is in the given list of names.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param names: List or set of name(s) of extension(s) to invoke.
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
response: list[U] = []
|
||||
for name in names:
|
||||
try:
|
||||
e = self.by_name[name]
|
||||
except KeyError:
|
||||
LOG.debug('Missing extension %r being ignored', name)
|
||||
else:
|
||||
self._invoke_one_plugin(
|
||||
response.append, func, e, *args, **kwds
|
||||
)
|
||||
return response
|
||||
|
||||
def map_method( # type: ignore[override]
|
||||
self, names: Sequence[str], method_name: str, *args: Any, **kwds: Any
|
||||
) -> Any:
|
||||
"""Iterate over the extensions invoking each one's object method called
|
||||
`method_name` for any where the name is in the given list of names.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param names: List or set of name(s) of extension(s) to invoke.
|
||||
:param method_name: The extension method name
|
||||
to call for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(
|
||||
names, self._call_extension_method, method_name, *args, **kwds
|
||||
)
|
||||
208
Backend/venv/lib/python3.12/site-packages/stevedore/driver.py
Normal file
208
Backend/venv/lib/python3.12/site-packages/stevedore/driver.py
Normal file
@@ -0,0 +1,208 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Callable
|
||||
import importlib.metadata
|
||||
from typing import Any
|
||||
from typing import Concatenate
|
||||
from typing import ParamSpec
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
from .exception import MultipleMatches
|
||||
from .exception import NoMatches
|
||||
from .extension import ConflictResolverT
|
||||
from .extension import Extension
|
||||
from .extension import ExtensionManager
|
||||
from .extension import ignore_conflicts
|
||||
from .extension import OnLoadFailureCallbackT
|
||||
from .named import NamedExtensionManager
|
||||
from .named import OnMissingEntrypointsCallbackT
|
||||
from .named import warning_on_missing_entrypoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
P = ParamSpec('P')
|
||||
|
||||
|
||||
class DriverManager(NamedExtensionManager[T]):
|
||||
"""Load a single plugin with a given name from the namespace.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param name: The name of the driver to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param on_missing_entrypoints_callback: Callback function that will be
|
||||
called when one or more names cannot be found. The provided argument
|
||||
will be a subset of the 'names' parameter.
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param warn_on_missing_entrypoint: **DEPRECATED** Flag to control whether
|
||||
failing to load a plugin is reported via a log mess. Only applies if
|
||||
on_missing_entrypoints_callback is None. Users should instead set
|
||||
``on_missing_entrypoints_callback`` to ``None`` if they wish to disable
|
||||
logging.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with the
|
||||
same name. This is only used if retrieving entrypoint by name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
name: str,
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
on_missing_entrypoints_callback: (
|
||||
OnMissingEntrypointsCallbackT | None
|
||||
) = warning_on_missing_entrypoint,
|
||||
verify_requirements: bool | None = None,
|
||||
warn_on_missing_entrypoint: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> None:
|
||||
invoke_args = () if invoke_args is None else invoke_args
|
||||
invoke_kwds = {} if invoke_kwds is None else invoke_kwds
|
||||
on_load_failure_callback = (
|
||||
on_load_failure_callback or self._default_on_load_failure
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
namespace=namespace,
|
||||
names=[name],
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
warn_on_missing_entrypoint=warn_on_missing_entrypoint,
|
||||
conflict_resolver=conflict_resolver,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _default_on_load_failure(
|
||||
manager: 'ExtensionManager[T]',
|
||||
ep: importlib.metadata.EntryPoint,
|
||||
err: BaseException,
|
||||
) -> None:
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def make_test_instance( # type: ignore[override]
|
||||
cls,
|
||||
extension: Extension[T],
|
||||
namespace: str = 'TESTING',
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> 'Self':
|
||||
"""Construct a test DriverManager
|
||||
|
||||
Test instances are passed a list of extensions to work from rather
|
||||
than loading them from entry points.
|
||||
|
||||
:param extension: Pre-configured Extension instance
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged
|
||||
and then ignored
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when an entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with
|
||||
the same name. This is only used if retrieving entrypoint by name.
|
||||
:return: The manager instance, initialized for testing
|
||||
"""
|
||||
o = super().make_test_instance(
|
||||
[extension],
|
||||
namespace=namespace,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
conflict_resolver=conflict_resolver,
|
||||
)
|
||||
return o
|
||||
|
||||
def _init_plugins(self, extensions: list[Extension[T]]) -> None:
|
||||
super()._init_plugins(extensions)
|
||||
|
||||
if not self.extensions:
|
||||
name = self._names[0]
|
||||
raise NoMatches(
|
||||
f'No {self.namespace!r} driver found, looking for {name!r}'
|
||||
)
|
||||
if len(self.extensions) > 1:
|
||||
discovered_drivers = ','.join(
|
||||
e.entry_point_target for e in self.extensions
|
||||
)
|
||||
|
||||
raise MultipleMatches(
|
||||
f'Multiple {self.namespace!r} drivers found: '
|
||||
f'{discovered_drivers}'
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
func: Callable[Concatenate[Extension[T], P], U],
|
||||
*args: Any,
|
||||
**kwds: Any,
|
||||
) -> U | None:
|
||||
"""Invokes func() for the single loaded extension.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are logged and ignored.
|
||||
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
results = self.map(func, *args, **kwds)
|
||||
if results:
|
||||
return results[0]
|
||||
return None
|
||||
|
||||
@property
|
||||
def driver(self) -> T | Callable[..., T]:
|
||||
"""Returns the driver being used by this manager."""
|
||||
ext = self.extensions[0]
|
||||
return ext.obj if ext.obj else ext.plugin
|
||||
103
Backend/venv/lib/python3.12/site-packages/stevedore/enabled.py
Normal file
103
Backend/venv/lib/python3.12/site-packages/stevedore/enabled.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Callable
|
||||
import importlib.metadata
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TypeVar
|
||||
|
||||
from .extension import ConflictResolverT
|
||||
from .extension import Extension
|
||||
from .extension import ExtensionManager
|
||||
from .extension import ignore_conflicts
|
||||
from .extension import OnLoadFailureCallbackT
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class EnabledExtensionManager(ExtensionManager[T]):
|
||||
"""Loads only plugins that pass a check function.
|
||||
|
||||
The check_func argument should return a boolean, with ``True``
|
||||
indicating that the extension should be loaded and made available
|
||||
and ``False`` indicating that the extension should be ignored.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with the
|
||||
same name. This is only used if retrieving entrypoint by name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
check_func: Callable[[Extension[T]], bool],
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
):
|
||||
invoke_args = () if invoke_args is None else invoke_args
|
||||
invoke_kwds = {} if invoke_kwds is None else invoke_kwds
|
||||
|
||||
self.check_func = check_func
|
||||
|
||||
super().__init__(
|
||||
namespace,
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
conflict_resolver=conflict_resolver,
|
||||
)
|
||||
|
||||
def _load_one_plugin(
|
||||
self,
|
||||
ep: importlib.metadata.EntryPoint,
|
||||
invoke_on_load: bool,
|
||||
invoke_args: tuple[Any, ...],
|
||||
invoke_kwds: dict[str, Any],
|
||||
) -> Extension[T] | None:
|
||||
ext = super()._load_one_plugin(
|
||||
ep, invoke_on_load, invoke_args, invoke_kwds
|
||||
)
|
||||
if ext and not self.check_func(ext):
|
||||
LOG.debug('ignoring extension %r', ep.name)
|
||||
return None
|
||||
return ext
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import abc
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
|
||||
class FormatterBase(metaclass=abc.ABCMeta):
|
||||
"""Base class for example plugin used in the tutorial."""
|
||||
|
||||
def __init__(self, max_width: int = 60) -> None:
|
||||
self.max_width = max_width
|
||||
|
||||
@abc.abstractmethod
|
||||
def format(self, data: dict[str, Any]) -> Iterable[str]:
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as values.
|
||||
:returns: Iterable producing the formatted text.
|
||||
"""
|
||||
@@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
|
||||
from stevedore import driver
|
||||
|
||||
from .base import FormatterBase
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'format', nargs='?', default='simple', help='the output format'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width', default=60, type=int, help='maximum output width for text'
|
||||
)
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}
|
||||
|
||||
mgr: driver.DriverManager[FormatterBase] = driver.DriverManager(
|
||||
namespace='stevedore.example.formatter',
|
||||
name=parsed_args.format,
|
||||
invoke_on_load=True,
|
||||
invoke_args=(parsed_args.width,),
|
||||
)
|
||||
# okay because invoke_on_load is true
|
||||
assert isinstance(mgr.driver, FormatterBase)
|
||||
for chunk in mgr.driver.format(data):
|
||||
print(chunk, end='')
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
from .base import FormatterBase
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'--width', default=60, type=int, help='maximum output width for text'
|
||||
)
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}
|
||||
|
||||
mgr: extension.ExtensionManager[FormatterBase]
|
||||
mgr = extension.ExtensionManager(
|
||||
namespace='stevedore.example.formatter',
|
||||
invoke_on_load=True,
|
||||
invoke_args=(parsed_args.width,),
|
||||
)
|
||||
|
||||
def format_data(
|
||||
ext: extension.Extension[FormatterBase], data: dict[str, Any], /
|
||||
) -> tuple[str, Iterable[str]]:
|
||||
assert ext.obj is not None
|
||||
return (ext.name, ext.obj.format(data))
|
||||
|
||||
results = mgr.map(format_data, data)
|
||||
|
||||
for name, result in results:
|
||||
print(f'Formatter: {name}')
|
||||
for chunk in result:
|
||||
print(chunk, end='')
|
||||
print('')
|
||||
@@ -0,0 +1,40 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "stevedore-examples"
|
||||
description = "Demonstration package for stevedore"
|
||||
authors = [
|
||||
{name = "Doug Hellmann", email = "doug@doughellmann.com"},
|
||||
]
|
||||
readme = {file = "README.rst", content-type = "text/x-rst"}
|
||||
license = {text = "Apache-2.0"}
|
||||
version = "0.1"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.openstack.org/stevedore"
|
||||
Repository = "https://opendev.org/openstack/stevedore"
|
||||
|
||||
[project.entry-points."stevedore.example.formatter"]
|
||||
simple = "stevedore.example.simple:Simple"
|
||||
plain = "stevedore.example.simple:Simple"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"stevedore.examples"
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from stevedore.example import base
|
||||
|
||||
|
||||
class Simple(base.FormatterBase):
|
||||
"""A very basic formatter."""
|
||||
|
||||
def format(self, data: dict[str, Any]) -> Iterable[str]:
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as values.
|
||||
:returns: Iterable producing the formatted text.
|
||||
"""
|
||||
for name, value in sorted(data.items()):
|
||||
line = f'{name} = {value}\n'
|
||||
yield line
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from collections.abc import Iterable
|
||||
import textwrap
|
||||
from typing import Any
|
||||
|
||||
from stevedore.example import base
|
||||
|
||||
|
||||
class FieldList(base.FormatterBase):
|
||||
"""Format values as a reStructuredText field list.
|
||||
|
||||
For example::
|
||||
|
||||
: name1 : value
|
||||
: name2 : value
|
||||
: name3 : a long value
|
||||
will be wrapped with
|
||||
a hanging indent
|
||||
"""
|
||||
|
||||
def format(self, data: dict[str, Any]) -> Iterable[str]:
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as values.
|
||||
:returns: Iterable producing the formatted text.
|
||||
"""
|
||||
for name, value in sorted(data.items()):
|
||||
full_text = f': {name} : {value}'
|
||||
wrapped_text = textwrap.fill(
|
||||
full_text,
|
||||
initial_indent='',
|
||||
subsequent_indent=' ',
|
||||
width=self.max_width,
|
||||
)
|
||||
yield wrapped_text + '\n'
|
||||
@@ -0,0 +1,39 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "stevedore-examples2"
|
||||
description = "Demonstration package for stevedore"
|
||||
authors = [
|
||||
{name = "Doug Hellmann", email = "doug@doughellmann.com"},
|
||||
]
|
||||
readme = {file = "README.rst", content-type = "text/x-rst"}
|
||||
license = {text = "Apache-2.0"}
|
||||
version = "0.1"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.openstack.org/stevedore"
|
||||
Repository = "https://opendev.org/openstack/stevedore"
|
||||
|
||||
[project.entry-points."stevedore.example.formatter"]
|
||||
field = "stevedore.example2.fields:FieldList"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = [
|
||||
"stevedore.examples2"
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -0,0 +1,23 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
class NoUniqueMatch(RuntimeError):
|
||||
"""There was more than one extension, or none, that matched the query."""
|
||||
|
||||
|
||||
class NoMatches(NoUniqueMatch):
|
||||
"""There were no extensions with the driver name found."""
|
||||
|
||||
|
||||
class MultipleMatches(NoUniqueMatch):
|
||||
"""There were multiple matches for the given name."""
|
||||
456
Backend/venv/lib/python3.12/site-packages/stevedore/extension.py
Normal file
456
Backend/venv/lib/python3.12/site-packages/stevedore/extension.py
Normal file
@@ -0,0 +1,456 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""ExtensionManager"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import ItemsView
|
||||
from collections.abc import Iterator
|
||||
import importlib.metadata
|
||||
import itertools
|
||||
import logging
|
||||
import operator
|
||||
from typing import Any
|
||||
from typing import Concatenate
|
||||
from typing import Generic
|
||||
from typing import ParamSpec
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeAlias
|
||||
from typing import TypeVar
|
||||
import warnings
|
||||
|
||||
from . import _cache
|
||||
from .exception import MultipleMatches
|
||||
from .exception import NoMatches
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
P = ParamSpec('P')
|
||||
|
||||
|
||||
class Extension(Generic[T]):
|
||||
"""Book-keeping object for tracking extensions.
|
||||
|
||||
The arguments passed to the constructor are saved as attributes of
|
||||
the instance using the same names, and can be accessed by the
|
||||
callables passed to :meth:`map` or when iterating over an
|
||||
:class:`ExtensionManager` directly.
|
||||
|
||||
:param name: The entry point name.
|
||||
:param entry_point: The EntryPoint instance returned by :mod:`entrypoints`.
|
||||
:param plugin: The value returned by entry_point.load()
|
||||
:param obj: The object returned by ``plugin(*args, **kwds)`` if the
|
||||
manager invoked the extension on load.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
entry_point: importlib.metadata.EntryPoint,
|
||||
plugin: Callable[..., T],
|
||||
obj: T | None,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.entry_point = entry_point
|
||||
self.plugin = plugin
|
||||
self.obj = obj
|
||||
|
||||
@property
|
||||
def module_name(self) -> str:
|
||||
"""The name of the module from which the entry point is loaded.
|
||||
|
||||
:return: A string in 'dotted.module' format.
|
||||
"""
|
||||
return self.entry_point.module
|
||||
|
||||
@property
|
||||
def attr(self) -> str:
|
||||
"""The attribute of the module to be loaded."""
|
||||
return self.entry_point.attr
|
||||
|
||||
@property
|
||||
def entry_point_target(self) -> str:
|
||||
"""The module and attribute referenced by this extension's entry_point.
|
||||
|
||||
:return: A string representation of the target of the entry point in
|
||||
'dotted.module:object' format.
|
||||
"""
|
||||
return self.entry_point.value
|
||||
|
||||
|
||||
#: OnLoadFailureCallbackT defines the type for callbacks when a plugin fails
|
||||
#: to load. The callback callable should expect the extension manager instance,
|
||||
#: the underlying entrypoint instance, and the exception raised during
|
||||
#: attempted loading.
|
||||
OnLoadFailureCallbackT: TypeAlias = Callable[
|
||||
['ExtensionManager[T]', importlib.metadata.EntryPoint, BaseException], None
|
||||
]
|
||||
|
||||
#: ConflictResolver defines the type for conflict resolution callables. The
|
||||
#: callable should expect the extension namespace, extension name, and a list
|
||||
#: of the entrypoints themselves.
|
||||
ConflictResolverT: TypeAlias = Callable[
|
||||
[str, str, list[Extension[T]]], Extension[T]
|
||||
]
|
||||
|
||||
|
||||
def ignore_conflicts(
|
||||
namespace: str, name: str, entrypoints: list[Extension[T]]
|
||||
) -> Extension[T]:
|
||||
LOG.warning(
|
||||
"multiple implementations found for the '%(name)s' extension in "
|
||||
"%(namespace)s namespace: %(conflicts)s",
|
||||
{
|
||||
'name': name,
|
||||
'namespace': namespace,
|
||||
'conflicts': ', '.join(
|
||||
ep.plugin.__qualname__ for ep in entrypoints
|
||||
),
|
||||
},
|
||||
)
|
||||
# use the most last found entrypoint
|
||||
return entrypoints[-1]
|
||||
|
||||
|
||||
def error_on_conflict(
|
||||
namespace: str, name: str, entrypoints: list[Extension[T]]
|
||||
) -> Extension[T]:
|
||||
raise MultipleMatches(
|
||||
"multiple implementations found for the '{name}' command in "
|
||||
"{namespace} namespace: {conflicts}".format(
|
||||
name=name,
|
||||
namespace=namespace,
|
||||
conflicts=', '.join(ep.plugin.__qualname__ for ep in entrypoints),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ExtensionManager(Generic[T]):
|
||||
"""Base class for all of the other managers.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with the
|
||||
same name. This is only used if retrieving entrypoint by name.
|
||||
"""
|
||||
|
||||
ENTRY_POINT_CACHE: dict[str, list[importlib.metadata.EntryPoint]] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> None:
|
||||
invoke_args = () if invoke_args is None else invoke_args
|
||||
invoke_kwds = {} if invoke_kwds is None else invoke_kwds
|
||||
|
||||
if verify_requirements is not None:
|
||||
warnings.warn(
|
||||
'The verify_requirements argument is now a no-op and is '
|
||||
'deprecated for removal. Remove the argument from calls.',
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
self.namespace = namespace
|
||||
self.propagate_map_exceptions = propagate_map_exceptions
|
||||
self._on_load_failure_callback = on_load_failure_callback
|
||||
self._conflict_resolver = conflict_resolver
|
||||
|
||||
extensions = self._load_plugins(
|
||||
invoke_on_load, invoke_args, invoke_kwds
|
||||
)
|
||||
|
||||
self._init_plugins(extensions)
|
||||
|
||||
@classmethod
|
||||
def make_test_instance(
|
||||
cls,
|
||||
extensions: list[Extension[T]],
|
||||
namespace: str = 'TESTING',
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> 'Self':
|
||||
"""Construct a test ExtensionManager
|
||||
|
||||
Test instances are passed a list of extensions to work from rather
|
||||
than loading them from entry points.
|
||||
|
||||
:param extensions: Pre-configured Extension instances to use
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:param propagate_map_exceptions: When calling map, controls whether
|
||||
exceptions are propagated up through the map call or whether they
|
||||
are logged and then ignored
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when an entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with
|
||||
the same name. This is only used if retrieving entrypoint by name.
|
||||
:return: The manager instance, initialized for testing
|
||||
"""
|
||||
if verify_requirements is not None:
|
||||
warnings.warn(
|
||||
'The verify_requirements argument is now a no-op and is '
|
||||
'deprecated for removal. Remove the argument from calls.',
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
o = cls.__new__(cls)
|
||||
o.namespace = namespace
|
||||
o.propagate_map_exceptions = propagate_map_exceptions
|
||||
o._on_load_failure_callback = on_load_failure_callback
|
||||
o._conflict_resolver = conflict_resolver
|
||||
o._init_plugins(extensions)
|
||||
return o
|
||||
|
||||
def _init_plugins(self, extensions: list[Extension[T]]) -> None:
|
||||
self.extensions: list[Extension[T]] = extensions
|
||||
self._extensions_by_name_cache: dict[str, Extension[T]] | None = None
|
||||
|
||||
@property
|
||||
def _extensions_by_name(self) -> dict[str, Extension[T]]:
|
||||
if self._extensions_by_name_cache is None:
|
||||
d = {}
|
||||
for name, _extensions in itertools.groupby(
|
||||
self.extensions, lambda x: x.name
|
||||
):
|
||||
extensions = list(_extensions)
|
||||
if len(extensions) > 1:
|
||||
ext = self._conflict_resolver(
|
||||
self.namespace, name, extensions
|
||||
)
|
||||
else:
|
||||
ext = extensions[0]
|
||||
|
||||
d[name] = ext
|
||||
|
||||
self._extensions_by_name_cache = d
|
||||
return self._extensions_by_name_cache
|
||||
|
||||
def list_entry_points(self) -> list[importlib.metadata.EntryPoint]:
|
||||
"""Return the list of entry points for this namespace.
|
||||
|
||||
The entry points are not actually loaded, their list is just read and
|
||||
returned.
|
||||
"""
|
||||
if self.namespace not in self.ENTRY_POINT_CACHE:
|
||||
eps = list(_cache.get_group_all(self.namespace))
|
||||
self.ENTRY_POINT_CACHE[self.namespace] = eps
|
||||
return self.ENTRY_POINT_CACHE[self.namespace]
|
||||
|
||||
def entry_points_names(self) -> list[str]:
|
||||
"""Return the list of entry points names for this namespace."""
|
||||
return list(map(operator.attrgetter("name"), self.list_entry_points()))
|
||||
|
||||
def _load_plugins(
|
||||
self,
|
||||
invoke_on_load: bool,
|
||||
invoke_args: tuple[Any, ...],
|
||||
invoke_kwds: dict[str, Any],
|
||||
) -> list[Extension[T]]:
|
||||
extensions = []
|
||||
for ep in self.list_entry_points():
|
||||
LOG.debug('found extension %r', ep)
|
||||
try:
|
||||
ext = self._load_one_plugin(
|
||||
ep, invoke_on_load, invoke_args, invoke_kwds
|
||||
)
|
||||
if ext:
|
||||
extensions.append(ext)
|
||||
except (KeyboardInterrupt, AssertionError):
|
||||
raise
|
||||
except Exception as err:
|
||||
if self._on_load_failure_callback is not None:
|
||||
self._on_load_failure_callback(self, ep, err)
|
||||
else:
|
||||
# Log the reason we couldn't import the module,
|
||||
# usually without a traceback. The most common
|
||||
# reason is an ImportError due to a missing
|
||||
# dependency, and the error message should be
|
||||
# enough to debug that. If debug logging is
|
||||
# enabled for our logger, provide the full
|
||||
# traceback.
|
||||
LOG.error(
|
||||
'Could not load %r: %s',
|
||||
ep.name,
|
||||
err,
|
||||
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
||||
)
|
||||
return extensions
|
||||
|
||||
# NOTE(stephenfin): While this can't return None, all the subclasses can,
|
||||
# and this allows us to satisfy Liskov's Principle. `_load_plugins` handles
|
||||
# things just fine in either case.
|
||||
def _load_one_plugin(
|
||||
self,
|
||||
ep: importlib.metadata.EntryPoint,
|
||||
invoke_on_load: bool,
|
||||
invoke_args: tuple[Any],
|
||||
invoke_kwds: dict[str, Any],
|
||||
) -> Extension[T] | None:
|
||||
plugin = ep.load()
|
||||
if invoke_on_load:
|
||||
obj = plugin(*invoke_args, **invoke_kwds)
|
||||
else:
|
||||
obj = None
|
||||
return Extension(ep.name, ep, plugin, obj)
|
||||
|
||||
def names(self) -> list[str]:
|
||||
"""Returns the names of the discovered extensions"""
|
||||
# We want to return the names of the extensions in the order
|
||||
# they would be used by map(), since some subclasses change
|
||||
# that order.
|
||||
return [e.name for e in self.extensions]
|
||||
|
||||
def map(
|
||||
self,
|
||||
func: Callable[Concatenate[Extension[T], P], U],
|
||||
*args: P.args,
|
||||
**kwds: P.kwargs,
|
||||
) -> list[U]:
|
||||
"""Iterate over the extensions invoking func() for each.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
if not self.extensions:
|
||||
# FIXME: Use a more specific exception class here.
|
||||
raise NoMatches(f'No {self.namespace} extensions found')
|
||||
response: list[U] = []
|
||||
for e in self.extensions:
|
||||
self._invoke_one_plugin(response.append, func, e, *args, **kwds)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _call_extension_method(
|
||||
extension: Extension[T], /, method_name: str, *args: Any, **kwds: Any
|
||||
) -> Any:
|
||||
return getattr(extension.obj, method_name)(*args, **kwds)
|
||||
|
||||
def map_method(self, method_name: str, *args: Any, **kwds: Any) -> Any:
|
||||
"""Iterate over the extensions invoking a method by name.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param method_name: The extension method name
|
||||
to call for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(
|
||||
self._call_extension_method, method_name, *args, **kwds
|
||||
)
|
||||
|
||||
def _invoke_one_plugin(
|
||||
self,
|
||||
response_callback: Callable[..., Any],
|
||||
func: Callable[Concatenate[Extension[T], P], U],
|
||||
e: Extension[T],
|
||||
*args: P.args,
|
||||
**kwds: P.kwargs,
|
||||
) -> None:
|
||||
try:
|
||||
response_callback(func(e, *args, **kwds))
|
||||
except Exception as err:
|
||||
if self.propagate_map_exceptions:
|
||||
raise
|
||||
else:
|
||||
LOG.error('error calling %r: %s', e.name, err)
|
||||
LOG.exception(err)
|
||||
|
||||
def items(self) -> ItemsView[str, Extension[T]]:
|
||||
"""Return an iterator of tuples of the form (name, extension).
|
||||
|
||||
This is analogous to the Mapping.items() method.
|
||||
"""
|
||||
return self._extensions_by_name.items()
|
||||
|
||||
def __iter__(self) -> Iterator[Extension[T]]:
|
||||
"""Produce iterator for the manager.
|
||||
|
||||
Iterating over an ExtensionManager produces the :class:`Extension`
|
||||
instances in the order they would be invoked.
|
||||
"""
|
||||
return iter(self.extensions)
|
||||
|
||||
def __getitem__(self, name: str) -> Extension[T]:
|
||||
"""Return the named extension.
|
||||
|
||||
Accessing an ExtensionManager as a dictionary (``em['name']``)
|
||||
produces the :class:`Extension` instance with the specified name.
|
||||
"""
|
||||
return self._extensions_by_name[name]
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
"""Return true if name is in list of enabled extensions."""
|
||||
return any(extension.name == name for extension in self.extensions)
|
||||
107
Backend/venv/lib/python3.12/site-packages/stevedore/hook.py
Normal file
107
Backend/venv/lib/python3.12/site-packages/stevedore/hook.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import Any
|
||||
from typing import TypeVar
|
||||
|
||||
from .extension import ConflictResolverT
|
||||
from .extension import Extension
|
||||
from .extension import ignore_conflicts
|
||||
from .extension import OnLoadFailureCallbackT
|
||||
from .named import NamedExtensionManager
|
||||
from .named import OnMissingEntrypointsCallbackT
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class HookManager(NamedExtensionManager[T]):
|
||||
"""Coordinate execution of multiple extensions using a common name.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param name: The name of the hooks to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param on_missing_entrypoints_callback: Callback function that will be
|
||||
called when one or more names cannot be found. The provided argument
|
||||
will be a subset of the 'names' parameter.
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param warn_on_missing_entrypoint: **DEPRECATED** Flag to control whether
|
||||
failing to load a plugin is reported via a log mess. Only applies if
|
||||
on_missing_entrypoints_callback is None. Users should instead set
|
||||
``on_missing_entrypoints_callback`` to ``None`` if they wish to disable
|
||||
logging.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with the
|
||||
same name. This is only used if retrieving entrypoint by name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
name: str,
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
# NOTE(dhellmann): This default is different from the
|
||||
# base class because for hooks it is less likely to
|
||||
# be an error to have no entry points present.
|
||||
on_missing_entrypoints_callback: (
|
||||
OnMissingEntrypointsCallbackT | None
|
||||
) = None,
|
||||
verify_requirements: bool | None = None,
|
||||
warn_on_missing_entrypoint: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
):
|
||||
invoke_args = () if invoke_args is None else invoke_args
|
||||
invoke_kwds = {} if invoke_kwds is None else invoke_kwds
|
||||
|
||||
super().__init__(
|
||||
namespace,
|
||||
[name],
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
on_missing_entrypoints_callback=on_missing_entrypoints_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
warn_on_missing_entrypoint=warn_on_missing_entrypoint,
|
||||
)
|
||||
|
||||
@property
|
||||
def _name(self) -> str:
|
||||
return self._names[0]
|
||||
|
||||
def __getitem__( # type: ignore[override]
|
||||
self, name: str
|
||||
) -> list[Extension[T]]:
|
||||
"""Return the named extensions.
|
||||
|
||||
Accessing a HookManager as a dictionary (``em['name']``)
|
||||
produces a list of the :class:`Extension` instance(s) with the
|
||||
specified name, in the order they would be invoked by map().
|
||||
"""
|
||||
if name != self._name:
|
||||
raise KeyError(name)
|
||||
return self.extensions
|
||||
218
Backend/venv/lib/python3.12/site-packages/stevedore/named.py
Normal file
218
Backend/venv/lib/python3.12/site-packages/stevedore/named.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Sequence
|
||||
import importlib.metadata
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
import warnings
|
||||
|
||||
from .extension import ConflictResolverT
|
||||
from .extension import Extension
|
||||
from .extension import ExtensionManager
|
||||
from .extension import ignore_conflicts
|
||||
from .extension import OnLoadFailureCallbackT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
OnMissingEntrypointsCallbackT = Callable[[Iterable[str]], None]
|
||||
|
||||
|
||||
def warning_on_missing_entrypoint(missing_names: Iterable[str]) -> None:
|
||||
LOG.warning('Could not load {}'.format(', '.join(missing_names)))
|
||||
|
||||
|
||||
class NamedExtensionManager(ExtensionManager[T]):
|
||||
"""Loads only the named extensions.
|
||||
|
||||
This is useful for explicitly enabling extensions in a
|
||||
configuration file, for example.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param names: The names of the extensions to load.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param name_order: If true, sort the loaded extensions to match the
|
||||
order used in ``names``.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
an entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:param on_missing_entrypoints_callback: Callback function that will be
|
||||
called when one or more names cannot be found. The provided argument
|
||||
will be a subset of the 'names' parameter.
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param warn_on_missing_entrypoint: **DEPRECATED** Flag to control whether
|
||||
failing to load a plugin is reported via a log mess. Only applies if
|
||||
on_missing_entrypoints_callback is None. Users should instead set
|
||||
``on_missing_entrypoints_callback`` to ``None`` if they wish to disable
|
||||
logging.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with the
|
||||
same name. This is only used if retrieving entrypoint by name.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
namespace: str,
|
||||
names: Sequence[str],
|
||||
invoke_on_load: bool = False,
|
||||
invoke_args: tuple[Any, ...] | None = None,
|
||||
invoke_kwds: dict[str, Any] | None = None,
|
||||
name_order: bool = False,
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
on_missing_entrypoints_callback: (
|
||||
OnMissingEntrypointsCallbackT | None
|
||||
) = warning_on_missing_entrypoint,
|
||||
verify_requirements: bool | None = None,
|
||||
warn_on_missing_entrypoint: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> None:
|
||||
self._names = names
|
||||
self._missing_names: set[str] = set()
|
||||
self._name_order = name_order
|
||||
|
||||
if warn_on_missing_entrypoint is not None:
|
||||
warnings.warn(
|
||||
"The warn_on_missing_entrypoint option is deprecated for "
|
||||
"removal. If you wish to disable warnings, you should instead "
|
||||
"override 'on_missing_entrypoints_callback'",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if not warn_on_missing_entrypoint:
|
||||
on_missing_entrypoints_callback = None
|
||||
|
||||
self._on_missing_entrypoints_callback = on_missing_entrypoints_callback
|
||||
|
||||
super().__init__(
|
||||
namespace,
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
conflict_resolver=conflict_resolver,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_test_instance(
|
||||
cls,
|
||||
extensions: list[Extension[T]],
|
||||
namespace: str = 'TESTING',
|
||||
propagate_map_exceptions: bool = False,
|
||||
on_load_failure_callback: 'OnLoadFailureCallbackT[T] | None' = None,
|
||||
verify_requirements: bool | None = None,
|
||||
*,
|
||||
conflict_resolver: 'ConflictResolverT[T]' = ignore_conflicts,
|
||||
) -> 'Self':
|
||||
"""Construct a test NamedExtensionManager
|
||||
|
||||
Test instances are passed a list of extensions to use rather than
|
||||
loading them from entry points.
|
||||
|
||||
:param extensions: Pre-configured Extension instances
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged
|
||||
and then ignored
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when an entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:param verify_requirements: **DEPRECATED** This is a no-op and will be
|
||||
removed in a future version.
|
||||
:param conflict_resolver: A callable that determines what to do in the
|
||||
event that there are multiple entrypoints in the same group with
|
||||
the same name. This is only used if retrieving entrypoint by name.
|
||||
:return: The manager instance, initialized for testing
|
||||
"""
|
||||
if verify_requirements is not None:
|
||||
warnings.warn(
|
||||
'The verify_requirements argument is now a no-op and is '
|
||||
'deprecated for removal. Remove the argument from calls.',
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
o = cls.__new__(cls)
|
||||
o.namespace = namespace
|
||||
o.propagate_map_exceptions = propagate_map_exceptions
|
||||
o._on_load_failure_callback = on_load_failure_callback
|
||||
o._names = [e.name for e in extensions]
|
||||
o._missing_names = set()
|
||||
o._name_order = False
|
||||
o._conflict_resolver = conflict_resolver
|
||||
o._init_plugins(extensions)
|
||||
return o
|
||||
|
||||
def _init_plugins(self, extensions: list[Extension[T]]) -> None:
|
||||
super()._init_plugins(extensions)
|
||||
|
||||
if self._name_order:
|
||||
self.extensions = [
|
||||
self[n] for n in self._names if n not in self._missing_names
|
||||
]
|
||||
|
||||
def _load_plugins(
|
||||
self,
|
||||
invoke_on_load: bool,
|
||||
invoke_args: tuple[Any, ...],
|
||||
invoke_kwds: dict[str, Any],
|
||||
) -> list[Extension[T]]:
|
||||
extensions = super()._load_plugins(
|
||||
invoke_on_load, invoke_args, invoke_kwds
|
||||
)
|
||||
|
||||
self._missing_names = set(self._names) - {e.name for e in extensions}
|
||||
if self._missing_names and self._on_missing_entrypoints_callback:
|
||||
self._on_missing_entrypoints_callback(self._missing_names)
|
||||
|
||||
return extensions
|
||||
|
||||
def _load_one_plugin(
|
||||
self,
|
||||
ep: importlib.metadata.EntryPoint,
|
||||
invoke_on_load: bool,
|
||||
invoke_args: tuple[Any, ...],
|
||||
invoke_kwds: dict[str, Any],
|
||||
) -> Extension[T] | None:
|
||||
# Check the name before going any further to prevent
|
||||
# undesirable code from being loaded at all if we are not
|
||||
# going to use it.
|
||||
if ep.name not in self._names:
|
||||
return None
|
||||
|
||||
return super()._load_one_plugin(
|
||||
ep, invoke_on_load, invoke_args, invoke_kwds
|
||||
)
|
||||
138
Backend/venv/lib/python3.12/site-packages/stevedore/sphinxext.py
Normal file
138
Backend/venv/lib/python3.12/site-packages/stevedore/sphinxext.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Sequence
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
from typing import Any
|
||||
from typing import TypeVar
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers import rst
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.statemachine import StringList
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.nodes import nested_parse_with_titles
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def _get_docstring(plugin: Callable[..., T]) -> str:
|
||||
return inspect.getdoc(plugin) or ''
|
||||
|
||||
|
||||
def _simple_list(
|
||||
mgr: extension.ExtensionManager[T],
|
||||
) -> Iterable[tuple[str, str]]:
|
||||
for name in sorted(mgr.names()):
|
||||
ext = mgr[name]
|
||||
doc = _get_docstring(ext.plugin) or '\n'
|
||||
summary = doc.splitlines()[0].strip()
|
||||
yield (f'* {ext.name} -- {summary}', ext.module_name)
|
||||
|
||||
|
||||
def _detailed_list(
|
||||
mgr: extension.ExtensionManager[T],
|
||||
over: str = '',
|
||||
under: str = '-',
|
||||
titlecase: bool = False,
|
||||
) -> Iterable[tuple[str, str]]:
|
||||
for name in sorted(mgr.names()):
|
||||
ext = mgr[name]
|
||||
if over:
|
||||
yield (over * len(ext.name), ext.module_name)
|
||||
if titlecase:
|
||||
yield (ext.name.title(), ext.module_name)
|
||||
else:
|
||||
yield (ext.name, ext.module_name)
|
||||
if under:
|
||||
yield (under * len(ext.name), ext.module_name)
|
||||
yield ('\n', ext.module_name)
|
||||
doc = _get_docstring(ext.plugin)
|
||||
if doc:
|
||||
yield (doc, ext.module_name)
|
||||
else:
|
||||
yield (
|
||||
f'.. warning:: No documentation found for {ext.name} in '
|
||||
f'{ext.entry_point_target}',
|
||||
ext.module_name,
|
||||
)
|
||||
yield ('\n', ext.module_name)
|
||||
|
||||
|
||||
class ListPluginsDirective(rst.Directive):
|
||||
"""Present a simple list of the plugins in a namespace."""
|
||||
|
||||
option_spec = {
|
||||
'class': directives.class_option,
|
||||
'detailed': directives.flag,
|
||||
'titlecase': directives.flag,
|
||||
'overline-style': directives.single_char_or_unicode,
|
||||
'underline-style': directives.single_char_or_unicode,
|
||||
}
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self) -> Sequence[nodes.Node]:
|
||||
namespace = ' '.join(self.content).strip()
|
||||
LOG.info(f'documenting plugins from {namespace!r}')
|
||||
overline_style = self.options.get('overline-style', '')
|
||||
underline_style = self.options.get('underline-style', '=')
|
||||
|
||||
def report_load_failure(
|
||||
mgr: extension.ExtensionManager[T],
|
||||
ep: importlib.metadata.EntryPoint,
|
||||
err: BaseException,
|
||||
) -> None:
|
||||
LOG.warning(f'Failed to load {ep.module}: {err}')
|
||||
|
||||
mgr: extension.ExtensionManager[Any]
|
||||
mgr = extension.ExtensionManager(
|
||||
namespace, on_load_failure_callback=report_load_failure
|
||||
)
|
||||
|
||||
result = StringList()
|
||||
|
||||
titlecase = 'titlecase' in self.options
|
||||
|
||||
if 'detailed' in self.options:
|
||||
data = _detailed_list(
|
||||
mgr,
|
||||
over=overline_style,
|
||||
under=underline_style,
|
||||
titlecase=titlecase,
|
||||
)
|
||||
else:
|
||||
data = _simple_list(mgr)
|
||||
for text, source in data:
|
||||
for line in text.splitlines():
|
||||
result.append(line, source)
|
||||
|
||||
# Parse what we have into a new section.
|
||||
node = nodes.section()
|
||||
node.document = self.state.document
|
||||
nested_parse_with_titles(self.state, result, node)
|
||||
|
||||
return node.children
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> dict[str, Any]:
|
||||
LOG.info('loading stevedore.sphinxext')
|
||||
app.add_directive('list-plugins', ListPluginsDirective)
|
||||
return {'parallel_read_safe': True, 'parallel_write_safe': True}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,64 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""TestExtensionManager
|
||||
|
||||
Extension manager used only for testing.
|
||||
"""
|
||||
|
||||
from typing import TypeVar
|
||||
import warnings
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class TestExtensionManager(extension.ExtensionManager[T]):
|
||||
"""ExtensionManager that is explicitly initialized for tests.
|
||||
|
||||
.. deprecated:: 0.13
|
||||
|
||||
Use the :func:`make_test_instance` class method of the class
|
||||
being replaced by the test instance instead of using this class
|
||||
directly.
|
||||
|
||||
:param extensions: Pre-configured Extension instances to use instead of
|
||||
loading them from entry points.
|
||||
:param namespace: The namespace for the entry points.
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
extensions,
|
||||
namespace='test',
|
||||
invoke_on_load=False,
|
||||
invoke_args=(),
|
||||
invoke_kwds={},
|
||||
):
|
||||
super().__init__(namespace, invoke_on_load, invoke_args, invoke_kwds)
|
||||
self.extensions = extensions
|
||||
warnings.warn(
|
||||
'TestExtesionManager has been replaced by make_test_instance()',
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
def _load_plugins(self, *args, **kwds):
|
||||
return []
|
||||
@@ -0,0 +1,63 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore._cache"""
|
||||
|
||||
import sys
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from stevedore import _cache
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestCache(utils.TestCase):
|
||||
def test_disable_caching_executable(self):
|
||||
"""Test caching is disabled if python interpreter is located under /tmp
|
||||
directory (Ansible)
|
||||
"""
|
||||
with mock.patch.object(sys, 'executable', '/tmp/fake'):
|
||||
sot = _cache.Cache()
|
||||
self.assertTrue(sot._disable_caching)
|
||||
|
||||
def test_disable_caching_file(self):
|
||||
"""Test caching is disabled if .disable file is present in target
|
||||
dir
|
||||
"""
|
||||
cache_dir = _cache._get_cache_dir()
|
||||
|
||||
with mock.patch('os.path.isfile') as mock_path:
|
||||
mock_path.return_value = True
|
||||
sot = _cache.Cache()
|
||||
mock_path.assert_called_with(f'{cache_dir}/.disable')
|
||||
self.assertTrue(sot._disable_caching)
|
||||
|
||||
mock_path.return_value = False
|
||||
sot = _cache.Cache()
|
||||
self.assertFalse(sot._disable_caching)
|
||||
|
||||
@mock.patch('os.makedirs')
|
||||
@mock.patch('builtins.open')
|
||||
def test__get_data_for_path_no_write(self, mock_open, mock_mkdir):
|
||||
sot = _cache.Cache()
|
||||
sot._disable_caching = True
|
||||
mock_open.side_effect = IOError
|
||||
sot._get_data_for_path(('fake',))
|
||||
mock_mkdir.assert_not_called()
|
||||
|
||||
def test__build_cacheable_data(self):
|
||||
# this is a rubbish test as we don't actually do anything with the
|
||||
# data, but it's too hard to script since it's totally environmentally
|
||||
# dependent and mocking out the underlying calls would remove the value
|
||||
# of this test (we want to test those underlying API calls)
|
||||
ret = _cache._build_cacheable_data()
|
||||
self.assertIsInstance(ret['groups'], dict)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for failure loading callback"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from stevedore import extension
|
||||
from stevedore import named
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_extension_failure_custom_callback(self):
|
||||
errors = []
|
||||
|
||||
def failure_callback(manager, entrypoint, error):
|
||||
errors.append((manager, entrypoint, error))
|
||||
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
on_load_failure_callback=failure_callback,
|
||||
)
|
||||
extensions = list(em.extensions)
|
||||
self.assertGreater(len(extensions), 0)
|
||||
self.assertEqual(len(errors), 2)
|
||||
for manager, entrypoint, error in errors:
|
||||
self.assertIs(manager, em)
|
||||
self.assertIsInstance(error, (IOError, ImportError))
|
||||
|
||||
@mock.patch('stevedore.extension.ExtensionManager._load_plugins')
|
||||
def test_missing_entrypoints_callback(self, load_fn):
|
||||
errors = set()
|
||||
|
||||
def callback(names):
|
||||
errors.update(names)
|
||||
|
||||
load_fn.return_value = [
|
||||
extension.Extension('foo', None, None, None) # type: ignore
|
||||
]
|
||||
named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['foo', 'bar'],
|
||||
invoke_on_load=True,
|
||||
on_missing_entrypoints_callback=callback,
|
||||
)
|
||||
self.assertEqual(errors, {'bar'})
|
||||
@@ -0,0 +1,103 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from stevedore import dispatch
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
def check_dispatch(ep, /, *args, **kwds):
|
||||
return ep.name == 't2'
|
||||
|
||||
|
||||
class TestDispatch(utils.TestCase):
|
||||
def test_dispatch(self):
|
||||
def invoke(ep, /, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em: dispatch.DispatchExtensionManager[Any]
|
||||
em = dispatch.DispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
self.assertEqual(set(em.names()), {'t1', 't2'})
|
||||
|
||||
results = em.map(check_dispatch, invoke, 'first', named='named value')
|
||||
expected = [('t2', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_dispatch_map_method(self):
|
||||
em: dispatch.DispatchExtensionManager[Any]
|
||||
em = dispatch.DispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map_method(check_dispatch, 'get_args_and_data', 'first')
|
||||
self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')])
|
||||
|
||||
def test_name_dispatch(self):
|
||||
def invoke(ep, /, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em: dispatch.DispatchExtensionManager[Any]
|
||||
em = dispatch.NameDispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
self.assertEqual(set(em.names()), {'t1', 't2'})
|
||||
|
||||
results = em.map(['t2'], invoke, 'first', named='named value')
|
||||
expected = [('t2', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_name_dispatch_ignore_missing(self):
|
||||
def invoke(ep, /, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em: dispatch.DispatchExtensionManager[Any]
|
||||
em = dispatch.NameDispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map(['t3', 't1'], invoke, 'first', named='named value')
|
||||
expected = [('t1', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_name_dispatch_map_method(self):
|
||||
em: dispatch.DispatchExtensionManager[Any]
|
||||
em = dispatch.NameDispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map_method(['t3', 't1'], 'get_args_and_data', 'first')
|
||||
self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')])
|
||||
@@ -0,0 +1,101 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.extension"""
|
||||
|
||||
import importlib.metadata
|
||||
from typing import Any
|
||||
|
||||
from stevedore import driver
|
||||
from stevedore import exception
|
||||
from stevedore import extension
|
||||
from stevedore.tests import test_extension
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_detect_plugins(self):
|
||||
em: driver.DriverManager[Any]
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1')
|
||||
names = sorted(em.names())
|
||||
self.assertEqual(names, ['t1'])
|
||||
|
||||
def test_call(self):
|
||||
def invoke(ext, /, *args, **kwds):
|
||||
return (ext.name, args, kwds)
|
||||
|
||||
em: driver.DriverManager[Any]
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1')
|
||||
result = em(invoke, 'a', b='C')
|
||||
self.assertEqual(result, ('t1', ('a',), {'b': 'C'}))
|
||||
|
||||
def test_driver_property_not_invoked_on_load(self):
|
||||
em: driver.DriverManager[Any]
|
||||
em = driver.DriverManager(
|
||||
'stevedore.test.extension', 't1', invoke_on_load=False
|
||||
)
|
||||
d = em.driver
|
||||
self.assertIs(d, test_extension.FauxExtension)
|
||||
|
||||
def test_driver_property_invoked_on_load(self):
|
||||
em: driver.DriverManager[Any]
|
||||
em = driver.DriverManager(
|
||||
'stevedore.test.extension', 't1', invoke_on_load=True
|
||||
)
|
||||
d = em.driver
|
||||
self.assertIsInstance(d, test_extension.FauxExtension)
|
||||
|
||||
def test_no_drivers(self):
|
||||
try:
|
||||
driver.DriverManager('stevedore.test.extension.none', 't1')
|
||||
except exception.NoMatches as err:
|
||||
self.assertIn(
|
||||
"No 'stevedore.test.extension.none' driver found", str(err)
|
||||
)
|
||||
|
||||
def test_bad_driver(self):
|
||||
try:
|
||||
driver.DriverManager('stevedore.test.extension', 'e2')
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
self.assertEqual(False, "No error raised")
|
||||
|
||||
def test_multiple_drivers(self):
|
||||
# The idea for this test was contributed by clayg:
|
||||
# https://gist.github.com/clayg/6311348
|
||||
extensions: list[extension.Extension[Any]] = [
|
||||
extension.Extension(
|
||||
'backend',
|
||||
importlib.metadata.EntryPoint(
|
||||
'backend', 'pkg1:driver', 'backend'
|
||||
),
|
||||
lambda x: None,
|
||||
None,
|
||||
),
|
||||
extension.Extension(
|
||||
'backend',
|
||||
importlib.metadata.EntryPoint(
|
||||
'backend', 'pkg2:driver', 'backend'
|
||||
),
|
||||
lambda x: None,
|
||||
None,
|
||||
),
|
||||
]
|
||||
try:
|
||||
dm = driver.DriverManager.make_test_instance(extensions[0])
|
||||
# Call the initialization code that verifies the extension
|
||||
dm._init_plugins(extensions)
|
||||
except exception.MultipleMatches as err:
|
||||
self.assertIn("Multiple", str(err))
|
||||
else:
|
||||
self.fail('Should have had an error')
|
||||
@@ -0,0 +1,44 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from stevedore import enabled
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestEnabled(utils.TestCase):
|
||||
def test_enabled(self):
|
||||
def check_enabled(ep):
|
||||
return ep.name == 't2'
|
||||
|
||||
em = enabled.EnabledExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
check_enabled,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t2'])
|
||||
|
||||
def test_enabled_after_load(self):
|
||||
def check_enabled(ext):
|
||||
return ext.obj and ext.name == 't2'
|
||||
|
||||
em = enabled.EnabledExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
check_enabled,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t2'])
|
||||
@@ -0,0 +1,39 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.example2.fields"""
|
||||
|
||||
from stevedore.example2 import fields
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestExampleFields(utils.TestCase):
|
||||
def test_simple_items(self):
|
||||
f = fields.FieldList(100)
|
||||
text = ''.join(f.format({'a': 'A', 'b': 'B'}))
|
||||
expected = '\n'.join([': a : A', ': b : B', ''])
|
||||
self.assertEqual(text, expected)
|
||||
|
||||
def test_long_item(self):
|
||||
f = fields.FieldList(25)
|
||||
text = ''.join(
|
||||
f.format({'name': 'a value longer than the allowed width'})
|
||||
)
|
||||
expected = '\n'.join(
|
||||
[
|
||||
': name : a value longer',
|
||||
' than the allowed',
|
||||
' width',
|
||||
'',
|
||||
]
|
||||
)
|
||||
self.assertEqual(text, expected)
|
||||
@@ -0,0 +1,24 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.example.simple"""
|
||||
|
||||
from stevedore.example import simple
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestExampleSimple(utils.TestCase):
|
||||
def test_simple_items(self):
|
||||
f = simple.Simple(100)
|
||||
text = ''.join(f.format({'a': 'A', 'b': 'B'}))
|
||||
expected = '\n'.join(['a = A', 'b = B', ''])
|
||||
self.assertEqual(text, expected)
|
||||
@@ -0,0 +1,375 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.extension"""
|
||||
|
||||
import importlib.metadata
|
||||
import operator
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
import warnings
|
||||
|
||||
from stevedore import exception
|
||||
from stevedore import extension
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
ALL_NAMES = ['e1', 't1', 't2']
|
||||
WORKING_NAMES = ['t1', 't2']
|
||||
|
||||
|
||||
class FauxExtension:
|
||||
def __init__(self, *args, **kwds):
|
||||
self.args = args
|
||||
self.kwds = kwds
|
||||
|
||||
def get_args_and_data(self, data):
|
||||
return self.args, self.kwds, data
|
||||
|
||||
|
||||
class BrokenExtension:
|
||||
def __init__(self, *args, **kwds):
|
||||
raise OSError("Did not create")
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_detect_plugins(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = sorted(em.names())
|
||||
self.assertEqual(names, ALL_NAMES)
|
||||
|
||||
def test_get_by_name(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
e = em['t1']
|
||||
self.assertEqual(e.name, 't1')
|
||||
|
||||
def test_list_entry_points(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
n = em.list_entry_points()
|
||||
self.assertEqual(
|
||||
{'e1', 'e2', 't1', 't2'}, set(map(operator.attrgetter("name"), n))
|
||||
)
|
||||
self.assertEqual(4, len(n))
|
||||
|
||||
def test_list_entry_points_names(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = em.entry_points_names()
|
||||
self.assertEqual({'e1', 'e2', 't1', 't2'}, set(names))
|
||||
self.assertEqual(4, len(names))
|
||||
|
||||
def test_contains_by_name(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
self.assertIn('t1', em, True)
|
||||
|
||||
def test_get_by_name_missing(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
try:
|
||||
em['t3']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
assert False, 'Failed to raise KeyError'
|
||||
|
||||
def test_load_multiple_times_entry_points(self):
|
||||
# We expect to get the same EntryPoint object because we save them
|
||||
# in the cache.
|
||||
em1: extension.ExtensionManager[Any]
|
||||
em1 = extension.ExtensionManager('stevedore.test.extension')
|
||||
eps1 = [ext.entry_point for ext in em1]
|
||||
em2: extension.ExtensionManager[Any]
|
||||
em2 = extension.ExtensionManager('stevedore.test.extension')
|
||||
eps2 = [ext.entry_point for ext in em2]
|
||||
self.assertIs(eps1[0], eps2[0])
|
||||
|
||||
def test_load_multiple_times_plugins(self):
|
||||
# We expect to get the same plugin object (module or class)
|
||||
# because the underlying import machinery will cache the values.
|
||||
em1: extension.ExtensionManager[Any]
|
||||
em1 = extension.ExtensionManager('stevedore.test.extension')
|
||||
plugins1 = [ext.plugin for ext in em1]
|
||||
em2: extension.ExtensionManager[Any]
|
||||
em2 = extension.ExtensionManager('stevedore.test.extension')
|
||||
plugins2 = [ext.plugin for ext in em2]
|
||||
self.assertIs(plugins1[0], plugins2[0])
|
||||
|
||||
def test_use_cache(self):
|
||||
# If we insert something into the cache of entry points,
|
||||
# the manager should not have to call into entrypoints
|
||||
# to find the plugins.
|
||||
cache = extension.ExtensionManager.ENTRY_POINT_CACHE
|
||||
cache['stevedore.test.faux'] = []
|
||||
with mock.patch(
|
||||
'stevedore._cache.get_group_all',
|
||||
side_effect=AssertionError('called get_group_all'),
|
||||
):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.faux')
|
||||
names = em.names()
|
||||
self.assertEqual(names, [])
|
||||
|
||||
def test_iterable(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = sorted(e.name for e in em)
|
||||
self.assertEqual(names, ALL_NAMES)
|
||||
|
||||
def test_invoke_on_load(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
for e in em.extensions:
|
||||
assert e.obj is not None
|
||||
self.assertEqual(e.obj.args, ('a',))
|
||||
self.assertEqual(e.obj.kwds, {'b': 'B'})
|
||||
|
||||
def test_map_return_values(self):
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
return ext.name
|
||||
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension', invoke_on_load=True
|
||||
)
|
||||
results = em.map(mapped)
|
||||
self.assertEqual(sorted(results), WORKING_NAMES)
|
||||
|
||||
def test_map_arguments(self):
|
||||
objs = []
|
||||
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
objs.append((ext, args, kwds))
|
||||
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension', invoke_on_load=True
|
||||
)
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
self.assertEqual(len(objs), 2)
|
||||
names = sorted([o[0].name for o in objs])
|
||||
self.assertEqual(names, WORKING_NAMES)
|
||||
for o in objs:
|
||||
self.assertEqual(o[1], (1, 2))
|
||||
self.assertEqual(o[2], {'a': 'A', 'b': 'B'})
|
||||
|
||||
def test_map_eats_errors(self):
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
raise RuntimeError('hard coded error')
|
||||
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension', invoke_on_load=True
|
||||
)
|
||||
results = em.map(mapped, 1, 2, a='A', b='B')
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_map_propagate_exceptions(self):
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
raise RuntimeError('hard coded error')
|
||||
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
propagate_map_exceptions=True,
|
||||
)
|
||||
|
||||
try:
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
assert False
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def test_map_errors_when_no_plugins(self):
|
||||
expected_str = 'No stevedore.test.extension.none extensions found'
|
||||
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
pass
|
||||
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension.none', invoke_on_load=True
|
||||
)
|
||||
try:
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
except exception.NoMatches as err:
|
||||
self.assertEqual(expected_str, str(err))
|
||||
|
||||
def test_map_method(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager(
|
||||
'stevedore.test.extension', invoke_on_load=True
|
||||
)
|
||||
|
||||
result = em.map_method('get_args_and_data', 42)
|
||||
self.assertEqual({r[2] for r in result}, {42})
|
||||
|
||||
def test_items(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
expected_output = {(name, em[name]) for name in ALL_NAMES}
|
||||
self.assertEqual(expected_output, set(em.items()))
|
||||
|
||||
|
||||
class TestConflictResolution(utils.TestCase):
|
||||
def test_ignore_conflicts(self):
|
||||
"""Test that ignore_conflicts logs a warning when conflicts exist."""
|
||||
extensions = [
|
||||
extension.Extension(
|
||||
'conflict',
|
||||
importlib.metadata.EntryPoint(
|
||||
'conflict', 'module1:Class1', 'test.group'
|
||||
),
|
||||
type('TestClass1', (), {}),
|
||||
None,
|
||||
),
|
||||
extension.Extension(
|
||||
'conflict',
|
||||
importlib.metadata.EntryPoint(
|
||||
'conflict', 'module2:Class2', 'test.group'
|
||||
),
|
||||
type('TestClass2', (), {}),
|
||||
None,
|
||||
),
|
||||
]
|
||||
|
||||
with self.assertLogs('stevedore.extension', level='WARNING') as log:
|
||||
result = extension.ignore_conflicts(
|
||||
'test.group', 'conflict', extensions
|
||||
)
|
||||
|
||||
self.assertIs(result, extensions[-1])
|
||||
self.assertEqual(len(log.records), 1)
|
||||
warning_msg = log.records[0].getMessage()
|
||||
self.assertIn("multiple implementations found", warning_msg)
|
||||
self.assertIn("'conflict' extension", warning_msg)
|
||||
self.assertIn("test.group namespace", warning_msg)
|
||||
|
||||
def test_error_on_conflict(self):
|
||||
"""Test error_on_conflict raises MultipleMatches exception."""
|
||||
extensions = [
|
||||
extension.Extension(
|
||||
'conflict',
|
||||
importlib.metadata.EntryPoint(
|
||||
'conflict', 'module1:Class1', 'test.group'
|
||||
),
|
||||
type('TestClass1', (), {}),
|
||||
None,
|
||||
),
|
||||
extension.Extension(
|
||||
'conflict',
|
||||
importlib.metadata.EntryPoint(
|
||||
'conflict', 'module2:Class2', 'test.group'
|
||||
),
|
||||
type('TestClass2', (), {}),
|
||||
None,
|
||||
),
|
||||
]
|
||||
|
||||
with self.assertRaises(exception.MultipleMatches) as cm:
|
||||
extension.error_on_conflict('test.group', 'conflict', extensions)
|
||||
|
||||
error_msg = str(cm.exception)
|
||||
self.assertIn("multiple implementations found", error_msg)
|
||||
self.assertIn("'conflict' command", error_msg)
|
||||
self.assertIn("test.group namespace", error_msg)
|
||||
|
||||
def test_custom_conflict_resolver(self):
|
||||
"""Test using a custom conflict resolver function."""
|
||||
|
||||
def first_resolver(namespace, name, extensions):
|
||||
return extensions[0]
|
||||
|
||||
ext1 = extension.Extension(
|
||||
'test',
|
||||
importlib.metadata.EntryPoint(
|
||||
'test', 'module1:Class1', 'test.group'
|
||||
),
|
||||
type('TestClass1', (), {}),
|
||||
None,
|
||||
)
|
||||
ext2 = extension.Extension(
|
||||
'test',
|
||||
importlib.metadata.EntryPoint(
|
||||
'test', 'module2:Class2', 'test.group'
|
||||
),
|
||||
type('TestClass2', (), {}),
|
||||
None,
|
||||
)
|
||||
|
||||
em = extension.ExtensionManager.make_test_instance(
|
||||
[ext1, ext2], conflict_resolver=first_resolver
|
||||
)
|
||||
|
||||
# Should get the first extension when accessing by name
|
||||
result = em['test']
|
||||
self.assertIs(result, ext1)
|
||||
|
||||
|
||||
class TestDeprecations(utils.TestCase):
|
||||
def test_verify_requirements(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter('always')
|
||||
extension.ExtensionManager.make_test_instance(
|
||||
[], verify_requirements=True
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(w))
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertIn(
|
||||
'The verify_requirements argument is now a no-op',
|
||||
str(w[-1].message),
|
||||
)
|
||||
|
||||
|
||||
class TestExtensionProperties(utils.TestCase):
|
||||
def setUp(self):
|
||||
self.ext1 = extension.Extension(
|
||||
'name',
|
||||
importlib.metadata.EntryPoint(
|
||||
'name', 'module.name:attribute.name [extra]', 'group_name'
|
||||
),
|
||||
mock.Mock(),
|
||||
None,
|
||||
)
|
||||
self.ext2 = extension.Extension(
|
||||
'name',
|
||||
importlib.metadata.EntryPoint(
|
||||
'name', 'module:attribute', 'group_name'
|
||||
),
|
||||
mock.Mock(),
|
||||
None,
|
||||
)
|
||||
|
||||
def test_module_name(self):
|
||||
self.assertEqual('module.name', self.ext1.module_name)
|
||||
self.assertEqual('module', self.ext2.module_name)
|
||||
|
||||
def test_attr(self):
|
||||
self.assertEqual('attribute.name', self.ext1.attr)
|
||||
self.assertEqual('attribute', self.ext2.attr)
|
||||
|
||||
def test_entry_point_target(self):
|
||||
self.assertEqual(
|
||||
'module.name:attribute.name [extra]', self.ext1.entry_point_target
|
||||
)
|
||||
self.assertEqual('module:attribute', self.ext2.entry_point_target)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from stevedore import hook
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestHook(utils.TestCase):
|
||||
def test_hook(self):
|
||||
em: hook.HookManager[Any]
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t1'])
|
||||
|
||||
def test_get_by_name(self):
|
||||
em: hook.HookManager[Any]
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
e_list = em['t1']
|
||||
self.assertEqual(len(e_list), 1)
|
||||
e = e_list[0]
|
||||
self.assertEqual(e.name, 't1')
|
||||
|
||||
def test_get_by_name_missing(self):
|
||||
em: hook.HookManager[Any]
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
try:
|
||||
em['t2']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
assert False, 'Failed to raise KeyError'
|
||||
@@ -0,0 +1,94 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
from stevedore import named
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestNamed(utils.TestCase):
|
||||
def test_named(self):
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['t1'],
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t1'])
|
||||
|
||||
def test_enabled_before_load(self):
|
||||
# Set up the constructor for the FauxExtension to cause an
|
||||
# AssertionError so the test fails if the class is instantiated,
|
||||
# which should only happen if it is loaded before the name of the
|
||||
# extension is compared against the names that should be loaded by
|
||||
# the manager.
|
||||
init_name = 'stevedore.tests.test_extension.FauxExtension.__init__'
|
||||
with mock.patch(init_name) as m:
|
||||
m.side_effect = AssertionError
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
# Look for an extension that does not exist so the
|
||||
# __init__ we mocked should never be invoked.
|
||||
names=['no-such-extension'],
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, [])
|
||||
|
||||
def test_extensions_listed_in_name_order(self):
|
||||
# Since we don't know the "natural" order of the extensions, run
|
||||
# the test both ways: if the sorting is broken, one of them will
|
||||
# fail
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension', names=['t1', 't2'], name_order=True
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t1', 't2'])
|
||||
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension', names=['t2', 't1'], name_order=True
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t2', 't1'])
|
||||
|
||||
def test_load_fail_ignored_when_sorted(self):
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['e1', 't2', 'e2', 't1'],
|
||||
name_order=True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(['t2', 't1'], actual)
|
||||
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['e1', 't1'],
|
||||
name_order=False,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(['t1'], actual)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for the sphinx extension"""
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from stevedore import extension
|
||||
from stevedore import sphinxext
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
def _make_ext(name, docstring):
|
||||
def inner():
|
||||
pass
|
||||
|
||||
inner.__doc__ = docstring
|
||||
m1 = importlib.metadata.EntryPoint(name, f'{name}_module:{name}', 'group')
|
||||
return extension.Extension(name, m1, inner, None)
|
||||
|
||||
|
||||
class TestSphinxExt(utils.TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.exts = [
|
||||
_make_ext('test1', 'One-line docstring'),
|
||||
_make_ext('test2', 'Multi-line docstring\n\nAnother para'),
|
||||
]
|
||||
self.em = extension.ExtensionManager.make_test_instance(self.exts)
|
||||
|
||||
def test_simple_list(self):
|
||||
results = list(sphinxext._simple_list(self.em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('* test1 -- One-line docstring', 'test1_module'),
|
||||
('* test2 -- Multi-line docstring', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_simple_list_no_docstring(self):
|
||||
ext = [_make_ext('nodoc', None)]
|
||||
em = extension.ExtensionManager.make_test_instance(ext)
|
||||
results = list(sphinxext._simple_list(em))
|
||||
self.assertEqual([('* nodoc -- ', 'nodoc_module')], results)
|
||||
|
||||
def test_detailed_list(self):
|
||||
results = list(sphinxext._detailed_list(self.em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('test1', 'test1_module'),
|
||||
('-----', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('One-line docstring', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('test2', 'test2_module'),
|
||||
('-----', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
('Multi-line docstring\n\nAnother para', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_detailed_list_format(self):
|
||||
results = list(sphinxext._detailed_list(self.em, over='+', under='+'))
|
||||
self.assertEqual(
|
||||
[
|
||||
('+++++', 'test1_module'),
|
||||
('test1', 'test1_module'),
|
||||
('+++++', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('One-line docstring', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('+++++', 'test2_module'),
|
||||
('test2', 'test2_module'),
|
||||
('+++++', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
('Multi-line docstring\n\nAnother para', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_detailed_list_no_docstring(self):
|
||||
ext = [_make_ext('nodoc', None)]
|
||||
em = extension.ExtensionManager.make_test_instance(ext)
|
||||
results = list(sphinxext._detailed_list(em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('nodoc', 'nodoc_module'),
|
||||
('-----', 'nodoc_module'),
|
||||
('\n', 'nodoc_module'),
|
||||
(
|
||||
(
|
||||
'.. warning:: No documentation found for '
|
||||
'nodoc in nodoc_module:nodoc'
|
||||
),
|
||||
'nodoc_module',
|
||||
),
|
||||
('\n', 'nodoc_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
@@ -0,0 +1,263 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import sentinel
|
||||
|
||||
from stevedore import dispatch
|
||||
from stevedore import driver
|
||||
from stevedore import enabled
|
||||
from stevedore import extension
|
||||
from stevedore import hook
|
||||
from stevedore import named
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
test_extension = extension.Extension(
|
||||
'test_extension',
|
||||
None, # type: ignore
|
||||
None, # type: ignore
|
||||
None,
|
||||
)
|
||||
test_extension2 = extension.Extension(
|
||||
'another_one',
|
||||
None, # type: ignore
|
||||
None, # type: ignore
|
||||
None,
|
||||
)
|
||||
|
||||
mock_entry_point = Mock(module_name='test.extension', attrs=['obj'])
|
||||
a_driver = extension.Extension(
|
||||
'test_driver',
|
||||
mock_entry_point,
|
||||
sentinel.driver_plugin,
|
||||
sentinel.driver_obj,
|
||||
)
|
||||
|
||||
|
||||
# base ExtensionManager
|
||||
class TestTestManager(utils.TestCase):
|
||||
def test_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = extension.ExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_instance_should_have_default_namespace(self):
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager.make_test_instance([])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_instance_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em: extension.ExtensionManager[Any]
|
||||
em = extension.ExtensionManager.make_test_instance(
|
||||
[], namespace=namespace
|
||||
)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_extension_name_should_be_listed(self):
|
||||
em = extension.ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertIn(test_extension.name, em.names())
|
||||
|
||||
def test_iterator_should_yield_extension(self):
|
||||
em = extension.ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertEqual(test_extension, next(iter(em)))
|
||||
|
||||
def test_manager_should_allow_name_access(self):
|
||||
em = extension.ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertEqual(test_extension, em[test_extension.name])
|
||||
|
||||
def test_manager_should_call(self):
|
||||
em = extension.ExtensionManager.make_test_instance([test_extension])
|
||||
func = Mock()
|
||||
em.map(func)
|
||||
func.assert_called_once_with(test_extension)
|
||||
|
||||
def test_manager_should_call_all(self):
|
||||
em = extension.ExtensionManager.make_test_instance(
|
||||
[test_extension2, test_extension]
|
||||
)
|
||||
func = Mock()
|
||||
em.map(func)
|
||||
func.assert_any_call(test_extension2)
|
||||
func.assert_any_call(test_extension)
|
||||
|
||||
def test_manager_return_values(self):
|
||||
def mapped(ext, /, *args, **kwds):
|
||||
return ext.name
|
||||
|
||||
em = extension.ExtensionManager.make_test_instance(
|
||||
[test_extension2, test_extension]
|
||||
)
|
||||
results = em.map(mapped)
|
||||
self.assertEqual(sorted(results), ['another_one', 'test_extension'])
|
||||
|
||||
def test_manager_should_eat_exceptions(self):
|
||||
em = extension.ExtensionManager.make_test_instance([test_extension])
|
||||
|
||||
func = Mock(side_effect=RuntimeError('hard coded error'))
|
||||
|
||||
results = em.map(func, 1, 2, a='A', b='B')
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_manager_should_propagate_exceptions(self):
|
||||
em = extension.ExtensionManager.make_test_instance(
|
||||
[test_extension], propagate_map_exceptions=True
|
||||
)
|
||||
func = Mock(side_effect=RuntimeError('hard coded error'))
|
||||
self.assertRaises(RuntimeError, em.map, func, 1, 2, a='A', b='B')
|
||||
|
||||
# NamedExtensionManager
|
||||
def test_named_manager_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = named.NamedExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_named_manager_should_have_default_namespace(self):
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager.make_test_instance([])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_named_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em: named.NamedExtensionManager[Any]
|
||||
em = named.NamedExtensionManager.make_test_instance(
|
||||
[], namespace=namespace
|
||||
)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_named_manager_should_populate_names(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = named.NamedExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(em.names(), ['test_extension', 'another_one'])
|
||||
|
||||
# HookManager
|
||||
def test_hook_manager_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = hook.HookManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_hook_manager_should_be_first_extension_name(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = hook.HookManager.make_test_instance(extensions)
|
||||
# This will raise KeyError if the names don't match
|
||||
assert em[test_extension.name]
|
||||
|
||||
def test_hook_manager_should_have_default_namespace(self):
|
||||
em = hook.HookManager.make_test_instance([test_extension])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_hook_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = hook.HookManager.make_test_instance(
|
||||
[test_extension], namespace=namespace
|
||||
)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_hook_manager_should_return_named_extensions(self):
|
||||
hook1 = extension.Extension(
|
||||
'captain',
|
||||
None, # type: ignore
|
||||
None, # type: ignore
|
||||
None,
|
||||
)
|
||||
hook2 = extension.Extension(
|
||||
'captain',
|
||||
None, # type: ignore
|
||||
None, # type: ignore
|
||||
None,
|
||||
)
|
||||
em = hook.HookManager.make_test_instance([hook1, hook2])
|
||||
self.assertEqual([hook1, hook2], em['captain'])
|
||||
|
||||
# DriverManager
|
||||
def test_driver_manager_should_use_supplied_extension(self):
|
||||
em = driver.DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual([a_driver], em.extensions)
|
||||
|
||||
def test_driver_manager_should_have_default_namespace(self):
|
||||
em = driver.DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_driver_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = driver.DriverManager.make_test_instance(
|
||||
a_driver, namespace=namespace
|
||||
)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_instance_should_use_driver_name(self):
|
||||
em = driver.DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(['test_driver'], em.names())
|
||||
|
||||
def test_instance_call(self):
|
||||
def invoke(ext, /, *args, **kwds):
|
||||
return ext.name, args, kwds
|
||||
|
||||
em = driver.DriverManager.make_test_instance(a_driver)
|
||||
result = em(invoke, 'a', b='C')
|
||||
self.assertEqual(result, ('test_driver', ('a',), {'b': 'C'}))
|
||||
|
||||
def test_instance_driver_property(self):
|
||||
em = driver.DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(sentinel.driver_obj, em.driver)
|
||||
|
||||
# EnabledExtensionManager
|
||||
def test_enabled_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = enabled.EnabledExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
# DispatchExtensionManager
|
||||
def test_dispatch_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = dispatch.DispatchExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_dispatch_map_should_invoke_filter_for_extensions(self):
|
||||
em = dispatch.DispatchExtensionManager.make_test_instance(
|
||||
[test_extension, test_extension2]
|
||||
)
|
||||
filter_func = Mock(return_value=False)
|
||||
args = ('A',)
|
||||
kw = {'big': 'Cheese'}
|
||||
em.map(filter_func, None, *args, **kw) # type: ignore
|
||||
filter_func.assert_any_call(test_extension, *args, **kw)
|
||||
filter_func.assert_any_call(test_extension2, *args, **kw)
|
||||
|
||||
# NameDispatchExtensionManager
|
||||
def test_name_dispatch_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = dispatch.NameDispatchExtensionManager.make_test_instance(
|
||||
extensions
|
||||
)
|
||||
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_name_dispatch_instance_should_build_extension_name_map(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = dispatch.NameDispatchExtensionManager.make_test_instance(
|
||||
extensions
|
||||
)
|
||||
self.assertEqual(test_extension, em.by_name[test_extension.name])
|
||||
self.assertEqual(test_extension2, em.by_name[test_extension2.name])
|
||||
|
||||
def test_named_dispatch_map_should_invoke_filter_for_extensions(self):
|
||||
em = dispatch.NameDispatchExtensionManager.make_test_instance(
|
||||
[test_extension, test_extension2]
|
||||
)
|
||||
func = Mock()
|
||||
args = ('A',)
|
||||
kw = {'BIGGER': 'Cheese'}
|
||||
em.map(['test_extension'], func, *args, **kw)
|
||||
func.assert_called_once_with(test_extension, *args, **kw)
|
||||
@@ -0,0 +1,17 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
pass
|
||||
Reference in New Issue
Block a user