Skip to content

Commit f3deb4c

Browse files
authored
feat(cli)!: actually switch directory with --directory/-C (#9831)
Introduces `--project` to retain old behavior.
1 parent c70cbf4 commit f3deb4c

File tree

6 files changed

+180
-48
lines changed

6 files changed

+180
-48
lines changed

docs/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ then `--help` combined with any of those can give you more information.
2929
* `--no-interaction (-n)`: Do not ask any interactive question.
3030
* `--no-plugins`: Disables plugins.
3131
* `--no-cache`: Disables Poetry source caches.
32-
* `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory).
32+
* `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory). All command-line arguments will be resolved relative to the given directory.
33+
* `--project=PROJECT (-P)`: Specify another path as the project root. All command-line arguments will be resolved relative to the current working directory or directory specified using `--directory` option if used.
3334

3435

3536
## new

src/poetry/console/application.py

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

2425

2526
if TYPE_CHECKING:
@@ -110,6 +111,64 @@ def __init__(self) -> None:
110111
command_loader = CommandLoader({name: load_command(name) for name in COMMANDS})
111112
self.set_command_loader(command_loader)
112113

114+
@property
115+
def _default_definition(self) -> Definition:
116+
from cleo.io.inputs.option import Option
117+
118+
definition = super()._default_definition
119+
120+
definition.add_option(
121+
Option("--no-plugins", flag=True, description="Disables plugins.")
122+
)
123+
124+
definition.add_option(
125+
Option(
126+
"--no-cache", flag=True, description="Disables Poetry source caches."
127+
)
128+
)
129+
130+
definition.add_option(
131+
Option(
132+
"--project",
133+
"-P",
134+
flag=False,
135+
description=(
136+
"Specify another path as the project root."
137+
" All command-line arguments will be resolved relative to the current working directory."
138+
),
139+
)
140+
)
141+
142+
definition.add_option(
143+
Option(
144+
"--directory",
145+
"-C",
146+
flag=False,
147+
description=(
148+
"The working directory for the Poetry command (defaults to the"
149+
" current working directory). All command-line arguments will be"
150+
" resolved relative to the given directory."
151+
),
152+
)
153+
)
154+
155+
return definition
156+
157+
@cached_property
158+
def _project_directory(self) -> Path:
159+
if self._io and self._io.input.option("project"):
160+
with directory(self._working_directory):
161+
return Path(self._io.input.option("project")).absolute()
162+
163+
return self._working_directory
164+
165+
@cached_property
166+
def _working_directory(self) -> Path:
167+
if self._io and self._io.input.option("directory"):
168+
return Path(self._io.input.option("directory")).absolute()
169+
170+
return Path.cwd()
171+
113172
@property
114173
def poetry(self) -> Poetry:
115174
from poetry.factory import Factory
@@ -118,7 +177,7 @@ def poetry(self) -> Poetry:
118177
return self._poetry
119178

