From b3a4442528893e117e638d54fd2b71b508cbf3d9 Mon Sep 17 00:00:00 2001 From: fahadhewad Date: Wed, 23 Sep 2026 09:26:35 +0000 Subject: [PATCH 1/2] Allow multiple authors to be passed to poetry init and poetry new The --author option was declared as a single value option, so passing it twice kept only the last value, and passing a comma separated list wrote a malformed email address into pyproject.toml. Neither case reported an error. Declare --author as a multiple option, the same way --dependency and --dev-dependency are declared, and let Layout take a list of authors instead of one. poetry new reuses the option from InitCommand, so it accepts several authors too. In interactive mode only the first author is prompted for, and any further author given on the command line is kept. --- docs/cli.md | 4 +-- src/poetry/console/commands/init.py | 24 +++++++++----- src/poetry/layouts/layout.py | 27 ++++++++-------- tests/conftest.py | 2 +- tests/console/commands/test_init.py | 50 +++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 25 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index b91b637ebab..4e8e2ca1db4 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. Repeat the option to declare more than one author. * `--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`. @@ -718,7 +718,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. Repeat the option to declare more than one author. * `--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 e18e41d9253..23905bb95d9 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -39,7 +39,9 @@ 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 +150,27 @@ 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"): + authors = list(self.option("author")) + if not authors 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 is_interactive: + # Only the first author is asked for interactively, any further author + # given on the command line is kept as is. + default_author = authors[0] if authors else None 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, default_author or "") ) - question.set_validator(lambda v: self._validate_author(v, author)) author = self.ask(question) - - authors = [author] if author else [] + authors = [author, *authors[1:]] if author else [] license_name = self.option("license") if not license_name and is_interactive: @@ -237,7 +245,7 @@ def _init_pyproject( name, version, description=description, - author=authors[0] if authors else None, + authors=authors, 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..a4f5e63dfd2 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 @@ -65,7 +66,7 @@ def __init__( version: str = "0.1.0", 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 +87,7 @@ 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) if authors else ["Your Name "] @property def basedir(self) -> Path: @@ -147,15 +145,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")} - if email := m.group("email"): - author["email"] = email - project_content["authors"].append(author) + for author_str in self._authors: + m = AUTHOR_REGEX.match(author_str) + if m is None: + # This should not happen because author has been validated before. + raise ValueError(f"Invalid author: {author_str}") + else: + author = {"name": m.group("name")} + if email := m.group("email"): + author["email"] = email + project_content["authors"].append(author) if self._license: project_content["license"] = self._license diff --git a/tests/conftest.py b/tests/conftest.py index e7ad8309183..2ed8fcd0964 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -487,7 +487,7 @@ def _factory( layout("src")( name, "0.1.0", - author="PyTest Tester ", + authors=["PyTest Tester "], readme_format="md", python=default_python, dependencies=dependencies, diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py index 1e866d8dc8f..33a413835bc 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -922,6 +922,56 @@ def test_predefined_all_options(tester: CommandTester, repo: DummyRepository) -> assert expected in output +def test_multiple_authors(tester: CommandTester, source_dir: Path) -> None: + tester.execute( + "--name my-package " + "--author 'Foo Bar ' " + "--author 'Baz Qux ' " + "--python '>=3.8'", + interactive=False, + ) + + expected = """\ +authors = [ + {name = "Foo Bar",email = "foo@example.com"}, + {name = "Baz Qux",email = "baz@example.com"} +] +""" + + assert expected in (source_dir / "pyproject.toml").read_text(encoding="utf-8") + + +def test_multiple_authors_interactive_only_asks_for_the_first( + tester: CommandTester, +) -> None: + inputs = [ + "1.2.3", # Version + "", # Author, keeps the first one given on the command line + "n", # Interactive packages + "n", # Interactive dev packages + "\n", # Generate + ] + + tester.execute( + "--name my-package " + "--description 'This is a description' " + "--author 'Foo Bar ' " + "--author 'Baz Qux ' " + "--python '>=3.8' " + "--license MIT", + inputs="\n".join(inputs), + ) + + expected = """\ +authors = [ + {name = "Foo Bar",email = "foo@example.com"}, + {name = "Baz Qux",email = "baz@example.com"} +] +""" + + assert expected in tester.io.fetch_output() + + def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: command = tester.command assert isinstance(command, InitCommand) From 210c02f0266cf8c35daf90be5a64174bfbcf1a3e Mon Sep 17 00:00:00 2001 From: fahadhewad Date: Thu, 1 Oct 2026 13:35:18 +0000 Subject: [PATCH 2/2] Keep remaining authors when the first one is skipped interactively Answering n to the interactive author prompt set the author list to empty, so any further author passed with --author on the command line was dropped. Only the prompted first author should be skipped. Drop just the first entry instead, and add a regression test covering an interactive run that skips the prompted author while a second one is given on the command line. --- src/poetry/console/commands/init.py | 2 +- tests/console/commands/test_init.py | 30 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py index 23905bb95d9..c831a5a5129 100644 --- a/src/poetry/console/commands/init.py +++ b/src/poetry/console/commands/init.py @@ -170,7 +170,7 @@ def _init_pyproject( lambda v: self._validate_author(v, default_author or "") ) author = self.ask(question) - authors = [author, *authors[1:]] if author else [] + authors = [author, *authors[1:]] if author else authors[1:] 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 33a413835bc..730ba73f7e5 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -972,6 +972,36 @@ def test_multiple_authors_interactive_only_asks_for_the_first( assert expected in tester.io.fetch_output() +def test_multiple_authors_interactive_skipping_the_first_keeps_the_rest( + tester: CommandTester, +) -> None: + inputs = [ + "1.2.3", # Version + "n", # Author, skips the first one given on the command line + "n", # Interactive packages + "n", # Interactive dev packages + "\n", # Generate + ] + + tester.execute( + "--name my-package " + "--description 'This is a description' " + "--author 'Foo Bar ' " + "--author 'Baz Qux ' " + "--python '>=3.8' " + "--license MIT", + inputs="\n".join(inputs), + ) + + expected = """\ +authors = [ + {name = "Baz Qux",email = "baz@example.com"} +] +""" + + assert expected in tester.io.fetch_output() + + def test_add_package_with_extras_and_whitespace(tester: CommandTester) -> None: command = tester.command assert isinstance(command, InitCommand)