forked from ApeWorX/ape
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
move PluginManager to src/ape/managers/plugins.py and clean_plugin_na…
…me to ape/plugins/_utils to avoid circular import
- Loading branch information
Showing
6 changed files
with
166 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import importlib | ||
import pkgutil | ||
import subprocess | ||
from typing import Generator, Iterator, List, Optional, Set, Tuple | ||
|
||
from ape.__modules__ import __modules__ | ||
from ape.exceptions import ApeAttributeError | ||
from ape.logging import logger | ||
from ape.plugins._utils import PIP_COMMAND, clean_plugin_name | ||
from ape.utils.basemodel import _assert_not_ipython_check | ||
from ape.utils.misc import log_instead_of_fail | ||
|
||
|
||
class PluginManager: | ||
_unimplemented_plugins: List[str] = [] | ||
|
||
def __init__(self) -> None: | ||
self.__registered = False | ||
|
||
@log_instead_of_fail(default="<PluginManager>") | ||
def __repr__(self) -> str: | ||
return f"<{PluginManager.__name__}>" | ||
|
||
def __getattr__(self, attr_name: str) -> Iterator[Tuple[str, Tuple]]: | ||
_assert_not_ipython_check(attr_name) | ||
|
||
# NOTE: The first time this method is called, the actual | ||
# plugin registration occurs. Registration only happens once. | ||
self._register_plugins() | ||
|
||
if not hasattr(pluggy_manager.hook, attr_name): | ||
raise ApeAttributeError(f"{PluginManager.__name__} has no attribute '{attr_name}'.") | ||
|
||
# Do this to get access to the package name | ||
hook_fn = getattr(pluggy_manager.hook, attr_name) | ||
hookimpls = hook_fn.get_hookimpls() | ||
|
||
def get_plugin_name_and_hookfn(h): | ||
return h.plugin_name, getattr(h.plugin, attr_name)() | ||
|
||
for plugin_name, results in map(get_plugin_name_and_hookfn, hookimpls): | ||
# NOTE: Some plugins return a tuple and some return iterators | ||
if not isinstance(results, Generator): | ||
validated_plugin = self._validate_plugin(plugin_name, results) | ||
if validated_plugin: | ||
yield validated_plugin | ||
else: | ||
# Only if it's an iterator, provider results as a series | ||
for result in results: | ||
validated_plugin = self._validate_plugin(plugin_name, result) | ||
if validated_plugin: | ||
yield validated_plugin | ||
|
||
@property | ||
def registered_plugins(self) -> Set[str]: | ||
self._register_plugins() | ||
return {x[0] for x in pluggy_manager.list_name_plugin()} | ||
|
||
@functools.cached_property | ||
def _plugin_modules(self) -> Tuple[str, ...]: | ||
core_plugin_module_names = {n for _, n, _ in pkgutil.iter_modules() if n.startswith("ape_")} | ||
if PIP_COMMAND[0] != "uv": | ||
# NOTE: Unable to use pkgutil.iter_modules() for installed plugins | ||
# because it does not work with editable installs. | ||
# See https://github.com/python/cpython/issues/99805. | ||
result = subprocess.check_output( | ||
["pip", "list", "--format", "freeze", "--disable-pip-version-check"] | ||
) | ||
packages = result.decode("utf8").splitlines() | ||
installed_plugin_module_names = { | ||
p.split("==")[0].replace("-", "_") for p in packages if p.startswith("ape-") | ||
} | ||
else: | ||
result = subprocess.check_output(["uv", "pip", "list"]) | ||
# format is in the output of: | ||
# Package Version Editable project location | ||
# ------------------------- ------------------------------- ------------------------- | ||
# aiosignal 1.3.1 | ||
# annotated-types 0.6.0 | ||
# skip the header | ||
packages = result.decode("utf8").splitlines()[2:] | ||
installed_plugin_module_names = { | ||
p.split(" ")[0].replace("-", "_") for p in packages if p.startswith("ape-") | ||
} | ||
|
||
# NOTE: Returns tuple because this shouldn't change. | ||
return tuple(installed_plugin_module_names.union(core_plugin_module_names)) | ||
|
||
def _register_plugins(self): | ||
if self.__registered: | ||
return | ||
|
||
for module_name in self._plugin_modules: | ||
try: | ||
module = importlib.import_module(module_name) | ||
pluggy_manager.register(module) | ||
except Exception as err: | ||
if module_name in __modules__: | ||
# Always raise core plugin registration errors. | ||
raise | ||
|
||
logger.warn_from_exception(err, f"Error loading plugin package '{module_name}'.") | ||
|
||
self.__registered = True | ||
|
||
def _validate_plugin(self, plugin_name: str, plugin_cls) -> Optional[Tuple[str, Tuple]]: | ||
if valid_impl(plugin_cls): | ||
return clean_plugin_name(plugin_name), plugin_cls | ||
else: | ||
self._warn_not_fully_implemented_error(plugin_cls, plugin_name) | ||
return None | ||
|
||
def _warn_not_fully_implemented_error(self, results, plugin_name): | ||
if plugin_name in self._unimplemented_plugins: | ||
# Already warned | ||
return | ||
|
||
unimplemented_methods = [] | ||
|
||
# Find the best API name to warn about. | ||
if isinstance(results, (list, tuple)): | ||
classes = [p for p in results if hasattr(p, "__name__")] | ||
if classes: | ||
# Likely only ever a single class in a registration, but just in case. | ||
api_name = " - ".join([p.__name__ for p in classes if hasattr(p, "__name__")]) | ||
for api_cls in classes: | ||
if ( | ||
abstract_methods := getattr(api_cls, "__abstractmethods__", None) | ||
) and isinstance(abstract_methods, dict): | ||
unimplemented_methods.extend(api_cls.__abstractmethods__) | ||
|
||
else: | ||
# This would only happen if the registration consisted of all primitives. | ||
api_name = " - ".join(results) | ||
|
||
elif hasattr(results, "__name__"): | ||
api_name = results.__name__ | ||
if (abstract_methods := getattr(results, "__abstractmethods__", None)) and isinstance( | ||
abstract_methods, dict | ||
): | ||
unimplemented_methods.extend(results.__abstractmethods__) | ||
|
||
else: | ||
api_name = results | ||
|
||
message = f"'{api_name}' from '{plugin_name}' is not fully implemented." | ||
if unimplemented_methods: | ||
methods_str = ", ".join(unimplemented_methods) | ||
message = f"{message} Remaining abstract methods: '{methods_str}'." | ||
|
||
logger.warning(message) | ||
|
||
# Record so we don't warn repeatedly | ||
self._unimplemented_plugins.append(plugin_name) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters