diff --git a/docs/cli.md b/docs/cli.md
index 7a10d62d63f..d47617007fe 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -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
diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py
index 670fa4cb09e..3e93adc763d 100644
--- a/src/poetry/console/commands/init.py
+++ b/src/poetry/console/commands/init.py
@@ -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
@@ -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"):
@@ -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():
@@ -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"
@@ -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 [{name}]: ", default=name
- )
- name = self.ask(question)
+ if is_interactive:
+ question = self.create_question(
+ f"Package name [{name}]: ", default=name
+ )
+ name = self.ask(question)
version = "0.1.0"
- question = self.create_question(
- f"Version [{version}]: ", default=version
- )
- version = self.ask(question)
- description = self.option("description")
- if not description:
+ if is_interactive:
+ question = self.create_question(
+ f"Version [{version}]: ", 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")
@@ -141,22 +156,23 @@ def handle(self) -> int:
if author_email:
author += f" <{author_email}>"
- question = self.create_question(
- f"Author [{author}, 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 [{author}, 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,
@@ -165,13 +181,14 @@ def handle(self) -> int:
).to_string()
)
- question = self.create_question(
- f"Compatible Python versions [{default_python}]: ",
- default=default_python,
- )
- python = self.ask(question)
+ if is_interactive:
+ question = self.create_question(
+ f"Compatible Python versions [{python}]: ",
+ default=python,
+ )
+ python = self.ask(question)
- if self.io.is_interactive():
+ if is_interactive:
self.line("")
requirements: Requirements = {}
@@ -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 (requests): this will search for matches on PyPI
- - A name and a constraint (requests@^2.23.0)
- - A git url (git+https://github.com/python-poetry/poetry.git)
- - A git url with a revision\
- (git+https://github.com/python-poetry/poetry.git#develop)
- - A file path (../my-package/my-package.whl)
- - A directory (../my-package/)
- - A url (https://example.com/packages/my-package-0.1.0.tar.gz)
-"""
+ You can specify a package in the following forms:
+ - A single name (requests): this will search for matches on PyPI
+ - A name and a constraint (requests@^2.23.0)
+ - A git url (git+https://github.com/python-poetry/poetry.git)
+ - A git url with a revision\
+ (git+https://github.com/python-poetry/poetry.git#develop)
+ - A file path (../my-package/my-package.whl)
+ - A directory (../my-package/)
+ - A url (https://example.com/packages/my-package-0.1.0.tar.gz)
+ """
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"):
@@ -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("Generated file")
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("Command aborted")
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 {layout_._package_name}> in"
+ f" {path.as_posix()}>"
+ )
+
return 0
def _generate_choice_list(
@@ -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 = []
@@ -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
diff --git a/src/poetry/console/commands/new.py b/src/poetry/console/commands/new.py
index fd32fa6137f..dc2b89e0a2b 100644
--- a/src/poetry/console/commands/new.py
+++ b/src/poetry/console/commands/new.py
@@ -1,13 +1,12 @@
from __future__ import annotations
-from contextlib import suppress
from typing import TYPE_CHECKING
from typing import ClassVar
from cleo.helpers import argument
from cleo.helpers import option
-from poetry.console.commands.command import Command
+from poetry.console.commands.init import InitCommand
if TYPE_CHECKING:
@@ -15,7 +14,7 @@
from cleo.io.inputs.option import Option
-class NewCommand(Command):
+class NewCommand(InitCommand):
name = "new"
description = "Creates a new Python project at ."
@@ -23,6 +22,12 @@ class NewCommand(Command):
argument("path", "The path to create the project at.")
]
options: ClassVar[list[Option]] = [
+ option(
+ "interactive",
+ "i",
+ "Allow interactive specification of project configuration.",
+ flag=True,
+ ),
option("name", None, "Set the resulting package name.", flag=False),
option("src", None, "Use the src layout for the project."),
option(
@@ -31,80 +36,45 @@ class NewCommand(Command):
"Specify the readme file format. One of md (default) or rst",
flag=False,
),
+ *[
+ o
+ for o in InitCommand.options
+ if o.name
+ in {
+ "description",
+ "author",
+ "python",
+ "dependency",
+ "dev-dependency",
+ "license",
+ }
+ ],
]
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.utils.env import EnvManager
-
if self.io.input.option("directory"):
self.line_error(
"--directory only makes sense with existing projects, and will"
" be ignored. You should consider the option --path instead."
)
- layout_cls = layout("src") if self.option("src") else layout("standard")
-
path = Path(self.argument("path"))
if not path.is_absolute():
# we do not use resolve here due to compatibility issues
# for path.resolve(strict=False)
path = Path.cwd().joinpath(path)
- name = self.option("name")
- if not name:
- name = path.name
-
if path.exists() and list(path.glob("*")):
# Directory is not empty. Aborting.
raise RuntimeError(
f"Destination {path}> exists and is not empty"
)
- readme_format = self.option("readme") or "md"
-
- config = GitConfig()
- author = None
- if config.get("user.name"):
- author = config["user.name"]
- author_email = config.get("user.email")
- if author_email:
- author += f" <{author_email}>"
-
- poetry_config = Config.create()
- default_python = (
- "^"
- + EnvManager.get_python_version(
- precision=2,
- prefer_active_python=poetry_config.get(
- "virtualenvs.prefer-active-python"
- ),
- io=self.io,
- ).to_string()
+ return self._init_pyproject(
+ project_path=path,
+ allow_interactive=self.option("interactive"),
+ layout_name="src" if self.option("src") else "standard",
+ readme_format=self.option("readme") or "md",
)
-
- layout_ = layout_cls(
- name,
- "0.1.0",
- author=author,
- readme_format=readme_format,
- python=default_python,
- )
- layout_.create(path)
-
- path = path.resolve()
-
- with suppress(ValueError):
- path = path.relative_to(Path.cwd())
-
- self.line(
- f"Created package {layout_._package_name}> in"
- f" {path.as_posix()}>"
- )
-
- return 0
diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py
index 41b6d6a8d80..f5174ba3951 100644
--- a/src/poetry/layouts/layout.py
+++ b/src/poetry/layouts/layout.py
@@ -103,7 +103,9 @@ def get_package_include(self) -> InlineTable | None:
return package
- def create(self, path: Path, with_tests: bool = True) -> None:
+ def create(
+ self, path: Path, with_tests: bool = True, with_pyproject: bool = True
+ ) -> None:
path.mkdir(parents=True, exist_ok=True)
self._create_default(path)
@@ -112,7 +114,8 @@ def create(self, path: Path, with_tests: bool = True) -> None:
if with_tests:
self._create_tests(path)
- self._write_poetry(path)
+ if with_pyproject:
+ self._write_poetry(path)
def generate_poetry_content(self) -> TOMLDocument:
template = POETRY_DEFAULT
diff --git a/tests/console/commands/conftest.py b/tests/console/commands/conftest.py
new file mode 100644
index 00000000000..8c095a6bb81
--- /dev/null
+++ b/tests/console/commands/conftest.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+import pytest
+
+
+@pytest.fixture
+def init_basic_inputs() -> str:
+ return "\n".join(
+ [
+ "my-package", # Package name
+ "1.2.3", # Version
+ "This is a description", # Description
+ "n", # Author
+ "MIT", # License
+ "~2.7 || ^3.6", # Python
+ "n", # Interactive packages
+ "n", # Interactive dev packages
+ "\n", # Generate
+ ]
+ )
+
+
+@pytest.fixture()
+def init_basic_toml() -> str:
+ return """\
+[tool.poetry]
+name = "my-package"
+version = "1.2.3"
+description = "This is a description"
+authors = ["Your Name "]
+license = "MIT"
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "~2.7 || ^3.6"
+"""
diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py
index 953926fbf3d..2f8ddbce675 100644
--- a/tests/console/commands/test_init.py
+++ b/tests/console/commands/test_init.py
@@ -60,39 +60,6 @@ def tester(patches: None) -> CommandTester:
return CommandTester(app.find("init"))
-@pytest.fixture
-def init_basic_inputs() -> str:
- return "\n".join(
- [
- "my-package", # Package name
- "1.2.3", # Version
- "This is a description", # Description
- "n", # Author
- "MIT", # License
- "~2.7 || ^3.6", # Python
- "n", # Interactive packages
- "n", # Interactive dev packages
- "\n", # Generate
- ]
- )
-
-
-@pytest.fixture()
-def init_basic_toml() -> str:
- return """\
-[tool.poetry]
-name = "my-package"
-version = "1.2.3"
-description = "This is a description"
-authors = ["Your Name "]
-license = "MIT"
-readme = "README.md"
-
-[tool.poetry.dependencies]
-python = "~2.7 || ^3.6"
-"""
-
-
def test_basic_interactive(
tester: CommandTester, init_basic_inputs: str, init_basic_toml: str
) -> None:
diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py
index 9bbef7fff17..3bd1da82fa7 100644
--- a/tests/console/commands/test_new.py
+++ b/tests/console/commands/test_new.py
@@ -229,3 +229,12 @@ def mock_check_output(cmd: str, *_: Any, **__: Any) -> str:
"""
assert expected in pyproject_file.read_text()
+
+
+def test_basic_interactive_new(
+ tester: CommandTester, tmp_path: Path, init_basic_inputs: str, init_basic_toml: str
+) -> None:
+ path = tmp_path / "somepackage"
+ tester.execute(f"--interactive {path.as_posix()}", inputs=init_basic_inputs)
+ verify_project_directory(path, "my-package", "my_package", None)
+ assert init_basic_toml in tester.io.fetch_output()