Skip to content

Commit 2cc721c

Browse files
committed
fix(application): check context options preflight
This change ensures that context options like --project and --directory are validated prior to executing the application to prevent runtime exceptions.
1 parent 31c8e08 commit 2cc721c

File tree

4 files changed

+104
-11
lines changed

4 files changed

+104
-11
lines changed

src/poetry/console/application.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from poetry.console.command_loader import CommandLoader
2121
from poetry.console.commands.command import Command
2222
from poetry.utils.helpers import directory
23+
from poetry.utils.helpers import ensure_path
2324

2425

2526
if TYPE_CHECKING:
@@ -214,6 +215,12 @@ def create_io(
214215
return io
215216

216217
def _run(self, io: IO) -> int:
218+
# we do this here and not inside the _configure_io implementation in order
219+
# to ensure the users are not exposed to a stack trace for providing invalid values to
220+
# the options --directory or --project, configuring the options here allow cleo to trap and
221+
# display the error cleanly unless the user uses verbose or debug
222+
self._configure_custom_application_options(io)
223+
217224
self._load_plugins(io)
218225

219226
with directory(self._working_directory):
@@ -251,16 +258,20 @@ def _configure_custom_application_options(self, io: IO | None) -> None:
251258
self._disable_cache = self._option_get_value(
252259
io, "no-cache", self._disable_cache
253260
)
254-
self._working_directory = self._project_directory = Path(
255-
self._option_get_value(io, "directory", Path.cwd())
256-
)
257261

258-
self._project_directory = Path(
259-
self._option_get_value(io, "project", self._working_directory)
262+
# we use ensure_path for the directories to make sure these are valid paths
263+
# this will raise an exception if the path is invalid
264+
self._working_directory = self._project_directory = ensure_path(
265+
self._option_get_value(io, "directory", Path.cwd()), is_directory=True
260266
)
261267

262-
if self._project_directory != self._working_directory:
263-
with directory(self._working_directory):
268+
with directory(self._working_directory):
269+
# project directory is always resolved relative to the working directory
270+
self._project_directory = ensure_path(
271+
self._option_get_value(io, "project", self._working_directory),
272+
is_directory=True,
273+
)
274+
if not self._project_directory.is_absolute():
264275
self._project_directory = self._project_directory.absolute()
265276

266277
def _configure_io(self, io: IO) -> None:
@@ -299,8 +310,6 @@ def _configure_io(self, io: IO) -> None:
299310

300311
super()._configure_io(io)
301312

302-
self._configure_custom_application_options(io)
303-
304313
def register_command_loggers(
305314
self, event: Event, event_name: str, _: EventDispatcher
306315
) -> None:

src/poetry/utils/helpers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,18 @@ def paths_csv(paths: list[Path]) -> str:
258258
return ", ".join(f'"{c!s}"' for c in paths)
259259

260260

261+
def ensure_path(path: str | Path, is_directory: bool = False) -> Path:
262+
if isinstance(path, str):
263+
path = Path(path)
264+
265+
if path.exists() and path.is_dir() == is_directory:
266+
return path
267+
268+
raise ValueError(
269+
f"Specified path '{path}' is not a valid {'directory' if is_directory else 'file'}."
270+
)
271+
272+
261273
def is_dir_writable(path: Path, create: bool = False) -> bool:
262274
try:
263275
if not path.exists():

tests/console/test_application_global_options.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,34 @@ def test_application_global_option_position_does_not_matter(
9292
assert len(stdout.splitlines()) == 8
9393

9494

95+
@pytest.mark.parametrize("parameter", ["-C", "--directory", "-P", "--project"])
96+
@pytest.mark.parametrize(
97+
"invalid_source_directory",
98+
[
99+
"--/invalid/path",
100+
__file__,
101+
"is this a path ?",
102+
],
103+
)
104+
def test_application_global_option_context_is_validated(
105+
parameter: str,
106+
tester: ApplicationTester,
107+
invalid_source_directory: str,
108+
) -> None:
109+
source_directory = Path(invalid_source_directory).as_posix()
110+
error_string = f"\nSpecified path '{source_directory}' is not a valid directory.\n"
111+
112+
option = f"{parameter} '{source_directory}'"
113+
tester.execute(f"show {option}")
114+
assert tester.status_code != 0
115+
116+
stdout = tester.io.fetch_output()
117+
assert stdout == ""
118+
119+
stderr = tester.io.fetch_error()
120+
assert stderr == error_string
121+
122+
95123
@pytest.mark.parametrize("parameter", ["project", "directory"])
96124
def test_application_with_context_parameters(
97125
parameter: str,

tests/utils/test_helpers.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import base64
44
import re
55

6+
from pathlib import Path
67
from typing import TYPE_CHECKING
78
from typing import Any
89

@@ -14,13 +15,12 @@
1415
from poetry.utils.helpers import Downloader
1516
from poetry.utils.helpers import HTTPRangeRequestSupportedError
1617
from poetry.utils.helpers import download_file
18+
from poetry.utils.helpers import ensure_path
1719
from poetry.utils.helpers import get_file_hash
1820
from poetry.utils.helpers import get_highest_priority_hash_type
1921

2022

2123
if TYPE_CHECKING:
22-
from pathlib import Path
23-
2424
from httpretty import httpretty
2525
from httpretty.core import HTTPrettyRequest
2626

@@ -299,3 +299,47 @@ def test_downloader_uses_authenticator_by_default(
299299
request = http.last_request()
300300
basic_auth = base64.b64encode(b"bar:baz").decode()
301301
assert request.headers["Authorization"] == f"Basic {basic_auth}"
302+
303+
304+
def test_ensure_path_converts_string(tmp_path: Path) -> None:
305+
assert tmp_path.exists()
306+
assert ensure_path(path=tmp_path.as_posix(), is_directory=True) == tmp_path
307+
308+
309+
def test_ensure_path_does_not_convert_path(tmp_path: Path) -> None:
310+
assert tmp_path.exists()
311+
assert Path(tmp_path.as_posix()) is not tmp_path
312+
313+
result = ensure_path(path=tmp_path, is_directory=True)
314+
315+
assert result == tmp_path
316+
assert result is tmp_path
317+
318+
319+
def test_ensure_path_is_directory_parameter(tmp_path: Path) -> None:
320+
with pytest.raises(ValueError):
321+
ensure_path(path=tmp_path, is_directory=False)
322+
323+
assert ensure_path(path=tmp_path, is_directory=True) is tmp_path
324+
325+
326+
def test_ensure_path_file(tmp_path: Path) -> None:
327+
path = tmp_path.joinpath("some_file.txt")
328+
assert not path.exists()
329+
330+
with pytest.raises(ValueError):
331+
ensure_path(path=path, is_directory=False)
332+
333+
path.write_text("foobar")
334+
assert ensure_path(path=path, is_directory=False) is path
335+
336+
337+
def test_ensure_path_directory(tmp_path: Path) -> None:
338+
path = tmp_path.joinpath("foobar")
339+
assert not path.exists()
340+
341+
with pytest.raises(ValueError):
342+
ensure_path(path=path, is_directory=True)
343+
344+
path.mkdir()
345+
assert ensure_path(path=path, is_directory=True) is path

0 commit comments

Comments
 (0)