Skip to content

Commit

Permalink
feat(cli)!: actually switch directory with --directory/-C (#9831)
Browse files Browse the repository at this point in the history
Introduces `--project` to retain old behavior.
  • Loading branch information
finswimmer authored Nov 26, 2024
1 parent c70cbf4 commit f3deb4c
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 48 deletions.
3 changes: 2 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ then `--help` combined with any of those can give you more information.
* `--no-interaction (-n)`: Do not ask any interactive question.
* `--no-plugins`: Disables plugins.
* `--no-cache`: Disables Poetry source caches.
* `--directory=DIRECTORY (-C)`: The working directory for the Poetry command (defaults to the current working directory).
* `--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.
* `--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.


## new
Expand Down
103 changes: 64 additions & 39 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from poetry.__version__ import __version__
from poetry.console.command_loader import CommandLoader
from poetry.console.commands.command import Command
from poetry.utils.helpers import directory


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

@property
def _default_definition(self) -> Definition:
from cleo.io.inputs.option import Option

definition = super()._default_definition

definition.add_option(
Option("--no-plugins", flag=True, description="Disables plugins.")
)

definition.add_option(
Option(
"--no-cache", flag=True, description="Disables Poetry source caches."
)
)

definition.add_option(
Option(
"--project",
"-P",
flag=False,
description=(
"Specify another path as the project root."
" All command-line arguments will be resolved relative to the current working directory."
),
)
)

definition.add_option(
Option(
"--directory",
"-C",
flag=False,
description=(
"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."
),
)
)

return definition

@cached_property
def _project_directory(self) -> Path:
if self._io and self._io.input.option("project"):
with directory(self._working_directory):
return Path(self._io.input.option("project")).absolute()

return self._working_directory

@cached_property
def _working_directory(self) -> Path:
if self._io and self._io.input.option("directory"):
return Path(self._io.input.option("directory")).absolute()

return Path.cwd()

@property
def poetry(self) -> Poetry:
from poetry.factory import Factory
Expand All @@ -118,7 +177,7 @@ def poetry(self) -> Poetry:
return self._poetry

self._poetry = Factory().create_poetry(
cwd=self._directory,
cwd=self._project_directory,
io=self._io,
disable_plugins=self._disable_plugins,
disable_cache=self._disable_cache,
Expand Down Expand Up @@ -171,7 +230,9 @@ def _run(self, io: IO) -> int:

self._load_plugins(io)

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

return exit_code

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

PluginManager.add_project_plugin_path(self._directory)
PluginManager.add_project_plugin_path(self._project_directory)
manager = PluginManager(ApplicationPlugin.group)
manager.load_plugins()
manager.activate(self)

self._plugins_loaded = True

@property
def _default_definition(self) -> Definition:
from cleo.io.inputs.option import Option

definition = super()._default_definition

definition.add_option(
Option("--no-plugins", flag=True, description="Disables plugins.")
)

definition.add_option(
Option(
"--no-cache", flag=True, description="Disables Poetry source caches."
)
)

definition.add_option(
Option(
"--directory",
"-C",
flag=False,
description=(
"The working directory for the Poetry command (defaults to the"
" current working directory)."
),
)
)

return definition

@cached_property
def _directory(self) -> Path:
if self._io and self._io.input.option("directory"):
return Path(self._io.input.option("directory")).absolute()
return Path.cwd()


def main() -> int:
exit_code: int = Application().run()
Expand Down
8 changes: 3 additions & 5 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,10 @@ def handle(self) -> int:

project_path = Path.cwd()

if self.io.input.option("directory"):
project_path = Path(self.io.input.option("directory"))
if self.io.input.option("project"):
project_path = Path(self.io.input.option("project"))
if not project_path.exists() or not project_path.is_dir():
self.line_error(
"<error>The --directory path is not a directory.</error>"
)
self.line_error("<error>The --project path is not a directory.</error>")
return 1

return self._init_pyproject(project_path=project_path)
Expand Down
4 changes: 2 additions & 2 deletions src/poetry/console/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ class NewCommand(InitCommand):
def handle(self) -> int:
from pathlib import Path

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

Expand Down
2 changes: 1 addition & 1 deletion tests/console/commands/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def test_build_relative_directory_src_layout(
# initializes Poetry before passing the directory.
app = Application()
tester = ApplicationTester(app)
tester.execute("build --directory .")
tester.execute("build --project .")

build_dir = tmp_project_path / "dist"

Expand Down
108 changes: 108 additions & 0 deletions tests/console/commands/test_version.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
from __future__ import annotations

import os
import textwrap

from pathlib import Path
from typing import TYPE_CHECKING

import pytest

from cleo.testers.application_tester import ApplicationTester

from poetry.console.application import Application
from poetry.console.commands.version import VersionCommand


if TYPE_CHECKING:
from cleo.testers.command_tester import CommandTester
from pytest_mock import MockerFixture

from poetry.poetry import Poetry
from tests.types import CommandTesterFactory
Expand Down Expand Up @@ -132,3 +140,103 @@ def test_dry_run(tester: CommandTester) -> None:
new_pyproject = tester.command.poetry.file.path.read_text(encoding="utf-8")
assert tester.io.fetch_output() == "Bumping version from 1.2.3 to 2.0.0\n"
assert old_pyproject == new_pyproject


def test_version_with_project_parameter(
fixture_dir: FixtureDirGetter, mocker: MockerFixture
) -> None:
app = Application()
tester = ApplicationTester(app)

orig_version_command = VersionCommand.handle

def mock_handle(command: VersionCommand) -> int:
exit_code = orig_version_command(command)

command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")

return exit_code

mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)

source_dir = fixture_dir("scripts")
tester.execute(f"--project {source_dir} version")

output = tester.io.fetch_output()
expected = textwrap.dedent(f"""\
scripts 0.1.0
ProjectPath: {source_dir}
WorkingDirectory: {os.getcwd()}
""")

assert source_dir != Path(os.getcwd())
assert output == expected


def test_version_with_directory_parameter(
fixture_dir: FixtureDirGetter, mocker: MockerFixture
) -> None:
app = Application()
tester = ApplicationTester(app)

orig_version_command = VersionCommand.handle

def mock_handle(command: VersionCommand) -> int:
exit_code = orig_version_command(command)

command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")

return exit_code

mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)

source_dir = fixture_dir("scripts")
tester.execute(f"--directory {source_dir} version")

output = tester.io.fetch_output()
expected = textwrap.dedent(f"""\
scripts 0.1.0
ProjectPath: {source_dir}
WorkingDirectory: {source_dir}
""")

assert source_dir != Path(os.getcwd())
assert output == expected


def test_version_with_directory_and_project_parameter(
fixture_dir: FixtureDirGetter, mocker: MockerFixture
) -> None:
app = Application()
tester = ApplicationTester(app)

orig_version_command = VersionCommand.handle

def mock_handle(command: VersionCommand) -> int:
exit_code = orig_version_command(command)

command.io.write_line(f"ProjectPath: {command.poetry.pyproject_path.parent}")
command.io.write_line(f"WorkingDirectory: {os.getcwd()}")

return exit_code

mocker.patch("poetry.console.commands.version.VersionCommand.handle", mock_handle)

source_dir = fixture_dir("scripts")
working_directory = source_dir.parent
project_path = "./scripts"

tester.execute(f"--directory {working_directory} --project {project_path} version")

output = tester.io.fetch_output()

expected = textwrap.dedent(f"""\
scripts 0.1.0
ProjectPath: {source_dir}
WorkingDirectory: {working_directory}
""")

assert source_dir != working_directory
assert output == expected

0 comments on commit f3deb4c

Please sign in to comment.