Skip to content

Commit

Permalink
cli/new: support interactive config
Browse files Browse the repository at this point in the history
This change allows new command to be used interactively similar to the
init command. Additionally, this also allows for configuration of
description, author, python and dependencies via command line options.
  • Loading branch information
abn committed Mar 3, 2024
1 parent 748883e commit ce826b9
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 150 deletions.
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.
* `--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

0 comments on commit ce826b9

Please sign in to comment.