Skip to content

Commit

Permalink
env: make pip/setuptools/wheel configurable
Browse files Browse the repository at this point in the history
This change introduces the following configuration options:

- `virtualenvs.options.no-pip` (Default: `false`)
- `virtualenvs.options.no-wheel` (Default: `false`)
- `virtualenvs.options.no-setuptools` (Default: `false`)

With these options, we allow, the user to control the inclusion of
`pip`, `wheel` and `setuptools` packages in new virtual environments.
The defaults used retain existing behaviour as of `1.1.5`.

In addition the following now holds true.

- projects can define `pip`, `wheel` and `setuptools` as dependencies
- if any of these packages are in the project's dependency tree, poetry
  will manage their versions
- all poetry internal commands will use pip embedded in virtualenv
  irrespective of pip beig available in the environment
- if `poetry run pip` is executed, pip installed in the virtualenv will
  be prioritsed
- `poetry install --remove-untracked` will ignore these packages if
  they are not present in the project's dependency tree

Relates-to: #2826
Relates-to: #3916
  • Loading branch information
abn committed Apr 11, 2021
1 parent ecec2fc commit 2c5b6aa
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 35 deletions.
11 changes: 10 additions & 1 deletion poetry/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ class Config:
"create": True,
"in-project": None,
"path": os.path.join("{cache-dir}", "virtualenvs"),
"options": {"always-copy": False, "system-site-packages": False},
"options": {
"always-copy": False,
"system-site-packages": False,
"no-pip": False,
"no-setuptools": False,
"no-wheel": False,
},
},
"experimental": {"new-installer": True},
"installer": {"parallel": True},
Expand Down Expand Up @@ -139,6 +145,9 @@ def _get_normalizer(self, name: str) -> Callable:
"virtualenvs.in-project",
"virtualenvs.options.always-copy",
"virtualenvs.options.system-site-packages",
"virtualenvs.options.no-pip",
"virtualenvs.options.no-wheel",
"virtualenvs.options.no-setuptools",
"installer.parallel",
}:
return boolean_normalizer
Expand Down
15 changes: 15 additions & 0 deletions poetry/console/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ def unique_config_values(self) -> Dict[str, Tuple[Any, Any, Any]]:
boolean_normalizer,
False,
),
"virtualenvs.options.no-pip": (
boolean_validator,
boolean_normalizer,
False,
),
"virtualenvs.options.no-setuptools": (
boolean_validator,
boolean_normalizer,
False,
),
"virtualenvs.options.no-wheel": (
boolean_validator,
boolean_normalizer,
False,
),
"virtualenvs.path": (
str,
lambda val: str(Path(val)),
Expand Down
13 changes: 11 additions & 2 deletions poetry/console/commands/debug/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ def handle(self) -> Optional[int]:

pool = self.poetry.pool

solver = Solver(package, pool, Repository(), Repository(), self._io)
solver = Solver(
package,
pool,
Repository(),
Repository(),
self._io,
config=self.poetry.config,
)

ops = solver.solve()

Expand Down Expand Up @@ -121,7 +128,9 @@ def handle(self) -> Optional[int]:

pool.add_repository(locked_repository)

solver = Solver(package, pool, Repository(), Repository(), NullIO())
solver = Solver(
package, pool, Repository(), Repository(), NullIO(), poetry=self.poetry
)
with solver.use_environment(env):
ops = solver.solve()

Expand Down
1 change: 1 addition & 0 deletions poetry/console/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def handle(self) -> Optional[int]:
installed=Repository(),
locked=locked_repo,
io=NullIO(),
config=self.poetry.config,
)
solver.provider.load_deferred(False)
with solver.use_environment(self.env):
Expand Down
4 changes: 4 additions & 0 deletions poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def __init__(
installed = self._get_installed()

self._installed_repository = installed
self._poetry_config = config

@property
def executor(self) -> Executor:
Expand Down Expand Up @@ -209,6 +210,7 @@ def _do_refresh(self) -> int:
locked_repository,
locked_repository,
self._io,
config=self._poetry_config,
)

ops = solver.solve(use_latest=[])
Expand Down Expand Up @@ -247,6 +249,7 @@ def _do_install(self, local_repo: Repository) -> int:
locked_repository,
self._io,
remove_untracked=self._remove_untracked,
config=self._poetry_config,
)

