Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli/new: support interactive config #9088

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,17 @@ my-package

### Options

* `--interactive (-i)`: Allow interactive specification of project configuration.
abn marked this conversation as resolved.
Show resolved Hide resolved
* `--name`: Set the resulting package name.
* `--src`: Use the src layout for the project.
* `--readme`: Specify the readme file extension. Default is `md`. If you intend to publish to PyPI
keep the [recommendations for a PyPI-friendly README](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/)
in mind.
* `--description`: Description of the package.
* `--author`: Author of the package.
* `--python` Compatible Python versions.
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
* `--dev-dependency`: Development requirements, see `--dependency`.


## init
Expand Down
154 changes: 95 additions & 59 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
Expand Down Expand Up @@ -71,13 +72,6 @@ def __init__(self) -> None:
def handle(self) -> int:
from pathlib import Path

from poetry.core.vcs.git import GitConfig

from poetry.config.config import Config
from poetry.layouts import layout
from poetry.pyproject.toml import PyProjectTOML
from poetry.utils.env import EnvManager

project_path = Path.cwd()

if self.io.input.option("directory"):
Expand All @@ -88,6 +82,24 @@ def handle(self) -> int:
)
return 1

return self._init_pyproject(project_path=project_path)

def _init_pyproject(
self,
project_path: Path,
allow_interactive: bool = True,
layout_name: str = "standard",
readme_format: str = "md",
) -> int:
from poetry.core.vcs.git import GitConfig

from poetry.config.config import Config
from poetry.layouts import layout
from poetry.pyproject.toml import PyProjectTOML
from poetry.utils.env import EnvManager

is_interactive = self.io.is_interactive() and allow_interactive

pyproject = PyProjectTOML(project_path / "pyproject.toml")

if pyproject.file.exists():
Expand All @@ -107,7 +119,7 @@ def handle(self) -> int:

vcs_config = GitConfig()

