Skip to content

Commit f0408d6

Browse files
committed
env: default to enabling pip/wheels/setuptools
For project virtual environments, default to enabling pip, setuptools and wheel packages to retain existing stable behaviour to prevent unexpected breakages caused by development environments making assumptions of base package availability in virtual environments. Poetry itself does not require the use of these packages and will execute correctly within environments that do not have these packages. This change retains the ability to manage these packages as direct project dependency as introduced in python-poetry#2826. All poetry internal execution of pip is retaining the use of the wheel embedded within the virtualenv package used by poetry. In cases where a one of these reserved packages are being managed as a project dependency, the will be treated as any other project dependency. Executing `poetry install --remove-untracked` will not remove any of these reserved packages. However, `poetry add pip` and `poetry remove pip` will trigger the update and removal of `pip` respectively. Relates-to: python-poetry#2826 Relates-to: python-poetry#3916
1 parent 3dceee3 commit f0408d6

File tree

8 files changed

+234
-109
lines changed

8 files changed

+234
-109
lines changed

poetry/puzzle/solver.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,31 @@ def __init__(
6363
self._overrides = []
6464
self._remove_untracked = remove_untracked
6565

66+
self._preserved_package_names = None
67+
6668
@property
6769
def provider(self) -> Provider:
6870
return self._provider
6971

72+
@property
73+
def preserved_package_names(self):
74+
if self._preserved_package_names is None:
75+
self._preserved_package_names = {
76+
self._package.name,
77+
*Provider.UNSAFE_PACKAGES,
78+
}
79+
80+
deps = {package.name for package in self._locked.packages}
81+
82+
# preserve pip/setuptools/wheel when not managed by poetry, this is so
83+
# to avoid externally managed virtual environments causing unnecessary
84+
# removals.
85+
for name in {"pip", "wheel", "setuptools"}:
86+
if name not in deps:
87+
self._preserved_package_names.add(name)
88+
89+
return self._preserved_package_names
90+
7091
@contextmanager
7192
def use_environment(self, env: Env) -> None:
7293
with self.provider.use_environment(env):
@@ -190,11 +211,9 @@ def solve(self, use_latest: List[str] = None) -> List["OperationTypes"]:
190211
locked_names = {locked.name for locked in self._locked.packages}
191212

192213
for installed in self._installed.packages:
193-
if installed.name == self._package.name:
194-
continue
195-
if installed.name in Provider.UNSAFE_PACKAGES:
196-
# Never remove pip, setuptools etc.
214+
if installed.name in self.preserved_package_names:
197215
continue
216+
198217
if installed.name not in locked_names:
199218
operations.append(Uninstall(installed))
200219

poetry/utils/env.py

+61-46
Original file line numberDiff line numberDiff line change
@@ -877,13 +877,8 @@ def create_venv(
877877
io.write_line(
878878
"Creating virtualenv <c1>{}</> in {}".format(name, str(venv_path))
879879
)
880-
881-
self.build_venv(
882-
venv,
883-
executable=executable,
884-
flags=self._poetry.config.get("virtualenvs.options"),
885-
)
886880
else:
881+
create_venv = False
887882
if force:
888883
if not env.is_sane():
889884
io.write_line(
@@ -895,14 +890,23 @@ def create_venv(
895890
"Recreating virtualenv <c1>{}</> in {}".format(name, str(venv))
896891
)
897892
self.remove_venv(venv)
898-
self.build_venv(
899-
venv,
900-
executable=executable,
901-
flags=self._poetry.config.get("virtualenvs.options"),
902-
)
893+
create_venv = True
903894
elif io.is_very_verbose():
904895
io.write_line(f"Virtualenv <c1>{name}</> already exists.")
905896

897+
if create_venv:
898+
self.build_venv(
899+
venv,
900+
executable=executable,
901+
flags=self._poetry.config.get("virtualenvs.options"),
902+
# TODO: in a future version switch remove pip/setuptools/wheel
903+
# poetry does not need them these exists today to not break developer
904+
# environment assumptions
905+
with_pip=True,
906+
with_setuptools=True,
907+
with_wheel=True,
908+
)
909+
906910
# venv detection:
907911
# stdlib venv may symlink sys.executable, so we can't use realpath.
908912
# but others can symlink *to* the venv Python,
@@ -927,12 +931,29 @@ def build_venv(
927931
path: Union[Path, str],
928932
executable: Optional[Union[str, Path]] = None,
929933
flags: Dict[str, bool] = None,
930-
with_pip: bool = False,
934+
with_pip: Optional[bool] = None,
931935
with_wheel: Optional[bool] = None,
932936
with_setuptools: Optional[bool] = None,
933937
) -> virtualenv.run.session.Session:
934938
flags = flags or {}
935939

940+
flags["no-pip"] = (
941+
not with_pip if with_pip is not None else flags.pop("no-pip", True)
942+
)
943+
944+
flags["no-setuptools"] = (
945+
not with_setuptools
946+
if with_setuptools is not None
947+
else flags.pop("no-setuptools", True)
948+
)
949+
950+
# we want wheels to be enabled when pip is required and it has not been explicitly disabled
951+
flags["no-wheel"] = (
952+
not with_wheel
953+
if with_wheel is not None
954+
else flags.pop("no-wheel", flags["no-pip"])
955+
)
956+
936957
if isinstance(executable, Path):
937958
executable = executable.resolve().as_posix()
938959

@@ -943,20 +964,6 @@ def build_venv(
943964
executable or sys.executable,
944965
]
945966

946-
if not with_pip:
947-
args.append("--no-pip")
948-
else:
949-
if with_wheel is None:
950-
# we want wheels to be enabled when pip is required and it has
951-
# not been explicitly disabled
952-
with_wheel = True
953-
954-
if with_wheel is None or not with_wheel:
955-
args.append("--no-wheel")
956-
957-
if with_setuptools is None or not with_setuptools:
958-
args.append("--no-setuptools")
959-
960967
for flag, value in flags.items():
961968
if value is True:
962969
args.append(f"--{flag}")
@@ -1039,6 +1046,8 @@ def __init__(self, path: Path, base: Optional[Path] = None) -> None:
10391046
self._platlib = None
10401047
self._script_dirs = None
10411048

1049+
self._embedded_pip_path = None
1050+
10421051
@property
10431052
def path(self) -> Path:
10441053
return self._path
@@ -1074,6 +1083,12 @@ def get_embedded_wheel(self, distribution):
10741083
distribution, "{}.{}".format(self.version_info[0], self.version_info[1])
10751084
).path
10761085

1086+
@property
1087+
def pip_embedded(self) -> str:
1088+
if self._embedded_pip_path is None:
1089+
self._embedded_pip_path = str(self.get_embedded_wheel("pip") / "pip")
1090+
return self._embedded_pip_path
1091+
10771092
@property
10781093
def pip(self) -> str:
10791094
"""
@@ -1082,7 +1097,7 @@ def pip(self) -> str:
10821097
# we do not use as_posix() here due to issues with windows pathlib2 implementation
10831098
path = self._bin("pip")
10841099
if not Path(path).exists():
1085-
return str(self.get_embedded_wheel("pip") / "pip")
1100+
return str(self.pip_embedded)
10861101
return path
10871102

10881103
@property
@@ -1187,7 +1202,7 @@ def get_python_implementation(self) -> str:
11871202
def get_marker_env(self) -> Dict[str, Any]:
11881203
raise NotImplementedError()
11891204

1190-
def get_pip_command(self) -> List[str]:
1205+
def get_pip_command(self, embedded: bool = False) -> List[str]:
11911206
raise NotImplementedError()
11921207

11931208
def get_supported_tags(self) -> List[Tag]:
@@ -1208,16 +1223,20 @@ def is_sane(self) -> bool:
12081223
"""
12091224
return True
12101225

1211-
def run(self, bin: str, *args: str, **kwargs: Any) -> Union[str, int]:
1226+
def get_command_from_bin(self, bin: str) -> List[str]:
12121227
if bin == "pip":
1213-
return self.run_pip(*args, **kwargs)
1228+
# when pip is required we need to ensure that we fallback to
1229+
# embedded pip when pip is not available in the environment
1230+
return self.get_pip_command()
1231+
1232+
return [self._bin(bin)]
12141233

1215-
bin = self._bin(bin)
1216-
cmd = [bin] + list(args)
1234+
def run(self, bin: str, *args: str, **kwargs: Any) -> Union[str, int]:
1235+
cmd = self.get_command_from_bin(bin) + list(args)
12171236
return self._run(cmd, **kwargs)
12181237

12191238
def run_pip(self, *args: str, **kwargs: Any) -> Union[int, str]:
1220-
pip = self.get_pip_command()
1239+
pip = self.get_pip_command(embedded=True)
12211240
cmd = pip + list(args)
12221241
return self._run(cmd, **kwargs)
12231242

@@ -1260,17 +1279,13 @@ def _run(self, cmd: List[str], **kwargs: Any) -> Union[int, str]:
12601279
return decode(output)
12611280

12621281
def execute(self, bin: str, *args: str, **kwargs: Any) -> Optional[int]:
1263-
if bin == "pip":
1264-
return self.run_pip(*args, **kwargs)
1265-
1266-
bin = self._bin(bin)
1282+
command = self.get_command_from_bin(bin) + list(args)
12671283
env = kwargs.pop("env", {k: v for k, v in os.environ.items()})
12681284

12691285
if not self._is_windows:
1270-
args = [bin] + list(args)
1271-
return os.execvpe(bin, args, env=env)
1286+
return os.execvpe(command[0], command, env=env)
12721287
else:
1273-
exe = subprocess.Popen([bin] + list(args), env=env, **kwargs)
1288+
exe = subprocess.Popen([command[0]] + command[1:], env=env, **kwargs)
12741289
exe.communicate()
12751290
return exe.returncode
12761291

@@ -1338,10 +1353,10 @@ def get_version_info(self) -> Tuple[int]:
13381353
def get_python_implementation(self) -> str:
13391354
return platform.python_implementation()
13401355

1341-
def get_pip_command(self) -> List[str]:
1356+
def get_pip_command(self, embedded: bool = False) -> List[str]:
13421357
# If we're not in a venv, assume the interpreter we're running on
13431358
# has a pip and use that
1344-
return [sys.executable, self.pip]
1359+
return [sys.executable, self.pip_embedded if embedded else self.pip]
13451360

13461361
def get_paths(self) -> Dict[str, str]:
13471362
# We can't use sysconfig.get_paths() because
@@ -1445,10 +1460,10 @@ def get_version_info(self) -> Tuple[int]:
14451460
def get_python_implementation(self) -> str:
14461461
return self.marker_env["platform_python_implementation"]
14471462

1448-
def get_pip_command(self) -> List[str]:
1463+
def get_pip_command(self, embedded: bool = False) -> List[str]:
14491464
# We're in a virtualenv that is known to be sane,
14501465
# so assume that we have a functional pip
1451-
return [self._bin("python"), self.pip]
1466+
return [self._bin("python"), self.pip_embedded if embedded else self.pip]
14521467

14531468
def get_supported_tags(self) -> List[Tag]:
14541469
file_path = Path(packaging.tags.__file__)
@@ -1560,8 +1575,8 @@ def __init__(
15601575
self._execute = execute
15611576
self.executed = []
15621577

1563-
def get_pip_command(self) -> List[str]:
1564-
return [self._bin("python"), self.pip]
1578+
def get_pip_command(self, embedded: bool = False) -> List[str]:
1579+
return [self._bin("python"), self.pip_embedded if embedded else self.pip]
15651580

15661581
def _run(self, cmd: List[str], **kwargs: Any) -> int:
15671582
self.executed.append(cmd)

poetry/utils/pip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def pip_install(
5353
executable=environment.python, with_pip=True, with_setuptools=True
5454
) as env:
5555
return environment.run(
56-
env._bin("pip"),
56+
*env.get_pip_command(),
5757
*args,
5858
env={**os.environ, "PYTHONPATH": str(env.purelib)},
5959
)

tests/console/commands/env/test_use.py

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
5555
venv_py37,
5656
executable="python3.7",
5757
flags={"always-copy": False, "system-site-packages": False},
58+
with_pip=True,
59+
with_setuptools=True,
60+
with_wheel=True,
5861
)
5962

6063
envs_file = TOMLFile(venv_cache / "envs.toml")

tests/inspection/test_info.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def test_info_setup_missing_mandatory_should_trigger_pep517(
217217
except PackageInfoError:
218218
assert spy.call_count == 3
219219
else:
220-
assert spy.call_count == 1
220+
assert spy.call_count == 2
221221

222222

223223
def test_info_prefer_poetry_config_over_egg_info():

0 commit comments

Comments
 (0)