Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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`.
Expand Down
24 changes: 16 additions & 8 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 [<comment>{author}</comment>, n to skip]: ", default=author
f"Author [<comment>{default_author}</comment>, 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:
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 13 additions & 14 deletions src/poetry/layouts/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -86,10 +87,7 @@ def __init__(
self._dependencies = dependencies or {}
self._dev_dependencies = dev_dependencies or {}

if not author:
author = "Your Name <you@example.com>"

self._author = author
self._authors = list(authors) if authors else ["Your Name <you@example.com>"]

@property
def basedir(self) -> Path:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ def _factory(
layout("src")(
name,
"0.1.0",
author="PyTest Tester <mc.testy@testface.com>",
authors=["PyTest Tester <mc.testy@testface.com>"],
readme_format="md",
python=default_python,
dependencies=dependencies,
Expand Down
80 changes: 80 additions & 0 deletions tests/console/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <foo@example.com>' "
"--author 'Baz Qux <baz@example.com>' "
"--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 <foo@example.com>' "
"--author 'Baz Qux <baz@example.com>' "
"--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 <foo@example.com>' "
"--author 'Baz Qux <baz@example.com>' "
"--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)
Expand Down
Loading