Skip to content

Conversation

@rbogart1990
Copy link
Contributor

@rbogart1990 rbogart1990 commented Jun 16, 2025

This PR introduces validation for user input values during the poetry init command. It uses the rules defined in the project-schema.json schema. These rules are already enforced when running the poetry add command, so this change extends the same validation rules to the poetry init command.

Previously, user inputs were not validated against these schema rules, allowing the creation of Poetry projects with invalid or disallowed names (e.g. "new+project").

This change ensures that inputs conform to the schema before proceeding, preventing invalid project configurations.

Pull Request Check List

Resolves: #10170

  • Added tests for changed code.
  • Updated documentation for changed code.

Summary by Sourcery

Add schema-based validation to the poetry init command to ensure project metadata, especially the project name, conforms to the defined project-schema rules and abort initialization on validation failures.

New Features:

  • Validate project metadata during poetry init using the existing project-schema JSON rules

Bug Fixes:

  • Prevent creation of Poetry projects with invalid or disallowed names during initialization

Enhancements:

  • Extract a dedicated InitCommand._validate method for reusable schema validation
  • Simplify interactive project name assignment by unifying option and default logic

Tests:

  • Add unit tests covering valid project names
  • Add unit tests covering various invalid project names

@sourcery-ai
Copy link

sourcery-ai bot commented Jun 16, 2025

Reviewer's Guide

This PR extends schema-based validation to the poetry init command by introducing a new _validate method that uses the existing project schema, refactors the package name assignment logic for conciseness, and adds parameterized tests for valid and invalid project names.

Sequence diagram for poetry init with schema validation

sequenceDiagram
    actor User
    participant CLI as Poetry CLI
    participant InitCmd as InitCommand
    participant Factory

    User->>CLI: Run 'poetry init'
    CLI->>InitCmd: Start initialization
    InitCmd->>User: Prompt for project name
    User->>InitCmd: Provide project name
    InitCmd->>InitCmd: Collect other inputs
    InitCmd->>InitCmd: Build pyproject data
    InitCmd->>InitCmd: Call _validate(pyproject_data)
    InitCmd->>Factory: validate(pyproject_data)
    Factory-->>InitCmd: Return validation results
    alt Validation errors
        InitCmd->>User: Show validation error and abort
    else Validation passes
        InitCmd->>InitCmd: Save pyproject.toml
        InitCmd->>User: Complete initialization
    end
Loading

Class diagram for InitCommand validation changes

classDiagram
    class InitCommand {
        +_init_pyproject(...)
        +_get_pool()
        +_validate(pyproject_data: dict) dict
    }
    class Factory {
        +validate(pyproject_data: dict) dict
    }
    InitCommand ..> Factory : uses
Loading

File-Level Changes

Change Details Files
Introduce validation step in poetry init to enforce project-schema rules
  • Invoke _validate on generated pyproject data before saving
  • Abort initialization with an error message and non-zero exit code if validation errors are found
src/poetry/console/commands/init.py
Add static _validate method to InitCommand
  • Define _validate(pyproject_data: dict) -> dict that delegates to Factory.validate
  • Include a docstring explaining its purpose
src/poetry/console/commands/init.py
Refactor default package name assignment
  • Replace separate if not name block with a single option or directory expression
  • Remove redundant branching around interactive name prompt
src/poetry/console/commands/init.py
Add parameterized tests for project name validation
  • Introduce build_pyproject_data helper to assemble test data
  • Add tests for valid project names that should pass
  • Add tests for invalid project names that should trigger schema errors
tests/console/commands/test_init.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @rbogart1990 - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/poetry/console/commands/init.py:270` </location>
<code_context>
+        # 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
+
</code_context>

<issue_to_address>
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.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        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
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `tests/console/commands/test_init.py:1213` </location>
<code_context>
+        (".", "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"])
</code_context>

<issue_to_address>
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.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +269 to +271
if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
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 1213 to 1218
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

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @rbogart1990 - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/poetry/console/commands/init.py:133` </location>
<code_context>
-            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
</code_context>

<issue_to_address>
Interactive prompt now always runs even when --name is provided

If this behavior is unintentional, move the prompt back inside the `if not name` block to avoid prompting when --name is provided.
</issue_to_address>

### Comment 2
<location> `src/poetry/console/commands/init.py:270` </location>
<code_context>
+        # 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
+
</code_context>

<issue_to_address>
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.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        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
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `tests/console/commands/test_init.py:1213` </location>
<code_context>
+        (".", "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"])
</code_context>

<issue_to_address>
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.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +269 to +271
if validation_results.get("errors"):
self.line_error(f"<error>Validation failed: {validation_results}</error>")
return 1
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

Comment on lines 1213 to 1218
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): 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.

@rbogart1990 rbogart1990 changed the title Feature/invalid project name Validate user input against project-schema.json Jun 17, 2025
@rbogart1990 rbogart1990 closed this Jul 1, 2025
@rbogart1990
Copy link
Contributor Author

This is no longer needed. Its been replaced with this PR, which has the same code changes, but with a cleaned up commit history.

@github-actions
Copy link

github-actions bot commented Aug 1, 2025

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 1, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prevent poetry from creating projects with invalid project names

1 participant