Repository: python-poetry/poetry (python)

Issue:
Make `poetry init` not add readme section if it doesn't detect a readme

### Issue Kind

Change in current behaviour

### Description

When creating a project using `poetry init`, poetry always generates a `readme` key, even if there is no readme file in the repository. That then makes `poetry install` fail, forcing the user to either create a readme or delete the key.

I think it would be better if poetry detected if the file existed, and only added the `readme` key if the README.md file actually exists

### Impact

Minimal impact ?

### Workarounds

removing `readme` key after doing poetry ini( which is what I do now)


Patch:
diff --git a/src/poetry/console/commands/init.py b/src/poetry/console/commands/init.py
index 6c02048267f..48ea6e206ed 100644
--- a/src/poetry/console/commands/init.py
+++ b/src/poetry/console/commands/init.py
@@ -252,7 +252,7 @@ def _init_pyproject(
         if create_layout:
             layout_.create(project_path, with_pyproject=False)
 
-        content = layout_.generate_project_content()
+        content = layout_.generate_project_content(project_path)
         for section, item in content.items():
             pyproject.data.append(section, item)
 
diff --git a/src/poetry/layouts/layout.py b/src/poetry/layouts/layout.py
index 6dca13aba21..e8d730c577e 100644
--- a/src/poetry/layouts/layout.py
+++ b/src/poetry/layouts/layout.py
@@ -136,7 +136,9 @@ def create(
         if with_pyproject:
             self._write_poetry(path)
 
-    def generate_project_content(self) -> TOMLDocument:
+    def generate_project_content(
+        self, project_path: Path | None = None
+    ) -> TOMLDocument:
         template = POETRY_DEFAULT
 
         content: dict[str, Any] = loads(template)
@@ -160,7 +162,14 @@ def generate_project_content(self) -> TOMLDocument:
         else:
             project_content.remove("license")
 
-        project_content["readme"] = f"README.{self._readme_format}"
+        if project_path:
+            project_dir = project_path / f"README.{self._readme_format}"
+            abs_path = project_dir.resolve()
+
+            if abs_path.exists():
+                project_content["readme"] = f"README.{self._readme_format}"
+            else:
+                project_content.remove("readme")
 
         if self._python:
             project_content["requires-python"] = self._python


Test patch:
diff --git a/tests/console/commands/conftest.py b/tests/console/commands/conftest.py
index cce363ac293..85a6d729ea6 100644
--- a/tests/console/commands/conftest.py
+++ b/tests/console/commands/conftest.py
@@ -36,6 +36,15 @@ def init_basic_toml() -> str:
 """
 
 
+@pytest.fixture
+def init_basic_toml_no_readme(init_basic_toml: str) -> str:
+    # Remove the readme line
+    lines = init_basic_toml.splitlines()
+    lines = [line for line in lines if not line.strip().startswith("readme =")]
+    init_basic_toml_no_readme = "\n".join(lines)
+    return init_basic_toml_no_readme
+
+
 @pytest.fixture()
 def new_basic_toml() -> str:
     return """\
diff --git a/tests/console/commands/test_init.py b/tests/console/commands/test_init.py
index d09d52cfce6..3d12e3015c5 100644
--- a/tests/console/commands/test_init.py
+++ b/tests/console/commands/test_init.py
@@ -62,10 +62,10 @@ def tester(patches: None) -> CommandTester:
 
 
 def test_basic_interactive(
-    tester: CommandTester, init_basic_inputs: str, init_basic_toml: str
+    tester: CommandTester, init_basic_inputs: str, init_basic_toml_no_readme: str
 ) -> None:
     tester.execute(inputs=init_basic_inputs)
-    assert init_basic_toml in tester.io.fetch_output()
+    assert init_basic_toml_no_readme in tester.io.fetch_output()
 
 
 def test_noninteractive(
@@ -146,7 +146,6 @@ def test_interactive_with_dependencies(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)",
@@ -203,7 +202,6 @@ def test_interactive_with_dependencies_and_no_selection(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 """
 
@@ -233,7 +231,6 @@ def test_empty_license(tester: CommandTester) -> None:
 authors = [
     {{name = "Your Name",email = "you@example.com"}}
 ]
-readme = "README.md"
 requires-python = ">={python}"
 """
     assert expected in tester.io.fetch_output()
@@ -273,7 +270,6 @@ def test_interactive_with_git_dependencies(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/demo.git"
@@ -369,7 +365,6 @@ def test_interactive_with_git_dependencies_with_reference(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/demo.git@develop"
@@ -418,7 +413,6 @@ def test_interactive_with_git_dependencies_and_other_name(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ git+https://github.com/demo/pyproject-demo.git"
@@ -474,7 +468,6 @@ def test_interactive_with_directory_dependency(
     {{name = "Your Name",email = "you@example.com"}}
 ]
 license = {{text = "MIT"}}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -529,7 +522,6 @@ def test_interactive_with_directory_dependency_and_other_name(
     {{name = "Your Name",email = "you@example.com"}}
 ]
 license = {{text = "MIT"}}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -585,7 +577,6 @@ def test_interactive_with_file_dependency(
     {{name = "Your Name",email = "you@example.com"}}
 ]
 license = {{text = "MIT"}}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "demo @ {demo_uri}"
@@ -633,7 +624,6 @@ def test_interactive_with_wrong_dependency_inputs(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.8"
 dependencies = [
     "foo (==1.19.2)",
@@ -671,7 +661,6 @@ def test_python_option(tester: CommandTester) -> None:
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 """
 
@@ -703,7 +692,6 @@ def test_predefined_dependency(tester: CommandTester, repo: TestRepository) -> N
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)"
@@ -746,7 +734,6 @@ def test_predefined_and_interactive_dependencies(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)",
@@ -782,7 +769,6 @@ def test_predefined_dev_dependency(tester: CommandTester, repo: TestRepository)
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
 ]
@@ -829,7 +815,6 @@ def test_predefined_and_interactive_dev_dependencies(
     {name = "Your Name",email = "you@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
 ]
@@ -877,7 +862,6 @@ def test_predefined_all_options(tester: CommandTester, repo: TestRepository) ->
     {name = "Foo Bar",email = "foo@example.com"}
 ]
 license = {text = "MIT"}
-readme = "README.md"
 requires-python = ">=3.8"
 dependencies = [
     "pendulum (>=2.0.0,<3.0.0)"
@@ -908,7 +892,7 @@ def test_init_existing_pyproject_simple(
     tester: CommandTester,
     source_dir: Path,
     init_basic_inputs: str,
-    init_basic_toml: str,
+    init_basic_toml_no_readme: str,
 ) -> None:
     pyproject_file = source_dir / "pyproject.toml"
     existing_section = """
@@ -917,8 +901,9 @@ def test_init_existing_pyproject_simple(
 """
     pyproject_file.write_text(existing_section, encoding="utf-8")
     tester.execute(inputs=init_basic_inputs)
-    assert f"{existing_section}\n{init_basic_toml}" in pyproject_file.read_text(
-        encoding="utf-8"
+    assert (
+        f"{existing_section}\n{init_basic_toml_no_readme}"
+        in pyproject_file.read_text(encoding="utf-8")
     )
 
 
@@ -927,7 +912,7 @@ def test_init_existing_pyproject_consistent_linesep(
     tester: CommandTester,
     source_dir: Path,
     init_basic_inputs: str,
-    init_basic_toml: str,
+    init_basic_toml_no_readme: str,
     linesep: str,
 ) -> None:
     pyproject_file = source_dir / "pyproject.toml"
@@ -940,7 +925,7 @@ def test_init_existing_pyproject_consistent_linesep(
     tester.execute(inputs=init_basic_inputs)
     with open(pyproject_file, newline="", encoding="utf-8") as f:
         content = f.read()
-    init_basic_toml = init_basic_toml.replace("\n", linesep)
+    init_basic_toml = init_basic_toml_no_readme.replace("\n", linesep)
     assert f"{existing_section}{linesep}{init_basic_toml}" in content
 
 
@@ -975,7 +960,6 @@ def test_init_non_interactive_existing_pyproject_add_dependency(
 authors = [
     {name = "Your Name",email = "you@example.com"}
 ]
-readme = "README.md"
 requires-python = ">=3.6"
 dependencies = [
     "foo (>=1.19.2,<2.0.0)"
@@ -1089,7 +1073,6 @@ def test_package_include(
         "authors = [\n"
         '    {name = "poetry"}\n'
         "]\n"
-        'readme = "README.md"\n'
         'requires-python = ">=3.10"\n'
         "dependencies = [\n"
         "]\n"
@@ -1206,3 +1189,29 @@ def test_init_does_not_create_project_structure_in_non_empty_directory(
     # Existing files should remain
     assert (source_dir / "existing_file.txt").exists()
     assert (source_dir / "existing_dir").exists()
+
+
+def test_init_adds_readme_key_when_readme_exists(
+    tester: CommandTester, tmp_path: Path
+) -> None:
+    # Arrange: ensure README.md exists
+    readme = tmp_path / "README.md"
+    readme.write_text("# My Project\n", encoding="utf-8")
+    # Act
+    tester.execute(interactive=False)
+    # Assert
+    pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8")
+    assert 'readme = "README.md"' in pyproject
+
+
+def test_init_does_not_add_readme_key_when_readme_missing(
+    tester: CommandTester, tmp_path: Path
+) -> None:
+    # Arrange: ensure README.md does NOT exist
+    readme = tmp_path / "README.md"
+    assert not readme.exists()
+    # Act
+    tester.execute(interactive=False)
+    # Assert
+    pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8")
+    assert "readme =" not in pyproject
diff --git a/tests/console/commands/test_new.py b/tests/console/commands/test_new.py
index 8f0dd360f79..3fa72d5e0f9 100644
--- a/tests/console/commands/test_new.py
+++ b/tests/console/commands/test_new.py
@@ -191,7 +191,12 @@ def test_command_new_with_readme(
     poetry = verify_project_directory(path, package, Path("src") / package)
     project_section = poetry.pyproject.data["project"]
     assert isinstance(project_section, dict)
-    assert project_section["readme"] == f"README.{fmt or 'md'}"
+
+    readme_file = path / f"README.{fmt or 'md'}"
+    assert readme_file.exists()
+
+    assert "readme" in project_section
+    assert project_section["readme"] == readme_file.name
 
 
 @pytest.mark.parametrize(