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 fe708c98ab4..53efafa7826 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 @@ -69,13 +70,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"): @@ -86,6 +80,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(): @@ -105,7 +117,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" @@ -115,21 +127,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") @@ -139,22 +154,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, @@ -163,13 +179,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 = {} @@ -180,27 +197,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"): @@ -211,44 +226,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( @@ -276,7 +308,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 = [] @@ -366,7 +402,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 5f896cf80e8..b9f853cf228 100644 --- a/src/poetry/console/commands/new.py +++ b/src/poetry/console/commands/new.py @@ -1,19 +1,23 @@ from __future__ import annotations -from contextlib import suppress - from cleo.helpers import argument from cleo.helpers import option -from poetry.console.commands.command import Command +from poetry.console.commands.init import InitCommand -class NewCommand(Command): +class NewCommand(InitCommand): name = "new" description = "Creates a new Python project at ." arguments = [argument("path", "The path to create the project at.")] options = [ + 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( @@ -22,80 +26,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() - ) - - 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 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", ) - - 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()