if self.io.is_interactive():
if is_interactive:
self.line("")
self.line(
"This command will guide you through creating your"
Expand All @@ -117,21 +129,24 @@ def handle(self) -> int:

name = self.option("name")
if not name:
name = Path.cwd().name.lower()
name = project_path.name.lower()

question = self.create_question(
f"Package name [<comment>{name}</comment>]: ", default=name
)
name = self.ask(question)
if is_interactive:
question = self.create_question(
f"Package name [<comment>{name}</comment>]: ", default=name
)
name = self.ask(question)

version = "0.1.0"
question = self.create_question(
f"Version [<comment>{version}</comment>]: ", default=version
)
version = self.ask(question)

description = self.option("description")
if not description:
if is_interactive:
question = self.create_question(
f"Version [<comment>{version}</comment>]: ", default=version
)
version = self.ask(question)

description = self.option("description") or ""
if not description and is_interactive:
description = self.ask(self.create_question("Description []: ", default=""))

author = self.option("author")
Expand All @@ -141,22 +156,23 @@ def handle(self) -> int:
if author_email:
author += f" <{author_email}>"

question = self.create_question(
f"Author [<comment>{author}</comment>, n to skip]: ", default=author
)
question.set_validator(lambda v: self._validate_author(v, author))
author = self.ask(question)
if is_interactive:
question = self.create_question(
f"Author [<comment>{author}</comment>, n to skip]: ", default=author
)
question.set_validator(lambda v: self._validate_author(v, author))
author = self.ask(question)

authors = [author] if author else []

license = self.option("license")
if not license:
license = self.ask(self.create_question("License []: ", default=""))
license_name = self.option("license")
if not license_name and is_interactive:
license_name = self.ask(self.create_question("License []: ", default=""))

python = self.option("python")
if not python:
config = Config.create()
default_python = (
python = (
"^"
+ EnvManager.get_python_version(
precision=2,
Expand All @@ -165,13 +181,14 @@ def handle(self) -> int:
).to_string()
)

question = self.create_question(
f"Compatible Python versions [<comment>{default_python}</comment>]: ",
default=default_python,
)
python = self.ask(question)
if is_interactive:
question = self.create_question(
f"Compatible Python versions [<comment>{python}</comment>]: ",
default=python,
)
python = self.ask(question)

if self.io.is_interactive():
if is_interactive:
self.line("")

requirements: Requirements = {}
Expand All @@ -182,27 +199,25 @@ def handle(self) -> int:

question_text = "Would you like to define your main dependencies interactively?"
help_message = """\
You can specify a package in the following forms:
- A single name (<b>requests</b>): this will search for matches on PyPI
- A name and a constraint (<b>requests@^2.23.0</b>)
- A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
- A git url with a revision\
(<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
- A file path (<b>../my-package/my-package.whl</b>)
- A directory (<b>../my-package/</b>)
- A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)
"""
You can specify a package in the following forms:
- A single name (<b>requests</b>): this will search for matches on PyPI
- A name and a constraint (<b>requests@^2.23.0</b>)
- A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
- A git url with a revision\
(<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
- A file path (<b>../my-package/my-package.whl</b>)
- A directory (<b>../my-package/</b>)
- A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)
"""

help_displayed = False
if self.confirm(question_text, True):
if self.io.is_interactive():
self.line(help_message)
help_displayed = True
if is_interactive and self.confirm(question_text, True):
self.line(help_message)
help_displayed = True
requirements.update(
self._format_requirements(self._determine_requirements([]))
)
if self.io.is_interactive():
self.line("")
self.line("")

dev_requirements: Requirements = {}
if self.option("dev-dependency"):
Expand All @@ -213,44 +228,61 @@ def handle(self) -> int:
question_text = (
"Would you like to define your development dependencies interactively?"
)
if self.confirm(question_text, True):
if self.io.is_interactive() and not help_displayed:
if is_interactive and self.confirm(question_text, True):
if not help_displayed:
self.line(help_message)

dev_requirements.update(
self._format_requirements(self._determine_requirements([]))
)
if self.io.is_interactive():
self.line("")

layout_ = layout("standard")(
self.line("")

layout_ = layout(layout_name)(
name,
version,
description=description,
author=authors[0] if authors else None,
license=license,
readme_format=readme_format,
license=license_name,
python=python,
dependencies=requirements,
dev_dependencies=dev_requirements,
)

create_layout = not project_path.exists()

if create_layout:
layout_.create(project_path, with_pyproject=False)

content = layout_.generate_poetry_content()
for section, item in content.items():
pyproject.data.append(section, item)

if self.io.is_interactive():
if is_interactive:
self.line("<info>Generated file</info>")
self.line("")
self.line(pyproject.data.as_string().replace("\r\n", "\n"))
self.line("")

if not self.confirm("Do you confirm generation?", True):
if is_interactive and not self.confirm("Do you confirm generation?", True):
self.line_error("<error>Command aborted</error>")

return 1

pyproject.save()

if create_layout:
path = project_path.resolve()

with suppress(ValueError):
path = path.relative_to(Path.cwd())

self.line(
f"Created package <info>{layout_._package_name}</> in"
f" <fg=blue>{path.as_posix()}</>"
)

return 0

def _generate_choice_list(
Expand Down Expand Up @@ -278,7 +310,11 @@ def _determine_requirements(
requires: list[str],
allow_prereleases: bool = False,
source: str | None = None,
is_interactive: bool | None = None,
) -> list[dict[str, Any]]:
if is_interactive is None:
is_interactive = self.io.is_interactive()

if not requires:
result = []

Expand Down Expand Up @@ -368,7 +404,7 @@ def _determine_requirements(
if package:
result.append(constraint)

if self.io.is_interactive():
if is_interactive:
package = self.ask(follow_up_question)

return result
Expand Down
Loading
Loading