120179
self._poetry = Factory().create_poetry(
121-
cwd=self._directory,
180+
cwd=self._project_directory,
122181
io=self._io,
123182
disable_plugins=self._disable_plugins,
124183
disable_cache=self._disable_cache,
@@ -171,7 +230,9 @@ def _run(self, io: IO) -> int:
171230

172231
self._load_plugins(io)
173232

174-
exit_code: int = super()._run(io)
233+
with directory(self._working_directory):
234+
exit_code: int = super()._run(io)
235+
175236
return exit_code
176237

177238
def _configure_io(self, io: IO) -> None:
@@ -331,49 +392,13 @@ def _load_plugins(self, io: IO) -> None:
331392
from poetry.plugins.application_plugin import ApplicationPlugin
332393
from poetry.plugins.plugin_manager import PluginManager
333394

334-
PluginManager.add_project_plugin_path(self._directory)
395+
PluginManager.add_project_plugin_path(self._project_directory)
335396
manager = PluginManager(ApplicationPlugin.group)
336397
manager.load_plugins()
337398
manager.activate(self)
338399

339400
self._plugins_loaded = True
340401

341-
@property
342-
def _default_definition(self) -> Definition:
343-
from cleo.io.inputs.option import Option
344-
345-
definition = super()._default_definition
346-
347-
definition.add_option(
348-
Option("--no-plugins", flag=True, description="Disables plugins.")
349-
)
350-
351-
definition.add_option(
352-
Option(
353-
"--no-cache", flag=True, description="Disables Poetry source caches."
354-
)
355-
)
356-
357-
definition.add_option(
358-
Option(
359-
"--directory",
360-
"-C",
361-
flag=False,
362-
description=(
363-
"The working directory for the Poetry command (defaults to the"
364-
" current working directory)."
365-
),
366-
)
367-
)
368-
369-
return definition
370-
371-
@cached_property
372-
def _directory(self) -> Path:
373-
if self._io and self._io.input.option("directory"):
374-
return Path(self._io.input.option("directory")).absolute()
375-
return Path.cwd()
376-
377402

378403
def main() -> int:
379404
exit_code: int = Application().run()

src/poetry/console/commands/init.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,10 @@ def handle(self) -> int:
7474

7575
project_path = Path.cwd()
7676

77-
if self.io.input.option("directory"):
78-
project_path = Path(self.io.input.option("directory"))
77+
if self.io.input.option("project"):
78+
project_path = Path(self.io.input.option("project"))
7979
if not project_path.exists() or not project_path.is_dir():
80-
self.line_error(
81-
"<error>The --directory path is not a directory.</error>"
82-
)
80+
self.line_error("<error>The --project path is not a directory.</error>")
8381
return 1
8482

8583
return self._init_pyproject(project_path=project_path)

src/poetry/console/commands/new.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ class NewCommand(InitCommand):
5454
def handle(self) -> int:
5555
from pathlib import Path
5656

57-
if self.io.input.option("directory"):
57+
if self.io.input.option("project"):
5858
self.line_error(
59-
"<warning>--directory only makes sense with existing projects, and will"
59+
"<warning>--project only makes sense with existing projects, and will"
6060
" be ignored. You should consider the option --path instead.</warning>"
6161
)
6262

tests/console/commands/test_build.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def test_build_relative_directory_src_layout(
206206
# initializes Poetry before passing the directory.
207207
app = Application()
208208
tester = ApplicationTester(app)
209-
tester.execute("build --directory .")
209+
tester.execute("build --project .")
210210

211211
build_dir = tmp_project_path / "dist"
212212

tests/console/commands/test_version.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
from __future__ import annotations
22

3+
import os
4+
import textwrap
5+
6+
from pathlib import Path
37
from typing import TYPE_CHECKING
48

59
import pytest
610

11+
from cleo.testers.application_tester import ApplicationTester
12+
13+
from poetry.console.application import Application
714
from poetry.console.commands.version import VersionCommand
815

916

1017
if TYPE_CHECKING:
1118
from cleo.testers.command_tester import CommandTester
19+
from pytest_mock import MockerFixture
1220

1321
from poetry.poetry import Poetry
1422
from tests.types import CommandTesterFactory
@@ -132,3 +140,103 @@ def test_dry_run(tester: CommandTester) -> None:
132140
new_pyproject = tester.command.poetry.file.path.read_text(encoding="utf-8")
133141
assert tester.io.fetch_output() == "Bumping version from 1.2.3 to 2.0.0\n"
134142
assert old_pyproject == new_pyproject
143+
144+
145+
def test_version_with_project_parameter(
146+
fixture_dir: FixtureDirGetter, mocker: MockerFixture
147+
) -> None:
148+
app = Application()
149+
tester = ApplicationTester(app)
150+
151+
orig_version_command = VersionCommand.handle
152+
153+
def mock_handle(command: VersionCommand) -> int:
154+
exit_code = orig_version_command(command)
155+
156+
command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
157+
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")
158+
159+
return exit_code
160+
161+
mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)
162+
163+
source_dir = fixture_dir("scripts")
164+
tester.execute(f"--project {source_dir} version")
165+
166+
output = tester.io.fetch_output()
167+
expected = textwrap.dedent(f"""\
168+
scripts 0.1.0
169+
ProjectPath: {source_dir}
170+
WorkingDirectory: {os.getcwd()}
171+
""")
172+
173+
assert source_dir != Path(os.getcwd())
174+
assert output == expected
175+
176+
177+
def test_version_with_directory_parameter(
178+
fixture_dir: FixtureDirGetter, mocker: MockerFixture
179+
) -> None:
180+
app = Application()
181+
tester = ApplicationTester(app)
182+
183+
orig_version_command = VersionCommand.handle
184+
185+
def mock_handle(command: VersionCommand) -> int:
186+
exit_code = orig_version_command(command)
187+
188+
command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
189+
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")
190+
191+
return exit_code
192+
193+
mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)
194+
195+
source_dir = fixture_dir("scripts")
196+
tester.execute(f"--directory {source_dir} version")
197+
198+
output = tester.io.fetch_output()
199+
expected = textwrap.dedent(f"""\
200+
scripts 0.1.0
201+
ProjectPath: {source_dir}
202+
WorkingDirectory: {source_dir}
203+
""")
204+
205+
assert source_dir != Path(os.getcwd())
206+
assert output == expected
207+
208+
209+
def test_version_with_directory_and_project_parameter(
210+
fixture_dir: FixtureDirGetter, mocker: MockerFixture
211+
) -> None:
212+
app = Application()
213+
tester = ApplicationTester(app)
214+
215+
orig_version_command = VersionCommand.handle
216+
217+
def mock_handle(command: VersionCommand) -> int:
218+
exit_code = orig_version_command(command)
219+
220+
command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
221+
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")
222+
223+
return exit_code
224+
225+
mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)
226+
227+
source_dir = fixture_dir("scripts")
228+
working_directory = source_dir.parent
229+
project_path = "./scripts"
230+
231+
tester.execute(f"--directory {working_directory} --project {project_path} version")
232+
233+
output = tester.io.fetch_output()
234+
235+
expected = textwrap.dedent(f"""\
236+
scripts 0.1.0
237+
ProjectPath: {source_dir}
238+
WorkingDirectory: {working_directory}
239+
""")
240+
241+
assert source_dir != working_directory
242+
assert output == expected

0 commit comments

Comments
 (0)