Skip to content

Commit c630dc6

Browse files
committed
cli/new: support interactive config
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.
1 parent 0753fb0 commit c630dc6

File tree

7 files changed

+177
-151
lines changed

7 files changed

+177
-151
lines changed

docs/cli.md

+6
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,17 @@ my-package
102102

103103
### Options
104104

105+
* `--interactive (-i)`: Allow interactive specification of project configuration.
105106
* `--name`: Set the resulting package name.
106107
* `--src`: Use the src layout for the project.
107108
* `--readme`: Specify the readme file extension. Default is `md`. If you intend to publish to PyPI
108109
keep the [recommendations for a PyPI-friendly README](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/)
109110
in mind.
111+
* `--description`: Description of the package.
112+
* `--author`: Author of the package.
113+
* `--python` Compatible Python versions.
114+
* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`.
115+
* `--dev-dependency`: Development requirements, see `--dependency`.
110116

111117

112118
## init

src/poetry/console/commands/init.py

+95-59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from contextlib import suppress
34
from pathlib import Path
45
from typing import TYPE_CHECKING
56
from typing import Any
@@ -69,13 +70,6 @@ def __init__(self) -> None:
6970
def handle(self) -> int:
7071
from pathlib import Path
7172

72-
from poetry.core.vcs.git import GitConfig
73-
74-
from poetry.config.config import Config
75-
from poetry.layouts import layout
76-
from poetry.pyproject.toml import PyProjectTOML
77-
from poetry.utils.env import EnvManager
78-
7973
project_path = Path.cwd()
8074

8175
if self.io.input.option("directory"):
@@ -86,6 +80,24 @@ def handle(self) -> int:
8680
)
8781
return 1
8882

83+
return self._init_pyproject(project_path=project_path)
84+
85+
def _init_pyproject(
86+
self,
87+
project_path: Path,
88+
allow_interactive: bool = True,
89+
layout_name: str = "standard",
90+
readme_format: str = "md",
91+
) -> int:
92+
from poetry.core.vcs.git import GitConfig
93+
94+
from poetry.config.config import Config
95+
from poetry.layouts import layout
96+
from poetry.pyproject.toml import PyProjectTOML
97+
from poetry.utils.env import EnvManager
98+
99+
is_interactive = self.io.is_interactive() and allow_interactive
100+
89101
pyproject = PyProjectTOML(project_path / "pyproject.toml")
90102

91103
if pyproject.file.exists():
@@ -105,7 +117,7 @@ def handle(self) -> int:
105117

106118
vcs_config = GitConfig()
107119

108-
if self.io.is_interactive():
120+
if is_interactive:
109121
self.line("")
110122
self.line(
111123
"This command will guide you through creating your"
@@ -115,21 +127,24 @@ def handle(self) -> int:
115127

116128
name = self.option("name")
117129
if not name:
118-
name = Path.cwd().name.lower()
130+
name = project_path.name.lower()
119131

120-
question = self.create_question(
121-
f"Package name [<comment>{name}</comment>]: ", default=name
122-
)
123-
name = self.ask(question)
132+
if is_interactive:
133+
question = self.create_question(
134+
f"Package name [<comment>{name}</comment>]: ", default=name
135+
)
136+
name = self.ask(question)
124137

125138
version = "0.1.0"
126-
question = self.create_question(
127-
f"Version [<comment>{version}</comment>]: ", default=version
128-
)
129-
version = self.ask(question)
130139

131-
description = self.option("description")
132-
if not description:
140+
if is_interactive:
141+
question = self.create_question(
142+
f"Version [<comment>{version}</comment>]: ", default=version
143+
)
144+
version = self.ask(question)
145+
146+
description = self.option("description") or ""
147+
if not description and is_interactive:
133148
description = self.ask(self.create_question("Description []: ", default=""))
134149

135150
author = self.option("author")
@@ -139,22 +154,23 @@ def handle(self) -> int:
139154
if author_email:
140155
author += f" <{author_email}>"
141156

142-
question = self.create_question(
143-
f"Author [<comment>{author}</comment>, n to skip]: ", default=author
144-
)
145-
question.set_validator(lambda v: self._validate_author(v, author))
146-
author = self.ask(question)
157+
if is_interactive:
158+
question = self.create_question(
159+
f"Author [<comment>{author}</comment>, n to skip]: ", default=author
160+
)
161+
question.set_validator(lambda v: self._validate_author(v, author))
162+
author = self.ask(question)
147163

148164
authors = [author] if author else []
149165

150-
license = self.option("license")
151-
if not license:
152-
license = self.ask(self.create_question("License []: ", default=""))
166+
license_name = self.option("license")
167+
if not license_name and is_interactive:
168+
license_name = self.ask(self.create_question("License []: ", default=""))
153169

154170
python = self.option("python")
155171
if not python:
156172
config = Config.create()
157-
default_python = (
173+
python = (
158174
"^"
159175
+ EnvManager.get_python_version(
160176
precision=2,
@@ -163,13 +179,14 @@ def handle(self) -> int:
163179
).to_string()
164180
)
165181

166-
question = self.create_question(
167-
f"Compatible Python versions [<comment>{default_python}</comment>]: ",
168-
default=default_python,
169-
)
170-
python = self.ask(question)
182+
if is_interactive:
183+
question = self.create_question(
184+
f"Compatible Python versions [<comment>{python}</comment>]: ",
185+
default=python,
186+
)
187+
python = self.ask(question)
171188

172-
if self.io.is_interactive():
189+
if is_interactive:
173190
self.line("")
174191

175192
requirements: Requirements = {}
@@ -180,27 +197,25 @@ def handle(self) -> int:
180197

181198
question_text = "Would you like to define your main dependencies interactively?"
182199
help_message = """\
183-
You can specify a package in the following forms:
184-
- A single name (<b>requests</b>): this will search for matches on PyPI
185-
- A name and a constraint (<b>requests@^2.23.0</b>)
186-
- A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
187-
- A git url with a revision\
188-
(<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
189-
- A file path (<b>../my-package/my-package.whl</b>)
190-
- A directory (<b>../my-package/</b>)
191-
- A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)
192-
"""
200+
You can specify a package in the following forms:
201+
- A single name (<b>requests</b>): this will search for matches on PyPI
202+
- A name and a constraint (<b>requests@^2.23.0</b>)
203+
- A git url (<b>git+https://github.com/python-poetry/poetry.git</b>)
204+
- A git url with a revision\
205+
(<b>git+https://github.com/python-poetry/poetry.git#develop</b>)
206+
- A file path (<b>../my-package/my-package.whl</b>)
207+
- A directory (<b>../my-package/</b>)
208+
- A url (<b>https://example.com/packages/my-package-0.1.0.tar.gz</b>)
209+
"""
193210

194211
help_displayed = False
195-
if self.confirm(question_text, True):
196-
if self.io.is_interactive():
197-
self.line(help_message)
198-
help_displayed = True
212+
if is_interactive and self.confirm(question_text, True):
213+
self.line(help_message)
214+
help_displayed = True
199215
requirements.update(
200216
self._format_requirements(self._determine_requirements([]))
201217
)
202-
if self.io.is_interactive():
203-
self.line("")
218+
self.line("")
204219

205220
dev_requirements: Requirements = {}
206221
if self.option("dev-dependency"):
@@ -211,44 +226,61 @@ def handle(self) -> int:
211226
question_text = (
212227
"Would you like to define your development dependencies interactively?"
213228
)
214-
if self.confirm(question_text, True):
215-
if self.io.is_interactive() and not help_displayed:
229+
if is_interactive and self.confirm(question_text, True):
230+
if not help_displayed:
216231
self.line(help_message)
217232

218233
dev_requirements.update(
219234
self._format_requirements(self._determine_requirements([]))
220235
)
221-
if self.io.is_interactive():
222-
self.line("")
223236

224-
layout_ = layout("standard")(
237+
self.line("")
238+
239+
layout_ = layout(layout_name)(
225240
name,
226241
version,
227242
description=description,
228243
author=authors[0] if authors else None,
229-
license=license,
244+
readme_format=readme_format,
245+
license=license_name,
230246
python=python,
231247
dependencies=requirements,
232248
dev_dependencies=dev_requirements,
233249
)
234250

251+
create_layout = not project_path.exists()
252+
253+
if create_layout:
254+
layout_.create(project_path, with_pyproject=False)
255+
235256
content = layout_.generate_poetry_content()
236257
for section, item in content.items():
237258
pyproject.data.append(section, item)
238259

239-
if self.io.is_interactive():
260+
if is_interactive:
240261
self.line("<info>Generated file</info>")
241262
self.line("")
242263
self.line(pyproject.data.as_string().replace("\r\n", "\n"))
243264
self.line("")
244265

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

248269
return 1
249270

250271
pyproject.save()
251272

273+
if create_layout:
274+
path = project_path.resolve()
275+
276+
with suppress(ValueError):
277+
path = path.relative_to(Path.cwd())
278+
279+
self.line(
280+
f"Created package <info>{layout_._package_name}</> in"
281+
f" <fg=blue>{path.as_posix()}</>"
282+
)
283+
252284
return 0
253285

254286
def _generate_choice_list(
@@ -276,7 +308,11 @@ def _determine_requirements(
276308
requires: list[str],
277309
allow_prereleases: bool = False,
278310
source: str | None = None,
311+
is_interactive: bool | None = None,
279312
) -> list[dict[str, Any]]:
313+
if is_interactive is None:
314+
is_interactive = self.io.is_interactive()
315+
280316
if not requires:
281317
result = []
282318

@@ -366,7 +402,7 @@ def _determine_requirements(
366402
if package:
367403
result.append(constraint)
368404

369-
if self.io.is_interactive():
405+
if is_interactive:
370406
package = self.ask(follow_up_question)
371407

372408
return result

0 commit comments

Comments
 (0)