ops = solver.solve(use_latest=self._whitelist)
Expand Down Expand Up @@ -316,6 +319,7 @@ def _do_install(self, local_repo: Repository) -> int:
locked_repository,
NullIO(),
remove_untracked=self._remove_untracked,
config=self._poetry_config,
)
# Everything is resolved at this point, so we no longer need
# to load deferred dependencies (i.e. VCS, URL and path dependencies)
Expand Down
28 changes: 24 additions & 4 deletions poetry/puzzle/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@


if TYPE_CHECKING:
from poetry.config.config import Config
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.directory_dependency import DirectoryDependency
from poetry.core.packages.file_dependency import FileDependency
Expand All @@ -49,6 +50,7 @@ def __init__(
io: IO,
remove_untracked: bool = False,
provider: Optional[Provider] = None,
config: Optional["Config"] = None,
):
self._package = package
self._pool = pool
Expand All @@ -63,10 +65,30 @@ def __init__(
self._overrides = []
self._remove_untracked = remove_untracked

self._poetry_config = config
self._preserved_package_names = None

@property
def provider(self) -> Provider:
return self._provider

@property
def preserved_package_names(self):
if self._preserved_package_names is None:
self._preserved_package_names = {
self._package.name,
*Provider.UNSAFE_PACKAGES,
}
if self._poetry_config is not None:
deps = {package.name for package in self._locked.packages}

for name in {"pip", "wheel", "setuptools"}:
if not self._poetry_config.get(f"virtualenvs.options.no-{name}"):
if name not in deps:
self._preserved_package_names.add(name)

return self._preserved_package_names

@contextmanager
def use_environment(self, env: Env) -> None:
with self.provider.use_environment(env):
Expand Down Expand Up @@ -190,11 +212,9 @@ def solve(self, use_latest: List[str] = None) -> List["OperationTypes"]:
locked_names = {locked.name for locked in self._locked.packages}

for installed in self._installed.packages:
if installed.name == self._package.name:
continue
if installed.name in Provider.UNSAFE_PACKAGES:
# Never remove pip, setuptools etc.
if installed.name in self.preserved_package_names:
continue

if installed.name not in locked_names:
operations.append(Uninstall(installed))

Expand Down
39 changes: 28 additions & 11 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,12 @@ def build_venv(
) -> virtualenv.run.session.Session:
flags = flags or {}

if flags:
# if flags are present and explicit parameters are false, use flag values
with_pip = with_pip or not flags.pop("no-pip", True)
with_setuptools = with_setuptools or not flags.pop("no-setuptools", True)
with_wheel = with_wheel or not flags.pop("no-wheel", True)

if isinstance(executable, Path):
executable = executable.resolve().as_posix()

Expand Down Expand Up @@ -1039,6 +1045,8 @@ def __init__(self, path: Path, base: Optional[Path] = None) -> None:
self._platlib = None
self._script_dirs = None

self._embedded_pip_path = None

@property
def path(self) -> Path:
return self._path
Expand Down Expand Up @@ -1074,6 +1082,12 @@ def get_embedded_wheel(self, distribution):
distribution, "{}.{}".format(self.version_info[0], self.version_info[1])
).path

@property
def pip_embedded(self) -> str:
if self._embedded_pip_path is None:
self._embedded_pip_path = str(self.get_embedded_wheel("pip") / "pip")
return self._embedded_pip_path

