diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py
index 9895745b48b..ad5b574ab37 100644
--- a/src/poetry/console/commands/install.py
+++ b/src/poetry/console/commands/install.py
@@ -93,6 +93,19 @@ def activated_groups(self) -> set[str]:
def _alternative_sync_command(self) -> str:
return "poetry sync"
+ @property
+ def _with_synchronization(self) -> bool:
+ with_synchronization = self.option("sync")
+ if with_synchronization:
+ self.line_error(
+ "The `--sync>` option is"
+ " deprecated and slated for removal in the next minor release"
+ " after June 2025, use the"
+ f" `{self._alternative_sync_command}>`"
+ " command instead."
+ )
+ return bool(with_synchronization)
+
def handle(self) -> int:
from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError
@@ -150,20 +163,10 @@ def handle(self) -> int:
self.installer.extras(extras)
- with_synchronization = self.option("sync")
- if with_synchronization:
- self.line_error(
- "The `--sync>` option is"
- " deprecated and slated for removal in the next minor release"
- " after June 2025, use the"
- f" `{self._alternative_sync_command}>`"
- " command instead."
- )
-
self.installer.only_groups(self.activated_groups)
self.installer.skip_directory(self.option("no-directory"))
self.installer.dry_run(self.option("dry-run"))
- self.installer.requires_synchronization(with_synchronization)
+ self.installer.requires_synchronization(self._with_synchronization)
self.installer.executor.enable_bytecode_compilation(self.option("compile"))
self.installer.verbose(self.io.is_verbose())
diff --git a/src/poetry/console/commands/self/sync.py b/src/poetry/console/commands/self/sync.py
index 0af576c7b80..05fc0c25a3b 100644
--- a/src/poetry/console/commands/self/sync.py
+++ b/src/poetry/console/commands/self/sync.py
@@ -29,3 +29,7 @@ class SelfSyncCommand(SelfInstallCommand):
You can add more packages using the self add command and remove them using \
the self remove command.
"""
+
+ @property
+ def _with_synchronization(self) -> bool:
+ return True
diff --git a/src/poetry/console/commands/sync.py b/src/poetry/console/commands/sync.py
index 29d6c1be873..4723483f725 100644
--- a/src/poetry/console/commands/sync.py
+++ b/src/poetry/console/commands/sync.py
@@ -34,3 +34,7 @@ class SyncCommand(InstallCommand):
If you want to use Poetry only for dependency management but not for packaging,
you can set the "package-mode" to false in your pyproject.toml file.
"""
+
+ @property
+ def _with_synchronization(self) -> bool:
+ return True
diff --git a/src/poetry/utils/env/__init__.py b/src/poetry/utils/env/__init__.py
index c3834f4fbaf..9ab3b37d021 100644
--- a/src/poetry/utils/env/__init__.py
+++ b/src/poetry/utils/env/__init__.py
@@ -21,7 +21,6 @@
from poetry.utils.env.script_strings import GET_ENV_PATH_ONELINER
from poetry.utils.env.script_strings import GET_ENVIRONMENT_INFO
from poetry.utils.env.script_strings import GET_PATHS
-from poetry.utils.env.script_strings import GET_PATHS_FOR_GENERIC_ENVS
from poetry.utils.env.script_strings import GET_PYTHON_VERSION_ONELINER
from poetry.utils.env.script_strings import GET_SYS_PATH
from poetry.utils.env.site_packages import SitePackages
@@ -97,7 +96,6 @@ def build_environment(
"GET_SYS_PATH",
"GET_ENV_PATH_ONELINER",
"GET_PYTHON_VERSION_ONELINER",
- "GET_PATHS_FOR_GENERIC_ENVS",
"EnvError",
"EnvCommandError",
"IncorrectEnvError",
diff --git a/src/poetry/utils/env/base_env.py b/src/poetry/utils/env/base_env.py
index f57fb9cc2ae..3c0da4ffa20 100644
--- a/src/poetry/utils/env/base_env.py
+++ b/src/poetry/utils/env/base_env.py
@@ -175,13 +175,10 @@ def os(self) -> str:
@property
def site_packages(self) -> SitePackages:
if self._site_packages is None:
- # we disable write checks if no user site exist
- fallbacks = [self.usersite] if self.usersite else []
self._site_packages = SitePackages(
self.purelib,
self.platlib,
- fallbacks,
- skip_write_checks=not fallbacks,
+ self.fallbacks,
)
return self._site_packages
@@ -214,8 +211,14 @@ def platlib(self) -> Path:
return self._platlib
+ @cached_property
+ def fallbacks(self) -> list[Path]:
+ paths = [Path(path) for path in self.paths.get("fallbacks", [])]
+ paths += [self.usersite] if self.usersite else []
+ return paths
+
def _get_lib_dirs(self) -> list[Path]:
- return [self.purelib, self.platlib]
+ return [self.purelib, self.platlib, *self.fallbacks]
def is_path_relative_to_lib(self, path: Path) -> bool:
for lib_path in self._get_lib_dirs():
diff --git a/src/poetry/utils/env/generic_env.py b/src/poetry/utils/env/generic_env.py
index 276d1b1eb18..28ab156d5a1 100644
--- a/src/poetry/utils/env/generic_env.py
+++ b/src/poetry/utils/env/generic_env.py
@@ -8,7 +8,7 @@
from typing import TYPE_CHECKING
from typing import Any
-from poetry.utils.env.script_strings import GET_PATHS_FOR_GENERIC_ENVS
+from poetry.utils.env.script_strings import GET_PATHS
from poetry.utils.env.virtual_env import VirtualEnv
@@ -78,7 +78,7 @@ def find_executables(self) -> None:
self._pip_executable = pip_executable
def get_paths(self) -> dict[str, str]:
- output = self.run_python_script(GET_PATHS_FOR_GENERIC_ENVS)
+ output = self.run_python_script(GET_PATHS)
paths: dict[str, str] = json.loads(output)
return paths
diff --git a/src/poetry/utils/env/script_strings.py b/src/poetry/utils/env/script_strings.py
index dc33e00ba05..a2935b566e9 100644
--- a/src/poetry/utils/env/script_strings.py
+++ b/src/poetry/utils/env/script_strings.py
@@ -92,18 +92,16 @@ def _version_nodot(version):
GET_PATHS = """\
import json
-import sysconfig
-
-print(json.dumps(sysconfig.get_paths()))
-"""
-
-GET_PATHS_FOR_GENERIC_ENVS = """\
-import json
import site
import sysconfig
paths = sysconfig.get_paths().copy()
+paths["fallbacks"] = [
+ p for p in site.getsitepackages()
+ if p and p not in {paths.get("purelib"), paths.get("platlib")}
+]
+
if site.check_enableusersite():
paths["usersite"] = site.getusersitepackages()
paths["userbase"] = site.getuserbase()
diff --git a/src/poetry/utils/env/site_packages.py b/src/poetry/utils/env/site_packages.py
index a082419031c..564aa003aa0 100644
--- a/src/poetry/utils/env/site_packages.py
+++ b/src/poetry/utils/env/site_packages.py
@@ -25,7 +25,6 @@ def __init__(
purelib: Path,
platlib: Path | None = None,
fallbacks: list[Path] | None = None,
- skip_write_checks: bool = False,
) -> None:
self._purelib = purelib
self._platlib = platlib or purelib
@@ -40,7 +39,7 @@ def __init__(
if path not in self._candidates:
self._candidates.append(path)
- self._writable_candidates = None if not skip_write_checks else self._candidates
+ self._writable_candidates: list[Path] | None = None
@property
def path(self) -> Path:
diff --git a/tests/console/commands/self/test_install.py b/tests/console/commands/self/test_install.py
index a5e15e41ed6..0cd9663a4f9 100644
--- a/tests/console/commands/self/test_install.py
+++ b/tests/console/commands/self/test_install.py
@@ -56,14 +56,9 @@ def test_self_install(
tester.execute()
- expected_output = """\
-Updating dependencies
-Resolving dependencies...
-
-Writing lock file
-"""
-
- assert tester.io.fetch_output() == expected_output
+ output = tester.io.fetch_output()
+ assert output.startswith("Updating dependencies")
+ assert output.endswith("Writing lock file\n")
assert tester.io.fetch_error() == ""
diff --git a/tests/console/commands/self/test_sync.py b/tests/console/commands/self/test_sync.py
index 6fdc27fc764..d1c351c74d3 100644
--- a/tests/console/commands/self/test_sync.py
+++ b/tests/console/commands/self/test_sync.py
@@ -6,6 +6,8 @@
from cleo.exceptions import CleoNoSuchOptionError
+from poetry.console.commands.self.sync import SelfSyncCommand
+
# import all tests from the self install command
# and run them for sync by overriding the command fixture
from tests.console.commands.self.test_install import * # noqa: F403
@@ -13,6 +15,7 @@
if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester
+ from pytest_mock import MockerFixture
@pytest.fixture # type: ignore[no-redef]
@@ -28,3 +31,15 @@ def test_sync_deprecation() -> None:
def test_sync_option_not_available(tester: CommandTester) -> None:
with pytest.raises(CleoNoSuchOptionError):
tester.execute("--sync")
+
+
+def test_synced_installer(tester: CommandTester, mocker: MockerFixture) -> None:
+ assert isinstance(tester.command, SelfSyncCommand)
+ mock = mocker.patch(
+ "poetry.console.commands.install.InstallCommand.installer",
+ new_callable=mocker.PropertyMock,
+ )
+
+ tester.execute()
+
+ mock.return_value.requires_synchronization.assert_called_with(True)
diff --git a/tests/console/commands/test_sync.py b/tests/console/commands/test_sync.py
index af75afd86f3..89d7e8c7760 100644
--- a/tests/console/commands/test_sync.py
+++ b/tests/console/commands/test_sync.py
@@ -6,6 +6,8 @@
from cleo.exceptions import CleoNoSuchOptionError
+from poetry.console.commands.sync import SyncCommand
+
# import all tests from the install command
# and run them for sync by overriding the command fixture
from tests.console.commands.test_install import * # noqa: F403
@@ -13,6 +15,7 @@
if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester
+ from pytest_mock import MockerFixture
@pytest.fixture # type: ignore[no-redef]
@@ -28,3 +31,15 @@ def test_sync_option_is_passed_to_the_installer() -> None:
def test_sync_option_not_available(tester: CommandTester) -> None:
with pytest.raises(CleoNoSuchOptionError):
tester.execute("--sync")
+
+
+def test_synced_installer(tester: CommandTester, mocker: MockerFixture) -> None:
+ assert isinstance(tester.command, SyncCommand)
+ mock = mocker.patch(
+ "poetry.console.commands.install.InstallCommand.installer",
+ new_callable=mocker.PropertyMock,
+ )
+
+ tester.execute()
+
+ mock.return_value.requires_synchronization.assert_called_with(True)
diff --git a/tests/utils/env/test_env.py b/tests/utils/env/test_env.py
index b3f63a7339d..97eda471416 100644
--- a/tests/utils/env/test_env.py
+++ b/tests/utils/env/test_env.py
@@ -309,7 +309,7 @@ def test_env_system_packages_are_relative_to_lib(
# These are the virtual environments' base env packages,
# in this case the system site packages.
- for dist in metadata.distributions(path=[str(env.parent_env.site_packages.path)]):
+ for dist in env.parent_env.site_packages.distributions():
assert (
env.is_path_relative_to_lib(
Path(str(dist._path)) # type: ignore[attr-defined]