Skip to content

Commit 1796ad2

Browse files
Allow Poetry to select best Python version (#103)
Co-authored-by: Randy Döring <[email protected]>
1 parent 8d8c7da commit 1796ad2

File tree

3 files changed

+82
-104
lines changed

3 files changed

+82
-104
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ will bundle the project in the `/path/to/environment` directory by creating the
4646
installing the dependencies and the current project inside it. If the directory does not exist,
4747
it will be created automatically.
4848

49-
By default, the command uses the current Python executable to build the virtual environment.
49+
By default, the command uses the same Python executable that Poetry would use
50+
when running `poetry install` to build the virtual environment.
5051
If you want to use a different one, you can specify it with the `--python/-p` option:
5152

5253
```bash

src/poetry_plugin_bundle/bundlers/venv_bundler.py

Lines changed: 47 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
from __future__ import annotations
22

3-
import subprocess
4-
import sys
5-
6-
from pathlib import Path
7-
from subprocess import CalledProcessError
83
from typing import TYPE_CHECKING
94

105
from poetry_plugin_bundle.bundlers.bundler import Bundler
116

127

138
if TYPE_CHECKING:
9+
from pathlib import Path
10+
1411
from cleo.io.io import IO
1512
from cleo.io.outputs.section_output import SectionOutput
16-
from poetry.core.constraints.version import Version
1713
from poetry.poetry import Poetry
1814
from poetry.repositories.lockfile_repository import LockfileRepository
1915

@@ -58,71 +54,74 @@ def bundle(self, poetry: Poetry, io: IO) -> bool:
5854
from tempfile import TemporaryDirectory
5955

6056
from cleo.io.null_io import NullIO
61-
from poetry.core.constraints.version import Version
6257
from poetry.core.masonry.builders.wheel import WheelBuilder
6358
from poetry.core.masonry.utils.module import ModuleOrPackageNotFound
6459
from poetry.core.packages.package import Package
6560
from poetry.installation.installer import Installer
6661
from poetry.installation.operations.install import Install
6762
from poetry.packages.locker import Locker
63+
from poetry.utils.env import Env
6864
from poetry.utils.env import EnvManager
69-
from poetry.utils.env import SystemEnv
70-
from poetry.utils.env import VirtualEnv
65+
from poetry.utils.env.exceptions import InvalidCurrentPythonVersionError
66+
67+
class CustomEnvManager(EnvManager):
68+
"""
69+
This class is used as an adapter for allowing us to use
70+
Poetry's EnvManager.create_venv but with a custom path.
71+
It works by hijacking the "in_project_venv" concept so that
72+
we can get that behavior, but with a custom path.
73+
"""
74+
75+
@property
76+
def in_project_venv(self) -> Path:
77+
return self._path
78+
79+
def use_in_project_venv(self) -> bool:
80+
return True
81+
82+
def create_venv_at_path(
83+
self, path: Path, executable: Path | None, force: bool
84+
) -> Env:
85+
self._path = path
86+
return self.create_venv(name=None, executable=executable, force=force)
7187

7288
warnings = []
7389

74-
manager = EnvManager(poetry)
75-
if self._executable:
76-
executable, python_version = self._get_executable_info(self._executable)
77-
else:
78-
executable = None
79-
version_info = SystemEnv(Path(sys.prefix)).get_version_info()
80-
python_version = Version.parse(".".join(str(v) for v in version_info[:3]))
90+
manager = CustomEnvManager(poetry)
91+
executable = Path(self._executable) if self._executable else None
8192

8293
message = self._get_message(poetry, self._path)
8394
if io.is_decorated() and not io.is_debug():
8495
io = io.section() # type: ignore[assignment]
8596

8697
io.write_line(message)
8798

88-
if self._path.exists():
89-
env = VirtualEnv(self._path)
90-
env_python_version = Version.parse(
91-
".".join(str(v) for v in env.version_info[:3])
99+
if executable:
100+
self._write(
101+
io,
102+
f"{message}: <info>Creating a virtual environment using Python"
103+
f" <b>{executable}</b></info>",
92104
)
93-
94-
if (
95-
not env.is_sane()
96-
or env_python_version != python_version
97-
or self._remove
98-
):
99-
self._write(
100-
io, f"{message}: <info>Removing existing virtual environment</info>"
101-
)
102-
103-
manager.remove_venv(self._path)
104-
105-
self._write(
106-
io,
107-
f"{message}: <info>Creating a virtual environment using Python"
108-
f" <b>{python_version}</b></info>",
109-
)
110-
111-
manager.build_venv(self._path, executable=executable)
112-
else:
113-
self._write(
114-
io, f"{message}: <info>Using existing virtual environment</info>"
115-
)
116105
else:
117106
self._write(
118107
io,
119-
f"{message}: <info>Creating a virtual environment using Python"
120-
f" <b>{python_version}</b></info>",
108+
f"{message}: <info>Creating a virtual environment"
109+
" using Poetry-determined Python",
121110
)
122111

123-
manager.build_venv(self._path, executable=executable)
124-
125-
env = VirtualEnv(self._path)
112+
try:
113+
env = manager.create_venv_at_path(
114+
self._path, executable=executable, force=self._remove
115+
)
116+
except InvalidCurrentPythonVersionError:
117+
self._write(
118+
io,
119+
f"{message}: <info>Replacing existing virtual environment"
120+
" due to incompatible Python version</info>",
121+
)
122+
env = manager.create_venv_at_path(
123+
self._path, executable=executable, force=True
124+
)
126125

127126
self._write(io, f"{message}: <info>Installing dependencies</info>")
128127

@@ -221,32 +220,3 @@ def _write(self, io: IO | SectionOutput, message: str) -> None:
221220
return
222221

223222
io.overwrite(message)
224-
225-
def _get_executable_info(self, executable: str) -> tuple[Path, Version]:
226-
from poetry.core.constraints.version import Version
227-
228-
try:
229-
python_version = Version.parse(executable)
230-
executable = f"python{python_version.major}"
231-
if python_version.precision > 1:
232-
executable += f".{python_version.minor}"
233-
except ValueError:
234-
# Executable in PATH or full executable path
235-
pass
236-
237-
try:
238-
python_version_str = subprocess.check_output(
239-
[
240-
executable,
241-
"-c",
242-
"import sys; print('.'.join([str(s) for s in sys.version_info[:3]]))",
243-
]
244-
).decode()
245-
except CalledProcessError as e:
246-
from poetry.utils.env import EnvCommandError
247-
248-
raise EnvCommandError(e) from None
249-
250-
python_version = Version.parse(python_version_str.strip())
251-
252-
return Path(executable), python_version

tests/bundlers/test_venv_bundler.py

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from poetry.puzzle.exceptions import SolverProblemError
1717
from poetry.repositories.repository import Repository
1818
from poetry.repositories.repository_pool import RepositoryPool
19+
from poetry.utils.env import MockEnv
1920

2021
from poetry_plugin_bundle.bundlers.venv_bundler import VenvBundler
2122

@@ -53,6 +54,12 @@ def poetry(config: Config) -> Poetry:
5354
return poetry
5455

5556

57+
def _create_venv_marker_file(tempdir: str | Path) -> Path:
58+
marker_file = Path(tempdir) / "existing-venv-marker.txt"
59+
marker_file.write_text("This file should get deleted as part of venv recreation.")
60+
return marker_file
61+
62+
5663
def test_bundler_should_build_a_new_venv_with_existing_python(
5764
io: BufferedIO, tmpdir: str, poetry: Poetry, mocker: MockerFixture
5865
) -> None:
@@ -64,10 +71,9 @@ def test_bundler_should_build_a_new_venv_with_existing_python(
6471

6572
assert bundler.bundle(poetry, io)
6673

67-
python_version = ".".join(str(v) for v in sys.version_info[:3])
6874
expected = f"""\
6975
• Bundling simple-project (1.2.3) into {tmpdir}
70-
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {python_version}
76+
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python
7177
• Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies
7278
• Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3)
7379
• Bundled simple-project (1.2.3) into {tmpdir}
@@ -87,10 +93,9 @@ def test_bundler_should_build_a_new_venv_with_given_executable(
8793

8894
assert bundler.bundle(poetry, io)
8995

90-
python_version = ".".join(str(v) for v in sys.version_info[:3])
9196
expected = f"""\
9297
• Bundling simple-project (1.2.3) into {tmpdir}
93-
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {python_version}
98+
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {sys.executable}
9499
• Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies
95100
• Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3)
96101
• Bundled simple-project (1.2.3) into {tmpdir}
@@ -103,16 +108,22 @@ def test_bundler_should_build_a_new_venv_if_existing_venv_is_incompatible(
103108
) -> None:
104109
mocker.patch("poetry.installation.executor.Executor._execute_operation")
105110

111+
mock_env = MockEnv(path=Path(tmpdir), is_venv=True, version_info=(1, 2, 3))
112+
mocker.patch("poetry.utils.env.EnvManager.get", return_value=mock_env)
113+
106114
bundler = VenvBundler()
107115
bundler.set_path(Path(tmpdir))
108116

117+
marker_file = _create_venv_marker_file(tmpdir)
118+
119+
assert marker_file.exists()
109120
assert bundler.bundle(poetry, io)
121+
assert not marker_file.exists()
110122

111-
python_version = ".".join(str(v) for v in sys.version_info[:3])
112123
expected = f"""\
113124
• Bundling simple-project (1.2.3) into {tmpdir}
114-
• Bundling simple-project (1.2.3) into {tmpdir}: Removing existing virtual environment
115-
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {python_version}
125+
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python
126+
• Bundling simple-project (1.2.3) into {tmpdir}: Replacing existing virtual environment due to incompatible Python version
116127
• Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies
117128
• Bundling simple-project (1.2.3) into {tmpdir}: Installing simple-project (1.2.3)
118129
• Bundled simple-project (1.2.3) into {tmpdir}
@@ -128,12 +139,16 @@ def test_bundler_should_use_an_existing_venv_if_compatible(
128139
bundler = VenvBundler()
129140
bundler.set_path(tmp_venv.path)
130141

142+
marker_file = _create_venv_marker_file(tmp_venv.path)
143+
144+
assert marker_file.exists()
131145
assert bundler.bundle(poetry, io)
146+
assert marker_file.exists()
132147

133148
path = str(tmp_venv.path)
134149
expected = f"""\
135150
• Bundling simple-project (1.2.3) into {path}
136-
• Bundling simple-project (1.2.3) into {path}: Using existing virtual environment
151+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
137152
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
138153
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
139154
• Bundled simple-project (1.2.3) into {path}
@@ -150,14 +165,16 @@ def test_bundler_should_remove_an_existing_venv_if_forced(
150165
bundler.set_path(tmp_venv.path)
151166
bundler.set_remove(True)
152167

168+
marker_file = _create_venv_marker_file(tmp_venv.path)
169+
170+
assert marker_file.exists()
153171
assert bundler.bundle(poetry, io)
172+
assert not marker_file.exists()
154173

155174
path = str(tmp_venv.path)
156-
python_version = ".".join(str(v) for v in sys.version_info[:3])
157175
expected = f"""\
158176
• Bundling simple-project (1.2.3) into {path}
159-
• Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment
160-
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version}
177+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
161178
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
162179
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
163180
• Bundled simple-project (1.2.3) into {path}
@@ -178,11 +195,9 @@ def test_bundler_should_fail_when_installation_fails(
178195

179196
assert not bundler.bundle(poetry, io)
180197

181-
python_version = ".".join(str(v) for v in sys.version_info[:3])
182198
expected = f"""\
183199
• Bundling simple-project (1.2.3) into {tmpdir}
184-
• Bundling simple-project (1.2.3) into {tmpdir}: Removing existing virtual environment
185-
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Python {python_version}
200+
• Bundling simple-project (1.2.3) into {tmpdir}: Creating a virtual environment using Poetry-determined Python
186201
• Bundling simple-project (1.2.3) into {tmpdir}: Installing dependencies
187202
• Bundling simple-project (1.2.3) into {tmpdir}: Failed at step Installing dependencies
188203
"""
@@ -212,11 +227,9 @@ def test_bundler_should_display_a_warning_for_projects_with_no_module(
212227
assert bundler.bundle(poetry, io)
213228

214229
path = str(tmp_venv.path)
215-
python_version = ".".join(str(v) for v in sys.version_info[:3])
216230
expected = f"""\
217231
• Bundling simple-project (1.2.3) into {path}
218-
• Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment
219-
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version}
232+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
220233
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
221234
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
222235
• Bundled simple-project (1.2.3) into {path}
@@ -259,11 +272,9 @@ def test_bundler_can_filter_dependency_groups(
259272
assert bundler.bundle(poetry, io)
260273

261274
path = tmpdir
262-
python_version = ".".join(str(v) for v in sys.version_info[:3])
263275
expected = f"""\
264276
• Bundling simple-project (1.2.3) into {path}
265-
• Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment
266-
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version}
277+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
267278
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
268279
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
269280
• Bundled simple-project (1.2.3) into {path}
@@ -296,11 +307,9 @@ def test_bundler_passes_compile_flag(
296307
mocker.assert_called_once_with(compile)
297308

298309
path = str(tmp_venv.path)
299-
python_version = ".".join(str(v) for v in sys.version_info[:3])
300310
expected = f"""\
301311
• Bundling simple-project (1.2.3) into {path}
302-
• Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment
303-
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version}
312+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
304313
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
305314
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
306315
• Bundled simple-project (1.2.3) into {path}
@@ -327,11 +336,9 @@ def test_bundler_editable_deps(
327336
bundler.bundle(poetry, io)
328337

329338
path = tmpdir
330-
python_version = ".".join(str(v) for v in sys.version_info[:3])
331339
expected = f"""\
332340
• Bundling simple-project (1.2.3) into {path}
333-
• Bundling simple-project (1.2.3) into {path}: Removing existing virtual environment
334-
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Python {python_version}
341+
• Bundling simple-project (1.2.3) into {path}: Creating a virtual environment using Poetry-determined Python
335342
• Bundling simple-project (1.2.3) into {path}: Installing dependencies
336343
• Bundling simple-project (1.2.3) into {path}: Installing simple-project (1.2.3)
337344
• Bundled simple-project (1.2.3) into {path}

0 commit comments

Comments
 (0)