Skip to content

Commit aace0b2

Browse files
committed
fix(application): handle global options correctly
Previously, the application implementation did not handle global options correctly when positioned after command options. This lead to incorrect cached working directory values prior to switching application context. Resolves: #9978
1 parent bd500dd commit aace0b2

File tree

2 files changed

+107
-19
lines changed

2 files changed

+107
-19
lines changed

src/poetry/console/application.py

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import re
55

66
from contextlib import suppress
7-
from functools import cached_property
87
from importlib import import_module
98
from pathlib import Path
109
from typing import TYPE_CHECKING
@@ -25,6 +24,7 @@
2524

2625
if TYPE_CHECKING:
2726
from collections.abc import Callable
27+
from typing import Any
2828

2929
from cleo.events.event import Event
3030
from cleo.io.inputs.argv_input import ArgvInput
@@ -103,6 +103,8 @@ def __init__(self) -> None:
103103
self._disable_plugins = False
104104
self._disable_cache = False
105105
self._plugins_loaded = False
106+
self._working_directory = Path.cwd()
107+
self._project_directory = self._working_directory
106108

107109
dispatcher = EventDispatcher()
108110
dispatcher.add_listener(COMMAND, self.register_command_loggers)
@@ -156,21 +158,6 @@ def _default_definition(self) -> Definition:
156158

157159
return definition
158160

159-
@cached_property
160-
def _project_directory(self) -> Path:
161-
if self._io and self._io.input.option("project"):
162-
with directory(self._working_directory):
163-
return Path(self._io.input.option("project")).absolute()
164-
165-
return self._working_directory
166-
167-
@cached_property
168-
def _working_directory(self) -> Path:
169-
if self._io and self._io.input.option("directory"):
170-
return Path(self._io.input.option("directory")).absolute()
171-
172-
return Path.cwd()
173-
174161
@property
175162
def poetry(self) -> Poetry:
176163
from poetry.factory import Factory
@@ -227,16 +214,55 @@ def create_io(
227214
return io
228215

229216
def _run(self, io: IO) -> int:
230-
self._disable_plugins = io.input.parameter_option("--no-plugins")
231-
self._disable_cache = io.input.has_parameter_option("--no-cache")
232-
233217
self._load_plugins(io)
234218

235219
with directory(self._working_directory):
236220
exit_code: int = super()._run(io)
237221

238222
return exit_code
239223

224+
def _option_get_value(self, io: IO, name: str, default: Any) -> Any:
225+
option = self.definition.option(name)
226+
227+
if option is None:
228+
return default
229+
230+
values = [f"--{option.name}"]
231+
232+
if option.shortcut:
233+
values.append(f"-{option.shortcut}")
234+
235+
if not io.input.has_parameter_option(values):
236+
return default
237+
238+
if option.is_flag():
239+
return True
240+
241+
return io.input.parameter_option(values=values, default=default)
242+
243+
def _configure_custom_application_options(self, io: IO) -> None:
244+
if io is None:
245+
# nothing to do here then
246+
return
247+
248+
self._disable_plugins = self._option_get_value(
249+
io, "no-plugins", self._disable_plugins
250+
)
251+
self._disable_cache = self._option_get_value(
252+
io, "no-cache", self._disable_cache
253+
)
254+
self._working_directory = self._project_directory = Path(
255+
self._option_get_value(io, "directory", Path.cwd())
256+
)
257+
258+
self._project_directory = Path(
259+
self._option_get_value(io, "project", self._working_directory)
260+
)
261+
262+
if self._project_directory != self._working_directory:
263+
with directory(self._working_directory):
264+
self._project_directory = self._project_directory.absolute()
265+
240266
def _configure_io(self, io: IO) -> None:
241267
# We need to check if the command being run
242268
# is the "run" command.
@@ -273,6 +299,8 @@ def _configure_io(self, io: IO) -> None:
273299

274300
super()._configure_io(io)
275301

302+
self._configure_custom_application_options(io)
303+
276304
def register_command_loggers(
277305
self, event: Event, event_name: str, _: EventDispatcher
278306
) -> None:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING
5+
6+
import pytest
7+
8+
from cleo.testers.application_tester import ApplicationTester
9+
10+
from poetry.console.application import Application
11+
12+
13+
if TYPE_CHECKING:
14+
from tests.types import FixtureCopier
15+
16+
17+
@pytest.fixture
18+
def project_source_directory(fixture_copier: FixtureCopier) -> Path:
19+
return fixture_copier("up_to_date_lock")
20+
21+
22+
@pytest.fixture
23+
def tester() -> ApplicationTester:
24+
return ApplicationTester(Application())
25+
26+
27+
@pytest.mark.parametrize("parameter", ["-C", "--directory", "-P", "--project"])
28+
def test_application_global_option_position_does_not_matter(
29+
parameter: str, tester: ApplicationTester, project_source_directory: Path
30+
) -> None:
31+
cwd = Path.cwd()
32+
assert cwd != project_source_directory
33+
34+
error_string = "Poetry could not find a pyproject.toml file in"
35+
36+
# command fails due to lack of pyproject.toml file in cwd
37+
tester.execute("show --only main")
38+
assert tester.status_code != 0
39+
40+
stderr = tester.io.fetch_error()
41+
assert error_string in stderr
42+
43+
option = f"{parameter} {project_source_directory.as_posix()}"
44+
45+
for args in [
46+
f"{option} show --only main",
47+
f"show {option} --only main",
48+
f"show --only main {option}",
49+
]:
50+
tester.execute(args)
51+
assert tester.status_code == 0
52+
53+
stdout = tester.io.fetch_output()
54+
stderr = tester.io.fetch_error()
55+
56+
assert error_string not in stderr
57+
assert error_string not in stdout
58+
59+
assert "certifi" in stdout
60+
assert len(stdout.splitlines()) == 8

0 commit comments

Comments
 (0)