Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
28 changes: 20 additions & 8 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand
from poetry.factory import Factory
from poetry.utils.dependency_specification import RequirementsParser
from poetry.utils.env.python import Python

Expand Down Expand Up @@ -126,15 +127,13 @@ def _init_pyproject(
)
self.line("")

name = self.option("name")
if not name:
name = project_path.name.lower()
name = self.option("name") or project_path.name.lower()

if is_interactive:
question = self.create_question(
f"Package name [<comment>{name}</comment>]: ", default=name
)
name = self.ask(question)
if is_interactive:
question = self.create_question(
f"Package name [<comment>{name}</comment>]: ", default=name
)
name = self.ask(question)

version = "0.1.0"

Expand Down Expand Up @@ -265,6 +264,12 @@ def _init_pyproject(

return 1

# validate fields before creating pyproject.toml file. If any validations fail, throw error.
validation_results = self._validate(pyproject.data)
if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
Comment on lines +274 to +276
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider formatting validation errors for readability

Displaying only relevant error messages or a summary instead of the full dictionary will make the output clearer for users.

Suggested change
if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
if validation_results.get("errors"):
errors = validation_results["errors"]
if isinstance(errors, dict):
error_list = [f"- {field}: {msg}" for field, msg in errors.items()]
elif isinstance(errors, list):
error_list = [f"- {msg}" for msg in errors]
else:
error_list = [str(errors)]
formatted_errors = "\n".join(error_list)
self.line_error(f"<error>Validation failed with the following errors:\n{formatted_errors}</error>")
return 1

Comment on lines +274 to +276
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Printing the full validation_results dict may expose internal details

Extract and display only the relevant error messages to prevent leaking internal or sensitive information.

Suggested change
if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
if validation_results.get("errors"):
+ error_messages = validation_results.get("errors")
+ if isinstance(error_messages, dict):
+ error_messages = list(error_messages.values())
+ if isinstance(error_messages, list):
+ for error in error_messages:
+ self.line_error(f"<error>Validation error: {error}</error>")
+ else:
+ self.line_error("<error>Validation failed due to unknown error format.</error>")
+ return 1


pyproject.save()

if create_layout:
Expand Down Expand Up @@ -533,3 +538,10 @@ def _get_pool(self) -> RepositoryPool:
self._pool.add_repository(PyPiRepository(pool_size=pool_size))

return self._pool

@staticmethod
def _validate(pyproject_data: dict) -> dict:
"""
Validates the given pyproject data and returns the validation results.
"""
return Factory.validate(pyproject_data)
73 changes: 73 additions & 0 deletions tests/console/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,3 +1143,76 @@ def test_get_pool(mocker: MockerFixture, source_dir: Path) -> None:
assert isinstance(command, InitCommand)
pool = command._get_pool()
assert pool.repositories


def build_pyproject_data(project_name: str, description: str = "A project") -> dict:
return {
"project": {
"name": project_name,
"version": "0.1.0",
"description": description,
"authors": [{"name": "Author Name", "email": "author@example.com"}],
"readme": "README.md",
"requires-python": ">=3.13",
"dependencies": [],
},
"tool": {},
"build-system": {
"requires": ["poetry-core>=2.0.0,<3.0.0"],
"build-backend": "poetry.core.masonry.api",
},
}


@pytest.mark.parametrize(
"valid_project_name",
[
"newproject",
"new_project",
"new-project",
"new.project",
"newproject123",
],
)
def test_valid_project_name(valid_project_name):
pyproject_data = build_pyproject_data(valid_project_name)
result = InitCommand._validate(pyproject_data)
assert result["errors"] == []


@pytest.mark.parametrize(
"invalid_project_name, reason",
[
("new+project", "plus sign"),
("new/project", "slash"),
("new@project", "at sign"),
("new project", "space"),
("", "empty string"),
(" newproject", "leading space"),
("newproject ", "trailing space"),
("new#project", "hash (#)"),
("new%project", "percent (%)"),
("new*project", "asterisk (*)"),
("new(project)", "parentheses"),
("-newproject", "leading hyphen"),
("newproject-", "trailing hyphen"),
(".newproject", "leading dot"),
("newproject.", "trailing dot"),
(
"_newproject",
"leading underscore (PEP 621 allows, stricter validators may reject)",
),
(
"newproject_",
"trailing underscore (PEP 621 allows, stricter validators may reject)",
),
("1newproject!", "starts with digit, ends with exclamation"),
(".", "just dot"),
],
)
def test_invalid_project_name(invalid_project_name, reason):
pyproject_data = build_pyproject_data(invalid_project_name)
result = InitCommand._validate(pyproject_data)

assert "errors" in result, f"Expected error for: {reason}"
assert any("project.name must match pattern" in err for err in result["errors"])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing test for interactive mode with invalid project name.

Please add an integration test for interactive mode that simulates invalid project name input and verifies the correct error handling and process exit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I struggled to create an integration test for this. I wasn't able to get it to work.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s one way to get an “end‐to‐end” / integration‐style test working without having to wrestle with fully interactive prompts:

  1. Use the --no-interaction flag so you can drive the command entirely by CLI options (no stdin juggling).
  2. Pass your invalid name via the --name option.
  3. Assert that Poetry aborts with a non‐zero exit code and that the validation message appears on stdout/stderr.

For example, drop something like this into tests/console/commands/test_init.py:

from pathlib import Path
import pytest
from click.testing import CliRunner

from poetry.console.application import Application


@pytest.mark.parametrize(
    "invalid_name, reason",
    [
        ("new+project", "plus sign"),
        ("new/project", "slash"),
        # … your other cases …
    ],
)
def test_init_non_interactive_invalid_name(tmp_path: Path, invalid_name: str, reason: str):
    # arrange: run inside an empty temp dir
    runner = CliRunner()
    app = Application()

    # act: invoke `poetry init --no-interaction --name <invalid>`
    result = runner.invoke(
        app,
        ["init", "--no-interaction", "--name", invalid_name],
        obj={},
        catch_exceptions=False,
    )

    # assert: we expect a failure exit code
    assert result.exit_code != 0, f"Should have failed for {reason}"
    # and the error message refers to the schema‐validation pattern
    assert "project.name must match pattern" in result.output
    # (you can tighten this to exactly match your error wording)

Why this works:

  • --no-interaction makes Poetry skip all the “press Enter to accept defaults” prompts.
  • You drive the only input you care about (--name) on the CLI.
  • You can then assert on exit_code and on result.output (which includes both stdout & stderr).

If you really want to simulate a fully interactive session (e.g. to catch the prompt, type a bad name, then press Enter several times), you can still do that with CliRunner.invoke(..., input="bad+name\n\n\n"), but in my experience the --no-interaction approach is far simpler and still 100% exercise the same validation logic.

Give that a try, and let me know if you hit any snags!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding tests for borderline valid/invalid names and unicode.

Adding tests for names at the validity boundary, including maximum length, unicode, and mixed case, will help ensure comprehensive validation coverage.