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..c831a5a5129 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 authors[1:] 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..730ba73f7e5 100644 --- a/tests/console/commands/test_init.py +++ b/tests/console/commands/test_init.py @@ -922,6 +922,86 @@ 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_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)