From 153a04194d46a642797cb9b07ce32a4ee875e869 Mon Sep 17 00:00:00 2001 From: lyijian86-source Date: Thu, 4 Jun 2026 08:54:44 +0800 Subject: [PATCH 1/2] feat(init): allow multiple author options --- docs/cli.md | 4 ++-- src/poetry/console/commands/init.py | 21 +++++++++++++++------ src/poetry/layouts/layout.py | 26 ++++++++++++++------------ tests/console/commands/test_init.py | 21 +++++++++++++++++++++ tests/console/commands/test_new.py | 20 ++++++++++++++++++++ 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 4a950a9b2a5..03a78ba8b1f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -465,7 +465,7 @@ poetry init * `--name`: Name of the package. * `--description`: Description of the package. -* `--author`: Author of the package. +* `--author`: Author of the package. Can be specified multiple times. * `--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`. @@ -717,7 +717,7 @@ my-package 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. +* `--author`: Author of the package. Can be specified multiple times. * `--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`. diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index a0921fdd1eb..17d0862ca5f 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -39,7 +39,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. Can be specified multiple times.", + flag=False, + multiple=True, + ), option("python", None, "Compatible Python versions.", flag=False), option( "dependency", @@ -148,21 +154,24 @@ def _init_pyproject( if not description and is_interactive: description = self.ask(self.create_question("Description []: ", default="")) - author = self.option("author") + authors = self.option("author") + author = authors[0] if 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}>" - if is_interactive: + if is_interactive and len(authors) < 2: question = self.create_question( f"Author [{author}, n to skip]: ", default=author ) - question.set_validator(lambda v: self._validate_author(v, author)) + question.set_validator(lambda v: self._validate_author(v, author or "")) author = self.ask(question) + authors = [author] if author else [] - authors = [author] if author else [] + if not authors and author: + authors = [author] if author else [] license_name = self.option("license") if not license_name and is_interactive: @@ -237,7 +246,7 @@ def _init_pyproject( name, version, description=description, - author=authors[0] if authors else None, + authors=authors or None, readme_format=readme_format, license=license_name, python=python, diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py index cbd406500a8..da34d766320 100644 --- a/src/poetry/layouts/layout.py +++ b/src/poetry/layouts/layout.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from collections.abc import Mapping + from collections.abc import Sequence from tomlkit.items import InlineTable from tomlkit.toml_document import TOMLDocument @@ -66,6 +67,7 @@ def __init__( description: str = "", readme_format: str = "md", author: str | None = None, + authors: Sequence[str] | None = None, license: str | None = None, python: str | None = None, dependencies: Mapping[str, str | Mapping[str, Any]] | None = None, @@ -86,10 +88,9 @@ def __init__( self._dependencies = dependencies or {} self._dev_dependencies = dev_dependencies or {} - if not author: - author = "Your Name " - - self._author = author + self._authors = list(authors or ([author] if author else [])) + if not self._authors: + self._authors = ["Your Name "] @property def basedir(self) -> Path: @@ -147,15 +148,16 @@ def generate_project_content( project_content["name"] = self._project project_content["version"] = self._version project_content["description"] = self._description - m = AUTHOR_REGEX.match(self._author) - if m is None: - # This should not happen because author has been validated before. - raise ValueError(f"Invalid author: {self._author}") - else: - author = {"name": m.group("name")} + for author in self._authors: + m = AUTHOR_REGEX.match(author) + if m is None: + # This should not happen because authors are validated before. + raise ValueError(f"Invalid author: {author}") + + author_content = {"name": m.group("name")} if email := m.group("email"): - author["email"] = email - project_content["authors"].append(author) + author_content["email"] = email + project_content["authors"].append(author_content) if self._license: project_content["license"] = self._license diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 935f6d1d9cb..edfacffc24a 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -879,6 +879,27 @@ def test_predefined_all_options(tester: CommandTester, repo: DummyRepository) -> assert expected in output +def test_noninteractive_multiple_authors( + tester: CommandTester, source_dir: Path +) -> None: + tester.execute( + "--name my-package " + "--author 'Foo Bar ' " + "--author 'Baz Qux '", + interactive=False, + ) + + expected = """\ +authors = [ + {name = "Foo Bar",email = "foo@example.com"}, + {name = "Baz Qux",email = "baz@example.com"} +] +""" + + pyproject_file = source_dir / "pyproject.toml" + assert expected in pyproject_file.read_text(encoding="utf-8") + + def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: command = tester.command assert isinstance(command, InitCommand) diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index 3fa72d5e0f9..ede4d766edd 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -199,6 +199,26 @@ def test_command_new_with_readme( assert project_section["readme"] == readme_file.name +def test_command_new_multiple_authors(tester: CommandTester, tmp_path: Path) -> None: + path = tmp_path / "package" + + tester.execute( + "--author 'Foo Bar ' " + "--author 'Baz Qux ' " + f"{path.as_posix()}" + ) + + pyproject_file = path / "pyproject.toml" + expected = """\ +authors = [ + {name = "Foo Bar",email = "foo@example.com"}, + {name = "Baz Qux",email = "baz@example.com"} +] +""" + + assert expected in pyproject_file.read_text(encoding="utf-8") + + @pytest.mark.parametrize( ["use_poetry_python", "python"], [ From 2a4aaec316deddd5d4ea3a3b9dec2a2b8b62947a Mon Sep 17 00:00:00 2001 From: lyijian86-source Date: Fri, 5 Jun 2026 17:52:58 +0800 Subject: [PATCH 2/2] test(init): harden multiple author coverage --- docs/cli.md | 4 ++-- src/poetry/console/commands/init.py | 30 ++++++++++++++++------------- tests/console/commands/test_init.py | 14 ++++++-------- tests/console/commands/test_new.py | 14 ++++++-------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 03a78ba8b1f..06e00ad9c87 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -467,7 +467,7 @@ poetry init * `--description`: Description of the package. * `--author`: Author of the package. Can be specified multiple times. * `--python` Compatible Python versions. -* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`. +* `--dependency`: Package to require with a version constraint. Should be in the format `foo:1.0.0`. * `--dev-dependency`: Development requirements, see `--dependency`. ## install @@ -719,7 +719,7 @@ my-package * `--description`: Description of the package. * `--author`: Author of the package. Can be specified multiple times. * `--python` Compatible Python versions. -* `--dependency`: Package to require with a version constraint. Should be in format `foo:1.0.0`. +* `--dependency`: Package to require with a version constraint. Should be in the format `foo:1.0.0`. * `--dev-dependency`: Development requirements, see `--dependency`. ## publish diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 17d0862ca5f..3b56ce3cd55 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -154,24 +154,28 @@ def _init_pyproject( if not description and is_interactive: description = self.ask(self.create_question("Description []: ", default="")) - authors = self.option("author") - author = authors[0] if authors else None - if not author and vcs_config.get("user.name"): - author = vcs_config["user.name"] + provided_authors = self.option("author") + default_author = provided_authors[0] if provided_authors else None + if not default_author and vcs_config.get("user.name"): + default_author = vcs_config["user.name"] author_email = vcs_config.get("user.email") if author_email: - author += f" <{author_email}>" + default_author += f" <{author_email}>" - if is_interactive and len(authors) < 2: + if is_interactive and len(provided_authors) < 2: question = self.create_question( - f"Author [{author}, n to skip]: ", default=author + f"Author [{default_author}, n to skip]: ", + default=default_author, ) - question.set_validator(lambda v: self._validate_author(v, author or "")) - author = self.ask(question) - authors = [author] if author else [] - - if not authors and author: - authors = [author] if author else [] + question.set_validator( + lambda v: self._validate_author(v, default_author or "") + ) + selected_author = self.ask(question) + authors = [selected_author] if selected_author else [] + else: + authors = list(provided_authors) if provided_authors else [] + if not authors and default_author: + authors = [default_author] license_name = self.option("license") if not license_name and is_interactive: diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index edfacffc24a..561613efbda 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -17,6 +17,7 @@ from poetry.console.application import Application from poetry.console.commands.init import InitCommand from poetry.repositories import RepositoryPool +from poetry.toml import TOMLFile from tests.helpers import get_package @@ -889,15 +890,12 @@ def test_noninteractive_multiple_authors( interactive=False, ) - expected = """\ -authors = [ - {name = "Foo Bar",email = "foo@example.com"}, - {name = "Baz Qux",email = "baz@example.com"} -] -""" - pyproject_file = source_dir / "pyproject.toml" - assert expected in pyproject_file.read_text(encoding="utf-8") + authors = TOMLFile(pyproject_file).read()["project"]["authors"] + assert [dict(author) for author in authors] == [ + {"name": "Foo Bar", "email": "foo@example.com"}, + {"name": "Baz Qux", "email": "baz@example.com"}, + ] def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py index ede4d766edd..3eb670ebfc4 100644 --- a/tests/console/commands/test_new.py +++ b/tests/console/commands/test_new.py @@ -10,6 +10,7 @@ from poetry.core.utils.helpers import module_name from poetry.factory import Factory +from poetry.toml import TOMLFile if TYPE_CHECKING: @@ -209,14 +210,11 @@ def test_command_new_multiple_authors(tester: CommandTester, tmp_path: Path) -> ) pyproject_file = path / "pyproject.toml" - expected = """\ -authors = [ - {name = "Foo Bar",email = "foo@example.com"}, - {name = "Baz Qux",email = "baz@example.com"} -] -""" - - assert expected in pyproject_file.read_text(encoding="utf-8") + authors = TOMLFile(pyproject_file).read()["project"]["authors"] + assert [dict(author) for author in authors] == [ + {"name": "Foo Bar", "email": "foo@example.com"}, + {"name": "Baz Qux", "email": "baz@example.com"}, + ] @pytest.mark.parametrize(