@property
def pip(self) -> str:
"""
Expand All @@ -1082,7 +1096,7 @@ def pip(self) -> str:
# we do not use as_posix() here due to issues with windows pathlib2 implementation
path = self._bin("pip")
if not Path(path).exists():
return str(self.get_embedded_wheel("pip") / "pip")
return str(self.pip_embedded)
return path

@property
Expand Down Expand Up @@ -1187,7 +1201,7 @@ def get_python_implementation(self) -> str:
def get_marker_env(self) -> Dict[str, Any]:
raise NotImplementedError()

def get_pip_command(self) -> List[str]:
def get_pip_command(self, embedded: bool = False) -> List[str]:
raise NotImplementedError()

def get_supported_tags(self) -> List[Tag]:
Expand All @@ -1210,14 +1224,17 @@ def is_sane(self) -> bool:

def run(self, bin: str, *args: str, **kwargs: Any) -> Union[str, int]:
if bin == "pip":
return self.run_pip(*args, **kwargs)
# when pip is required we need to ensure that we fallback to
# embedded pip when pip is not available in the environment
bin = self.get_pip_command()
else:
bin = self._bin(bin)

bin = self._bin(bin)
cmd = [bin] + list(args)
return self._run(cmd, **kwargs)

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

Expand Down Expand Up @@ -1337,10 +1354,10 @@ def get_version_info(self) -> Tuple[int]:
def get_python_implementation(self) -> str:
return platform.python_implementation()

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

def get_paths(self) -> Dict[str, str]:
# We can't use sysconfig.get_paths() because
Expand Down Expand Up @@ -1444,10 +1461,10 @@ def get_version_info(self) -> Tuple[int]:
def get_python_implementation(self) -> str:
return self.marker_env["platform_python_implementation"]

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

def get_supported_tags(self) -> List[Tag]:
file_path = Path(packaging.tags.__file__)
Expand Down Expand Up @@ -1559,8 +1576,8 @@ def __init__(
self._execute = execute
self.executed = []

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

def _run(self, cmd: List[str], **kwargs: Any) -> int:
self.executed.append(cmd)
Expand Down
8 changes: 7 additions & 1 deletion tests/console/commands/env/test_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
mock_build_env.assert_called_with(
venv_py37,
executable="python3.7",
flags={"always-copy": False, "system-site-packages": False},
flags={
"always-copy": False,
"system-site-packages": False,
"no-pip": False,
"no-wheel": False,
"no-setuptools": False,
},
)

envs_file = TOMLFile(venv_cache / "envs.toml")
Expand Down
9 changes: 9 additions & 0 deletions tests/console/commands/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def test_list_displays_default_value_if_not_set(tester, config, config_cache_dir
virtualenvs.create = true
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.no-setuptools = false
virtualenvs.options.no-wheel = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = {path} # {virtualenvs}
""".format(
Expand All @@ -54,6 +57,9 @@ def test_list_displays_set_get_setting(tester, config, config_cache_dir):
virtualenvs.create = false
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.no-setuptools = false
virtualenvs.options.no-wheel = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = {path} # {virtualenvs}
""".format(
Expand Down Expand Up @@ -98,6 +104,9 @@ def test_list_displays_set_get_local_setting(tester, config, config_cache_dir):
virtualenvs.create = false
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.no-setuptools = false
virtualenvs.options.no-wheel = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = {path} # {virtualenvs}
""".format(
Expand Down
2 changes: 1 addition & 1 deletion tests/inspection/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def test_info_setup_missing_mandatory_should_trigger_pep517(
except PackageInfoError:
assert spy.call_count == 3
else:
assert spy.call_count == 1
assert spy.call_count == 2


def test_info_prefer_poetry_config_over_egg_info():
Expand Down
23 changes: 18 additions & 5 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,16 @@ def test_run_install_no_dev_and_dev_only(installer, locker, repo, package, insta
assert 1 == installer.executor.removals_count


def test_run_install_remove_untracked(installer, locker, repo, package, installed):
@pytest.mark.parametrize("bare_venv", [True, False])
def test_run_install_remove_untracked(
bare_venv, installer, locker, repo, package, installed
):
installer._poetry_config._config["virtualenvs"]["options"]["no-pip"] = bare_venv
installer._poetry_config._config["virtualenvs"]["options"]["no-wheel"] = bare_venv
installer._poetry_config._config["virtualenvs"]["options"][
"no-setuptools"
] = bare_venv

locker.locked(True)
locker.mock_lock_data(
{
Expand Down Expand Up @@ -416,10 +425,14 @@ def test_run_install_remove_untracked(installer, locker, repo, package, installe

assert 0 == installer.executor.installations_count
assert 0 == installer.executor.updates_count
assert 4 == installer.executor.removals_count
assert {"b", "c", "pip", "setuptools"} == set(
r.name for r in installer.executor.removals
)

expected_removals = {"b", "c"}
if bare_venv:
expected_removals.add("pip")
expected_removals.add("setuptools")

assert len(expected_removals) == installer.executor.removals_count
assert expected_removals == set(r.name for r in installer.executor.removals)


def test_run_whitelist_add(installer, locker, repo, package):
Expand Down
Loading

0 comments on commit 2c5b6aa

Please sign in to comment.