-
Notifications
You must be signed in to change notification settings - Fork 2
Variant Plugin Auto install #86
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
Conversation
variantlib/api.py
Outdated
itertools.chain.from_iterable( | ||
provider_cfg.to_list_of_properties() | ||
for provider_cfg in plugin_loader.get_supported_configs().values() | ||
with PluginLoader(variant_nfo=parsed_variants_json, isolated=False) as plugin_ctx: |
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 confused at what's happening here. What's the plan for isolated=True
support?
variantlib/installer.py
Outdated
def _get_pip_index_urls() -> tuple[list[str], list[str]]: | ||
# Load pip configuration | ||
configuration = pip._internal.configuration.Configuration( # noqa: SLF001 | ||
isolated=False, load_only=None | ||
) | ||
configuration.load() | ||
|
||
def unpack_pip_index_config_value(val: str | list[str] | tuple[str]) -> list[str]: | ||
val: list[str] | tuple[str] | ||
return [v.strip() for v in val.split("\n") if v.strip()] | ||
|
||
# Retrieve index-url and extra-index-url values | ||
# index_url = configuration.get_value("global.index-url") | ||
# extra_index_urls = configuration.get_value("global.extra-index-url") | ||
index_url = [] | ||
extra_index_urls = [] | ||
for key, val in configuration.items(): | ||
if key in ["global.index-url", "install.index-url"]: | ||
index_url.extend(unpack_pip_index_config_value(val)) | ||
if key in ["global.extra-index-url", "install.extra-index-url"]: | ||
extra_index_urls.extend(unpack_pip_index_config_value(val)) | ||
|
||
return list(set(index_url)), list(set(extra_index_urls)) |
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.
This looks out of place. I don't think it's really for variantlib to deal with pip configs.
variantlib/installer.py
Outdated
_env_backend: _EnvBackend | ||
|
||
def __init__(self) -> None: | ||
if shutil.which("uv") is not None: |
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.
This is a risky assumption. Given that the code will be primarily used by pip
, I dare say it should be using pip
rather than calling uv
internally.
variantlib/installer.py
Outdated
def install_requirements( | ||
self, requirements: Collection[str], py_exec: str | None = None | ||
) -> None: | ||
index_urls, extra_index_urls = _get_pip_index_urls() |
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.
Do we actually need to handle pip config for pip?
variantlib/installer.py
Outdated
) -> None: | ||
index_urls, extra_index_urls = _get_pip_index_urls() | ||
|
||
with self.prepare_requirements(requirements) as req_file: |
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.
Do we need to worry that pip
will call pip
, which in turn will call pip
and we'd end up with infinite recursion over this?
variantlib/installer.py
Outdated
|
||
def __enter__(self) -> Self: | ||
try: | ||
self._path = pathlib.Path(tempfile.mkdtemp(prefix="variant-env-")) |
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.
tempfile.TemporaryDirectory
?
variantlib/installer.py
Outdated
|
||
def _create_venv(self, path: pathlib.Path) -> tuple[str, str]: | ||
try: | ||
import virtualenv |
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 virtualenv
at all. venv
is supported on all Python versions which we support, it's simpler and faster.
variantlib/installer.py
Outdated
try: | ||
venv.EnvBuilder( | ||
symlinks=_fs_supports_symlink(), | ||
with_pip=True, |
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.
Shouldn't this be false?
variantlib/loader.py
Outdated
if self._installer_ctx is None or self._plugins is not None: | ||
raise RuntimeError( | ||
"Impossible to get supported configs outside of an installer context" | ||
) |
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.
Honestly, it looks like instead of using two separate classes for inside and outside the context, you are trying to use a single class which doesn't really seem to have any benefit.
variantlib/loader.py
Outdated
location=pathlib.Path( | ||
self._installer_ctx.python_executable | ||
).parent.parent, |
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.
This doesn't seem like a robust assumption to make.
By the way, does this logic handle dependencies? How does it handle different versions of modules present in the isolated and host environments (say, variantlib
)?
Hmm -- I was assuming that the installer (i.e., the thing calling variantlib) would be responsible for all of the plugin interaction, and that variantlib would just consist of pure functions (similar to packaging). The fact that pip would call variantlib, which would then call pip, seems like a sign that the control flow is going the wrong way, maybe? Copying over my comments from Discord:
|
@charliermarsh, that's pretty much what I said — we need to hook into installer's isolation and install logic, which is already there. I'm currently working on pip, so hopefully I'll get there and find a reasonably clean way of doing that. |
@@ -50,22 +54,34 @@ | |||
def get_variant_hashes_by_priority( | |||
*, | |||
variants_json: dict, | |||
plugin_loader: PluginLoader, |
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 don't really like losing the flexibility of being able to provide a custom PluginLoader
instance and effectively forcing people to copy over the whole logic if they need to do something nonstandard. At least until it's 100% clear that we won't need it ever.
@@ -50,22 +54,34 @@ | |||
def get_variant_hashes_by_priority( | |||
*, | |||
variants_json: dict, | |||
plugin_loader: PluginLoader, | |||
use_auto_install: bool = True, | |||
venv_path: str | pathlib.Path | None = None, |
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.
Why not just require Path
? It's a new API.
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 think it's pretty rare for APIs to absolutely force a Path.
In general people have a silent conversation inside APIs
yield ctx | ||
|
||
except Exception: | ||
logger.exception("An error occured during plugin installation.") |
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.
Do we really want to silently proceed when environment setup fails? Wouldn't this lead to unpredictable behavior and/or confusing failures afterwards?
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 wasn't sure ... I can raise an exception if you want. Would you create a new custom one ?
variantlib/plugins/py_envs.py
Outdated
sys.path.insert(0, str(ctx.package_dir)) | ||
yield ctx | ||
sys.path = original_sys_path |
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've already said and I'll repeat it: switching context to a different environment without process isolation is a very bad idea. If anything, you end up mixing dependencies from inside the isolated environment with already-loaded modules from the environment running variantlib. Effectively, plugin dependencies won't be respected and plugins will end up randomly using different versions of their dependencies than they specified (and we've installed).
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.
Fair I agree - so how would you fix that ? Because it's not like variantlib
can access a different venv.
I thought about this issue - but I didnt have a better idea
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.
Honestly, let's leave it for now and "back to the drawing board" after PyCon. I'm afraid we need to change how plugins work.
variantlib/plugins/py_envs.py
Outdated
class IsolatedPythonEnvMixin: | ||
_venv_path: pathlib.Path | None = None | ||
|
||
@property | ||
def python_executable(self) -> pathlib.Path: | ||
return self._get_venv_path(0) | ||
|
||
@property | ||
def script_dir(self) -> pathlib.Path: | ||
return self._get_venv_path(1) | ||
|
||
@property | ||
def package_dir(self) -> pathlib.Path: | ||
return self._get_venv_path(2) | ||
|
||
def _get_venv_path(self, idx: int) -> pathlib.Path: | ||
if self._venv_path is None or not self._venv_path.exists(): | ||
raise FileNotFoundError | ||
assert 0 <= idx <= 2 | ||
return pathlib.Path(_find_executable_and_scripts(str(self._venv_path))[idx]) |
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 don't get this at all. Why aren't we just calling _find_executable_and_scripts()
in __init__()
and setting them all as class attributes?
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.
Oh because the venv is not created when the environment is created but rather when you enter the contextmanager.
Maybe this could be done differently - feel free to experiment
757a502
to
ddee54b
Compare
Alright - this PR is fairly large so here are a few commands you can run to test:
Must-Do / Prepare
1. Make-Variant
2. Analyze Platform
3. Config Setup
$ variantlib config setup -p provider_fictional_hw.plugin:FictionalHWPlugin variantlib.plugins.py_envs - INFO - Using externally managed python environment variantlib.plugins.loader - INFO - Loading plugin via provider_fictional_hw.plugin:FictionalHWPlugin Final configuration: property_priorities = [] feature_priorities = [] namespace_priorities = ["fictional_hw"]
4. Plugin Commands
Demo Install from
pip
Preparation of the environment
Now the install