-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Add initial support for plugins #12985
base: main
Are you sure you want to change the base?
Conversation
src/pip/_internal/utils/plugins.py
Outdated
from pathlib import Path | ||
from typing import Iterator, List | ||
|
||
from pip._vendor.pygments.plugin import iter_entry_points |
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?
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:
groups = entry_points()
if hasattr(groups, "select"):
# New interface in Python 3.10 and newer versions of the
# importlib_metadata backport.
return groups.select(group=group_name)
else:
assert hasattr(groups, "get")
# Older interface, deprecated in Python 3.10 and recent
# importlib_metadata, but we need it in Python 3.8 and 3.9.
return groups.get(group_name, [])
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.
src/pip/_internal/utils/plugins.py
Outdated
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 comment
The 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 load_plugins()
function that gets called at a suitable point in pip's initialisation process? Or better still, load plugins on demand, because we don't want to impact performance loading plugins we don't need.
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 moved it into a load_plugins
and called it right before command.main(cmd_args)
.
For loading plugins on demand, maybe we can change utils.plugins.plugin_pre_download_hook
and utils.pugins.plugin_pre_extract_hook
to check if plugins have been loaded, and load them if not.
Then the plugins should only be loaded when the first hook is called (and never if the code path does not include any hooks).
WDYT?
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.
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 uv
has demonstrated that performance is an important characteristic in an installer, and I want to make sure we avoid making pip slower and slower as we add features.
I'm still not clear on the status of the dist-inspector plugin type. Is it intended as an actual, supported plugin type for pip, and more to the point, is it intended to be the only supported plugin type (until someone submits PRs providing other plugin types)? What evidence do we have that this is plugin type is important? And where are the use cases (issues, feature requests) providing evidence that people need this plugin type and will use it? I'm still trying to make the point that we shouldn't be adding a plugin mechanism in isolation. Rather, we should have a real world use case that justifies being implemented as a plugin, and we should create a plugin mechanism in support of implementing a solution for that use case. Specifically, with a concrete use case, we can decide whether it warrants something as general as a plugin approach, or if a builtin feature specific to that use case would be a better solution. At the moment, this still feels like a solution looking for a problem. |
Yes, it's intended as an actual plugin type for pip. One of the motivations behind the Instead, we could use a plugin (of type (edit: Support for downloading the PEP-740 attestations from PyPI is almost complete. Once it's finished, I'll create a repo with a plugin that does the above)
Unless there is a strong need for other types, I think starting with just one is a good idea to keep things simple |
Thank you. That had been mentioned before, but I'd forgotten. The sigstore dependency is a good example of why a plugin is needed. |
de5dc8a
to
0b0e276
Compare
0b0e276
to
9279db4
Compare
Signed-off-by: Facundo Tuesca <[email protected]>
615b54b
to
c8233a1
Compare
After some thought, I think having plugin "types" would cause more pain than necessary. For example, let's say at some point someone comes up with a valid usecase for a "distribution inspector" plugin that requires a hook different than the ones defined originally for
Given that, I propose that instead of having plugin types, each plugin reports which hooks it wants to implement: def provided_hooks() -> List[str]:
return ["pre_download"]
def pre_download() -> None:
... That way, when I've pushed a commit that makes that change. I've also created a repo for a plugin that verifies PEP-740 attestations before installing packages. It implements only the |
This is a PR with an initial implementation of the plugin support discussed in #12766.
High-level description
Plugins will be detected and loaded via registered entrypoints. For example, a Python package that contains the following in
pyproject.toml
:will register the
example-distinspector
plugin by providing thepip_plugin_example
module object. This module object will be loaded bypip
and its methods will be called at different places while pip runs.The methods that will be called (and the places where they will be called from) depend on the plugin type. For now, this implementation only supports a single plugin type:
dist-inspector
, which should provide three functions:These functions (provided by the plugin in module
pip_plugin_example
) will be called bypip
right before downloading or extracting a distribution file.A repository with an example plugin package is here.
Implementation details
models/plugin.py
andutils/plugins.py
). The only change to existing files is adding a function call in the specific places the plugin should run (before download and before extraction).ValueError
, in case of other exception types I put a defensive check that logs a warning and converts the exception to aValueError
. This means that plugins that don't follow the contract will still work (with a warning). If we want to avoid that and just abort when a plugin misbehaves, this check needs to be changed.Open questions
pre-download
andpre-extract
) correct?TODO
cc @woodruffw