Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
20 changes: 20 additions & 0 deletions src/poetry/console/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
from cleo.helpers import option
from packaging.utils import canonicalize_name
from tomlkit import inline_table
from tomlkit import parse

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 @@ -265,6 +267,14 @@ def _init_pyproject(

return 1

# Validate fields before creating pyproject.toml file. If any validations fail, throw an error.
# Convert TOML string to a TOMLDocument (a dict-like object) for validation.
pyproject_dict = parse(pyproject.data.as_string())
validation_results = self._validate(pyproject_dict)
Copy link
Member

Choose a reason for hiding this comment

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

Factory.validate(pyproject.data) should suffice.

Copy link
Contributor Author

@rbogart1990 rbogart1990 Jul 7, 2025

Choose a reason for hiding this comment

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

@radoering , thanks for the suggestion. I updated the code to call Factory.validate(pyproject.data) directly. All tests are passing!

I kept it inside of InitCommand()._validate() so that I can unit-test it.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I see. So we are actually just unit testing Factory.validate(), which does not give much value because it is (or at least should be) already tested in test_factory.py.

Further, my comment suggests that converting pyproject.data to string and parsing it is unnecessary. You probably also do this just for unit testing to have a clear interface? I do not like that the production code becomes more complicated just for unit testing.

I think you should check the other (non-interactive) unit tests in test_init.py and use one of these as base for your unit test so that you do not need a separate method to test. Further, this is probably the wrong place to test the regex in detail because as mentioned the validation is part of Factory. Here, we should just check that validation is called and gives a nicely formatted output.

if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
Comment on lines +274 to +280
Copy link
Member

Choose a reason for hiding this comment

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

The output is not so nicely formatted. It contains a raw dict, e.g.:

Validation failed: {'errors': ['project.name must match pattern ^([a-zA-Z\\d]|[a-zA-Z\\d][\\w.-]*[a-zA-Z\\d])$'], 'warnings': []}

You may take a look at https://github.com/python-poetry/poetry-core/blob/002aa3e16f98d21645bb9a45f698b55adc40f317/src/poetry/core/factory.py#L53-L60 for better formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@radoering , thanks for the feedback about this.

Would this format be acceptable?

Validation failed:
  - project.name must match pattern ^([a-zA-Z\d]|[a-zA-Z\d][\w.-]*[a-zA-Z\d])$

Copy link
Member

Choose a reason for hiding this comment

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

Absolutely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@radoering , change made.

Copy link
Member

Choose a reason for hiding this comment

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

You must be kidding. Error messages should be readable and clear for users. Imagine some beginner developer getting hit by that error message. Hell, 9/10 seasoned developers would have a hard time understanding that error message without some sort of regex explorer. This should be a clear message, that a 5 year old is able to understand. I would split the checks and make a separate clear message for each condition, gather the errors and list them clearly. Poetry's motto is "Python packaging and dependency management made easy". Getting hit with a regex is not "easy" by any means.

Copy link
Member

Choose a reason for hiding this comment

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

My comment was just about formatting the output. At this point, we get a dict with lists of messages and the output should just be nicely formatted. The content of the single messages has been created before and is fixed at this point.

In my opinion, it is too much effort to improve each possible message that comes from schema validation - or at least this is clearly out of scope of this PR. The message is the same message if you run poetry check on such an invalid pyproject.toml.


pyproject.save()

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

return self._pool

@staticmethod
def _validate(pyproject_data: dict[str, Any]) -> dict[str, Any]:
"""
Validates the given pyproject data and returns the validation results.
"""
# Instantiate a new Factory to avoid relying on shared/global state,
# which can cause unexpected behavior in other parts of the codebase or test suite.
factory = Factory()
return factory.validate(pyproject_data)
76 changes: 76 additions & 0 deletions tests/console/commands/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

import pytest

Expand Down Expand Up @@ -1143,3 +1144,78 @@ 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[str, Any]:
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: str) -> None:
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: str, reason: str) -> None:
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"])