-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add initial support for plugins #12985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
2cb0ca5
c6e5935
d5b3da5
9279db4
c8233a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import abc | ||
import logging | ||
from pathlib import Path | ||
from types import ModuleType | ||
from typing import Optional | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
PLUGIN_TYPE_DIST_INSPECTOR = "dist-inspector" | ||
SUPPORTED_PLUGIN_TYPES = [PLUGIN_TYPE_DIST_INSPECTOR] | ||
|
||
|
||
class Plugin(metaclass=abc.ABCMeta): | ||
@abc.abstractmethod | ||
def plugin_type(self) -> str: | ||
raise NotImplementedError | ||
|
||
@property | ||
@abc.abstractmethod | ||
def name(self) -> str: | ||
raise NotImplementedError | ||
|
||
|
||
class DistInspectorPlugin(Plugin): | ||
def __init__(self, name: str, loaded_module: ModuleType): | ||
assert loaded_module.plugin_type() == PLUGIN_TYPE_DIST_INSPECTOR | ||
if not hasattr(loaded_module, "pre_download") or not hasattr( | ||
loaded_module, "pre_extract" | ||
): | ||
raise ValueError( | ||
f'Plugin "{name}" of type {PLUGIN_TYPE_DIST_INSPECTOR} is' | ||
"missing pre_download and/or pre_extract definitions" | ||
) | ||
|
||
self._name = name | ||
self._module = loaded_module | ||
|
||
def plugin_type(self) -> str: | ||
return self._module.plugin_type() | ||
|
||
@property | ||
def name(self) -> str: | ||
return self._name | ||
|
||
def pre_download(self, url: str, filename: str, digest: str) -> None: | ||
# contract: `pre_download` raises `ValueError` to terminate | ||
# the operation that intends to download `filename` from `url` | ||
# with hash `digest` 8000 span> | ||
self._module.pre_download(url=url, filename=filename, digest=digest) | ||
|
||
def pre_extract(self, dist: Path) -> None: | ||
# contract: `pre_extract` raises `ValueError` to terminate | ||
# the operation that intends to unarchive `dist` | ||
self._module.pre_extract(dist) | ||
|
||
|
||
def plugin_from_module(name: str, loaded_module: ModuleType) -> Optional[Plugin]: | ||
if not hasattr(loaded_module, "plugin_type"): | ||
logger.warning("Ignoring plugin %s due to missing plugin_type definition", name) | ||
plugin_type = loaded_module.plugin_type() | ||
if plugin_type not in SUPPORTED_PLUGIN_TYPES: | ||
logger.warning( | ||
"Ignoring plugin %s due to unknown plugin type: %s", name, plugin_type | ||
) | ||
|
||
if plugin_type == PLUGIN_TYPE_DIST_INSPECTOR: | ||
try: | ||
return DistInspectorPlugin(name, loaded_module) | ||
except ValueError as e: | ||
logger.warning("Ignoring plugin %s due to error: %s", name, e) | ||
return None |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import contextlib | ||
import logging | ||
from pathlib import Path | ||
from typing import Iterator, List | ||
|
||
from pip._vendor.pygments.plugin import iter_entry_points | ||
|
||
from pip._internal.models.plugin import DistInspectorPlugin, Plugin, plugin_from_module | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
_loaded_plugins: List[Plugin] = [] | ||
for entrypoint in iter_entry_points(group_name="pip.plugins"): | ||
try: | ||
module = entrypoint.load() | ||
except ModuleNotFoundError: | ||
logger.warning("Tried to load plugin %s but failed", entrypoint.name) | ||
continue | ||
plugin = plugin_from_module(entrypoint.name, module) | ||
if plugin is not None: | ||
_loaded_plugins.append(plugin) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a great fan of running complex code at import time. Couldn't we have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! I moved it into a For loading plugins on demand, maybe we can change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That'll do for now, but let's keep on-demand loading as an option, and do some proper testing at some point. I'm very aware that |
||
|
||
|
||
@contextlib.contextmanager | ||
def _only_raise_value_error(plugin_name: str) -> Iterator[None]: | ||
try: | ||
yield | ||
except ValueError as e: | ||
raise ValueError(f"Plugin {plugin_name}: {e}") from e | ||
except Exception as e: | ||
logger.warning( | ||
"Plugin %s raised an unexpected exception type: %s", | ||
plugin_name, | ||
{e.__class__.__name__}, | ||
) | ||
raise ValueError(f"Plugin {plugin_name}: {e}") from e | ||
|
||
|
||
def plugin_pre_download_hook(url: str, filename: str, digest: str) -> None: | ||
"""Call the pre-download hook of all loaded plugins | ||
|
||
This function should be called right before a distribution is downloaded. | ||
It will go through all the loaded plugins and call their `pre_download(url)` | ||
function. | ||
Only ValueError will be raised. If the plugin (incorrectly) raises another | ||
exception type, this function will wrap it as a ValueError and log | ||
a warning. | ||
""" | ||
|
||
for p in _loaded_plugins: | ||
if not isinstance(p, DistInspectorPlugin): | ||
continue | ||
with _only_raise_value_error(p.name): | ||
p.pre_download(url=url, filename=filename, digest=digest) | ||
|
||
|
||
def plugin_pre_extract_hook(dist: Path) -> None: | ||
"""Call the pre-extract hook of all loaded plugins | ||
|
||
This function should be called right before a distribution is extracted. | ||
It will go through all the loaded plugins and call their `pre_extract(dist)` | ||
function. | ||
Only ValueError will be raised. If the plugin (incorrectly) raises another | ||
exception type, this function will wrap it as a ValueError and log | ||
a warning. | ||
""" | ||
|
||
for p in _loaded_plugins: | ||
if not isinstance(p, DistInspectorPlugin): | ||
continue | ||
with _only_raise_value_error(p.name): | ||
p.pre_extract(dist) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't be using an undocumented helper function from pygments here. It could disappear without warning. If we need the functionality we should implement our own copy. Although isn't this available in
importlib.metadata
anyway?All reactions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point! I copied the logic to this file. The reason why we need it is because the API for
EntryPoints
changed in Python 3.10, so the way to get entry points matching a specific group name depends on the Python version:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm so sick of the instability of the
importlib.{metadata, resources}
APIs, but we have to work with what we have, I guess :-(Thanks for doing this.