Skip to content
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

Ensure writable paths are used for wheel installer #9014

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/poetry/installation/wheel_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def install(self, wheel: Path) -> None:
except _WheelFileValidationError as e:
self.invalid_wheels[wheel] = e.issues

scheme_dict = self._env.paths.copy()
scheme_dict = self._env.installer_scheme_dict.copy()
scheme_dict["headers"] = str(
Path(scheme_dict["include"]) / source.distribution
)
Expand Down
41 changes: 40 additions & 1 deletion src/poetry/utils/env/base_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from poetry.utils.env.exceptions import EnvCommandError
from poetry.utils.env.site_packages import SitePackages
from poetry.utils.helpers import get_real_windows_path
from poetry.utils.helpers import is_dir_writable


if TYPE_CHECKING:
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self, path: Path, base: Path | None = None) -> None:
self._marker_env: dict[str, Any] | None = None
self._site_packages: SitePackages | None = None
self._paths: dict[str, str] | None = None
self._installer_scheme_dict: dict[str, str] | None = None
self._supported_tags: list[Tag] | None = None
self._purelib: Path | None = None
self._platlib: Path | None = None
Expand Down Expand Up @@ -242,6 +244,41 @@ def paths(self) -> dict[str, str]:

return self._paths

@property
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may use @cached_property instead so we don't need _installer_scheme_dict.

def installer_scheme_dict(self) -> dict[str, str]:
"""
This property exists to allow cases where system environment paths are not writable and
user site is enabled. This enables us to ensure packages (wheels) are correctly installed
into directories where the current user can write to.
"""
if self._installer_scheme_dict is None:
paths = self.paths.copy()

if not self.is_venv() and paths.get("userbase"):
overrides: dict[str, str] = {}
base_path = os.path.commonpath([paths["include"], paths["purelib"]])

for key in [
"include",
"purelib",
"platlib",
"stdlib",
"platinclude",
"scripts",
"data",
]:
if not is_dir_writable(path=Path(paths[key]), create=True):
overrides[key] = paths[key].replace(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to end up potentially with a mix of system and user directories here? This is not something I have looked into but I would have guessed you'd want all or nothing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure there is a feature request somewhere asking for --user type installs, perhaps it makes sense to do that instead, have this be explicit

would it get messy if eg you provide --user on install but forget it on update, or similar? I dont know.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect that would confuse things more from the user perspective. Especially because we are predominantly servicing virtual environments.

Such a flag would in theory only be used when the user knows they are running in a system environment and does not have write access to the system environment. Hence, I'm not sure how valuable it might be as a cli option. Maybe a config option, I do not know.

If I understood the issues requesting --user, this would simply solve the root issue for them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to end up potentially with a mix of system and user directories here? This is not something I have looked into but I would have guessed you'd want all or nothing

In practice it shouldn't matter. And in reality it is highly unlikely that parts of it is writable and others not.

Python will look everywhere by default.

Copy link
Contributor

@dimbleby dimbleby Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. pip checks all directories when making the same decision I think - https://github.com/pypa/pip/blob/f4a543917eac822a3b06ab0fcc4df7c4e37e387a/src/pip/_internal/commands/install.py#L652. And then once it has made that decision it is fully committed to a user install rather than a site-wide install.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can replicate that I guess.

base_path, paths["userbase"]
)

for k, v in overrides.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paths.update(overrides)?

paths[k] = v

self._installer_scheme_dict = paths

return self._installer_scheme_dict

@property
def supported_tags(self) -> list[Tag]:
if self._supported_tags is None:
Expand Down Expand Up @@ -312,7 +349,9 @@ def run_pip(self, *args: str, **kwargs: Any) -> str:
def run_python_script(self, content: str, **kwargs: Any) -> str:
return self.run(
self._executable,
"-I",
# if isolation is enabled for system env, user site would be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this reintroduces #6627, which was the reason for adding this flag

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch thanks. Let me see if there is a better way.

# incorrectly disabled
*(["-I"] if self.is_venv() else []),
"-W",
"ignore",
"-c",
Expand Down