diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index a0921fdd1eb..5040dc86ceb 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -11,6 +11,8 @@ from cleo.helpers import option from packaging.utils import canonicalize_name +from poetry.core.utils.patterns import AUTHOR_REGEX +from tomlkit import array from tomlkit import inline_table from poetry.console.commands.command import Command @@ -39,7 +41,13 @@ class InitCommand(Command): options: ClassVar[list[Option]] = [ option("name", None, "Name of the package.", flag=False), option("description", None, "Description of the package.", flag=False), - option("author", None, "Author name of the package.", flag=False), + option( + "author", + None, + "Author name of the package.", + flag=False, + multiple=True, + ), option("python", None, "Compatible Python versions.", flag=False), option( "dependency", @@ -148,21 +156,42 @@ def _init_pyproject( if not description and is_interactive: description = self.ask(self.create_question("Description []: ", default="")) - author = self.option("author") - if not author and vcs_config.get("user.name"): - author = vcs_config["user.name"] - author_email = vcs_config.get("user.email") - if author_email: - author += f" <{author_email}>" - - if is_interactive: - question = self.create_question( - f"Author [{author}, n to skip]: ", default=author + raw_option_authors = self.option("author") + if raw_option_authors is None: + option_authors: list[str] = [] + elif isinstance(raw_option_authors, str): + option_authors = [raw_option_authors] + else: + option_authors = list(raw_option_authors) + + option_authors = [ + author + for author in ( + self._validate_author(author, "") for author in option_authors ) - question.set_validator(lambda v: self._validate_author(v, author)) - author = self.ask(question) + if author + ] + authors: list[str] = [] + + if len(option_authors) > 1 or (option_authors and not is_interactive): + authors = option_authors + else: + author = option_authors[0] if option_authors else None + if not author and vcs_config.get("user.name"): + author = vcs_config["user.name"] + author_email = vcs_config.get("user.email") + if author_email: + author += f" <{author_email}>" - authors = [author] if author else [] + 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 or "")) + author = self.ask(question) + + authors = [author] if author else [] license_name = self.option("license") if not license_name and is_interactive: @@ -253,6 +282,11 @@ def _init_pyproject( layout_.create(project_path, with_pyproject=False) content = layout_.generate_project_content(project_path) + if len(authors) > 1: + content["project"]["authors"] = array().multiline(True) + for author in authors: + content["project"]["authors"].append(self._format_author(author)) + for section, item in content.items(): pyproject.data.append(section, item) @@ -498,7 +532,6 @@ def _format_requirements(self, requirements: list[dict[str, str]]) -> Requiremen @staticmethod def _validate_author(author: str, default: str) -> str | None: from poetry.core.utils.helpers import combine_unicode - from poetry.core.utils.patterns import AUTHOR_REGEX author = combine_unicode(author or default) @@ -514,6 +547,19 @@ def _validate_author(author: str, default: str) -> str | None: return author + @staticmethod + def _format_author(author: str) -> dict[str, str]: + m = AUTHOR_REGEX.match(author) + if m is None: + # This should not happen because author has been validated before. + raise ValueError(f"Invalid author: {author}") + + formatted_author = {"name": m.group("name")} + if email := m.group("email"): + formatted_author["email"] = email + + return formatted_author + @staticmethod def _validate_package(package: str | None) -> str | None: if package and len(package.split()) > 2: diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 935f6d1d9cb..be6e221296f 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -972,6 +972,33 @@ def test_init_non_interactive_existing_pyproject_add_dependency( ) +def test_init_non_interactive_with_multiple_authors( + tester: CommandTester, source_dir: Path +) -> None: + tester.execute( + "--author 'Alice Example ' " + "--author 'Bob Example ' " + "--name 'my-package' " + "--python '>=3.12'", + interactive=False, + ) + + expected = """\ +[project] +name = "my-package" +version = "0.1.0" +description = "" +authors = [ + {name = "Alice Example",email = "alice@example.com"}, + {name = "Bob Example",email = "bob@example.com"}, +] +requires-python = ">=3.12" +dependencies = [ +] +""" + assert expected in (source_dir / "pyproject.toml").read_text(encoding="utf-8") + + def test_init_existing_pyproject_with_build_system_fails( tester: CommandTester, source_dir: Path, init_basic_inputs: str ) -> None: